From be02433ecc9c44ad9316502a60becd3dcaaad38d Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:45:30 +0000 Subject: [PATCH 1/3] feat: set RPC decode concurrency with Rpc.ParseOptions Effect 4 ignores schema parseOptions annotations, so constructors no longer stamp unbounded concurrency onto Struct, Array, Record, and schema classes. Client and server RPC groups now carry DefaultParseOptions through Rpc.ParseOptions, after prefix and middleware. effect@4.0.6 is not on npm, so patches/effect@4.0.5.patch backports that annotation until it can be installed. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- .changeset/rpc-parse-options.md | 10 + packages/effect-app/src/Schema.ts | 10 +- packages/effect-app/src/Schema/Class.ts | 21 +- packages/effect-app/src/Schema/ext.ts | 14 +- .../effect-app/src/client/apiClientFactory.ts | 3 +- packages/effect-app/src/rpc.ts | 1 + .../effect-app/src/rpc/RpcParseOptions.ts | 13 ++ .../effect-app/test/rpc-parse-options.test.ts | 193 +++++++++++++++++ packages/infra/src/routing.ts | 60 +++--- patches/effect@4.0.5.patch | 198 ++++++++++++++++++ pnpm-lock.yaml | 69 +++--- pnpm-workspace.yaml | 1 + 12 files changed, 495 insertions(+), 98 deletions(-) create mode 100644 .changeset/rpc-parse-options.md create mode 100644 packages/effect-app/src/rpc/RpcParseOptions.ts create mode 100644 packages/effect-app/test/rpc-parse-options.test.ts create mode 100644 patches/effect@4.0.5.patch diff --git a/.changeset/rpc-parse-options.md b/.changeset/rpc-parse-options.md new file mode 100644 index 000000000..9a2e6f7fd --- /dev/null +++ b/.changeset/rpc-parse-options.md @@ -0,0 +1,10 @@ +--- +"effect-app": patch +"@effect-app/infra": patch +--- + +Adopt effect's `Rpc.ParseOptions` for RPC encode and decode concurrency, and drop the per-schema `parseOptions` concurrency annotations. + +Effect 4 ignores `parseOptions` annotations on schemas. Client and server RPC groups now carry `DefaultParseOptions` (unbounded unless `SCHEMA_CONCURRENCY` is set) via `annotateRpcParseOptions`, applied after prefix and middleware. `decodeEffectConcurrently` is unchanged for call sites that pass parse options themselves. + +`effect@4.0.6` is not on npm yet, so this repo temporarily patches `effect@4.0.5` with the `Rpc.ParseOptions` backport from Effect-TS/effect#9665. Drop `patches/effect@4.0.5.patch` once `effect@4.0.6` can be installed. Published packages call `Rpc.ParseOptions` directly, so a consumer still on stock `effect@4.0.5` without that patch cannot import them. diff --git a/packages/effect-app/src/Schema.ts b/packages/effect-app/src/Schema.ts index 01eecece8..336e1b2f2 100644 --- a/packages/effect-app/src/Schema.ts +++ b/packages/effect-app/src/Schema.ts @@ -6,7 +6,7 @@ import type { RequiredKeys } from "effect/Types" import type { Arbitrary as FastCheckArbitrary } from "fast-check" import type { NonEmptyReadonlyArray } from "./Array.ts" import { Email as EmailT, type Email as EmailType } from "./Schema/email.ts" -import { concurrencyUnbounded, withDefaultMake, withDefaultParseOptions } from "./Schema/ext.ts" +import { 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" @@ -156,7 +156,7 @@ export const Void: Void = S.Void export function Struct( fields: Fields ): Struct { - const result = S.Struct(fields).annotate(concurrencyUnbounded) + const result = S.Struct(fields) const allowVoidMake = (schema: any): any => { // Normalize omitted input to an empty object so optional/default-only structs can be constructed with make(). // Do not read schema.make first: Effect 4 caches it as a non-writable own property. @@ -194,7 +194,7 @@ export function Struct( } ;(result as any).mapFields = function(this: any, f: any, options?: any) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const mapped = origMapFields.call(this, f, options).annotate(concurrencyUnbounded) + const mapped = origMapFields.call(this, f, options) return preserveCopyAndMethods(mapped) } ;(result as any).annotate = function(this: any, annotations?: any) { @@ -296,7 +296,7 @@ export type StructNestedEncoded = T extends { fields: infer Fields extends S. : StructNestedEncodedError export function NonEmptyArray(value: Value): S.NonEmptyArray { - return S.NonEmptyArray(value).annotate(concurrencyUnbounded) + return S.NonEmptyArray(value) } export function TaggedStruct( @@ -336,7 +336,7 @@ export function Record( key: Key, value: Value ): S.$Record { - return S.Record(key, value).annotate(concurrencyUnbounded) + return S.Record(key, value) } export declare namespace Record { export type Key = S.Record.Key diff --git a/packages/effect-app/src/Schema/Class.ts b/packages/effect-app/src/Schema/Class.ts index a13163e22..c52ef31c5 100644 --- a/packages/effect-app/src/Schema/Class.ts +++ b/packages/effect-app/src/Schema/Class.ts @@ -8,7 +8,6 @@ import * as SchemaTransformation from "effect/SchemaTransformation" import type { Struct as EffectAppStruct } from "../Schema.ts" import * as SchemaAST from "../SchemaAST.ts" import { copyOrigin } from "../utils.ts" -import { concurrencyUnbounded } from "./ext.ts" import * as SchemaParser from "./SchemaParser.ts" type ClassAnnotations = S.Annotations.Declaration @@ -130,7 +129,7 @@ export const Class: = (identifier) => (fields, annotations, options) => { const relaxed = options?.strict === false // Build the original Schema.Class - const Base = (S.Class as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations }) + const Base = (S.Class as any)(identifier)(fields, annotations) // Get the original ast getter from the base class const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! @@ -157,9 +156,6 @@ export const Class: = (identifier) => (tag, fields, annotations, options) => { const relaxed = options?.strict === false - const Base = (S.TaggedClass as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations }) + const Base = (S.TaggedClass as any)(identifier)(tag, fields, annotations) const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! const astCache = new WeakMap() const copyCache = new WeakMap>() @@ -219,9 +215,6 @@ export const TaggedClass: = (identifier) => (fields, annotations, options) => { const relaxed = options?.strict === false - const Base = (S.Error as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations }) + const Base = (S.Error as any)(identifier)(fields, annotations) const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! const astCache = new WeakMap() const copyCache = new WeakMap>() @@ -264,9 +257,6 @@ export const ErrorClass: = (identifier) => (tag, fields, annotations, options) => { const relaxed = options?.strict === false - const Base = (S.TaggedError as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations }) + const Base = (S.TaggedError as any)(identifier)(tag, fields, annotations) const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! const astCache = new WeakMap() const copyCache = new WeakMap>() @@ -310,9 +300,6 @@ export const TaggedErrorClass: "unbounded" as const) } -/** - * Parse-options annotation used on schema constructors for decode paths where callers - * cannot currently pass parse options (notably some RPC / HttpApi integration paths). - * - * Keep this annotation in place so those framework-managed decodes still run with - * unbounded concurrency by default. - */ -export const concurrencyUnbounded = { parseOptions: DefaultParseOptions } as const - type DecodeLike = (schema: any) => (input: any, options?: SchemaAST.ParseOptions) => any export const withDefaultParseOptions = ( @@ -245,7 +236,7 @@ export const Literals = (value: ValueSchema) { return pipe( - S.Array(value).annotate(concurrencyUnbounded), + S.Array(value), (s) => Object.assign(s, { /** @@ -273,7 +264,7 @@ export function Array(value: ValueSchema) { export const ReadonlySetFromArray = (value: ValueSchema) => { const from = S .Array(value) - .annotate({ ...concurrencyUnbounded, expected: "an array of unique items that will be decoded as a ReadonlySet" }) + .annotate({ expected: "an array of unique items that will be decoded as a ReadonlySet" }) const to = S.instanceOf(Set) as S.instanceOf> const schema = from.pipe( S.decodeTo( @@ -297,7 +288,6 @@ export const ReadonlyMapFromArray = [keyof newM], `${ModuleName}.`> > - return rpcs + return annotateRpcParseOptions(rpcs) } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/effect-app/src/rpc.ts b/packages/effect-app/src/rpc.ts index e07797b67..ee58fad40 100644 --- a/packages/effect-app/src/rpc.ts +++ b/packages/effect-app/src/rpc.ts @@ -2,3 +2,4 @@ export * as Invalidation from "./rpc/Invalidation.ts" export * as MiddlewareMaker from "./rpc/MiddlewareMaker.ts" export * as RpcContextMap from "./rpc/RpcContextMap.ts" export * as RpcMiddleware from "./rpc/RpcMiddleware.ts" +export { annotateRpcParseOptions } from "./rpc/RpcParseOptions.ts" diff --git a/packages/effect-app/src/rpc/RpcParseOptions.ts b/packages/effect-app/src/rpc/RpcParseOptions.ts new file mode 100644 index 000000000..06972139d --- /dev/null +++ b/packages/effect-app/src/rpc/RpcParseOptions.ts @@ -0,0 +1,13 @@ +import { Rpc, type RpcGroup } from "effect/rpc" +import { DefaultParseOptions } from "../Schema/ext.ts" + +/** + * Apply {@link DefaultParseOptions} to every RPC in `group`. + * + * `RpcClient` and `RpcServer` read `Rpc.ParseOptions` when encoding and decoding + * payloads, successes, stream chunks, and exits. Schema `parseOptions` annotations + * are not consulted. Call this after `prefix` and `middleware`, which clone RPCs. + */ +export const annotateRpcParseOptions = ( + group: RpcGroup.RpcGroup +): RpcGroup.RpcGroup => group.annotateRpcs(Rpc.ParseOptions, DefaultParseOptions) diff --git a/packages/effect-app/test/rpc-parse-options.test.ts b/packages/effect-app/test/rpc-parse-options.test.ts new file mode 100644 index 000000000..be704b03f --- /dev/null +++ b/packages/effect-app/test/rpc-parse-options.test.ts @@ -0,0 +1,193 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { HttpEffect } from "effect/http" +import * as Layer from "effect/Layer" +import * as EffectRequest from "effect/Request" +import * as RequestResolver from "effect/RequestResolver" +import { Rpc, RpcGroup, RpcMiddleware, RpcSerialization, RpcServer } from "effect/rpc" +import type * as SchemaAST from "effect/SchemaAST" +import * as SchemaTransformation from "effect/SchemaTransformation" +import { expect, test } from "vitest" +import { makeRpcClient, makeRpcGroupFromRequestsAndModuleName, NotLoggedInError } from "../src/client.js" +import { S } from "../src/index.js" +import { RpcContextMap } from "../src/rpc.js" +import { annotateRpcParseOptions } from "../src/rpc/RpcParseOptions.js" +import { DefaultParseOptions } from "../src/Schema/ext.js" + +const RequestContextMap = RpcContextMap.makeMap({ + allowAnonymous: RpcContextMap.makeInverted()(NotLoggedInError) +}) + +const stubMiddleware = { + requestContextMap: RequestContextMap.config, + requestContext: undefined +} + +const parseOptionsOf = (ast: SchemaAST.AST): unknown => { + const seen = new Set() + const visit = (node: SchemaAST.AST): unknown => { + if (seen.has(node)) return undefined + seen.add(node) + const parseOptions = node.annotations?.["parseOptions"] + if (parseOptions !== undefined) return parseOptions + if (node.encoding !== undefined) { + for (const link of node.encoding) { + const found = visit(link.to) + if (found !== undefined) return found + } + } + return undefined + } + return visit(ast) +} + +const expectNoParseOptions = (schema: { readonly ast: SchemaAST.AST }) => { + expect(parseOptionsOf(schema.ast)).toBeUndefined() +} + +const makeUser = (batches: Array>) => { + class GetUser extends EffectRequest.TaggedClass("GetUser")<{ readonly id: number }, string> {} + const resolver = RequestResolver.fromFunctionBatched((entries) => { + const ids = entries.map((entry) => entry.request.id) + batches.push(ids) + return ids.map((id) => `user-${id}`) + }) + return S.Number.pipe( + S.decodeTo( + S.String, + SchemaTransformation.transformEffect({ + decode: (id) => Effect.request(new GetUser({ id }), resolver), + encode: (user) => Effect.succeed(Number(user.slice("user-".length))) + }) + ) + ) +} + +const readExits = (body: string): Array => + body.split("\n").flatMap((line) => { + const trimmed = line.trim() + if (trimmed.length === 0) return [] + const parsed: unknown = JSON.parse(trimmed) + if (typeof parsed !== "object" || parsed === null || !("_tag" in parsed) || parsed._tag !== "Exit") { + return [] + } + return "exit" in parsed ? [parsed.exit] : [] + }) + +const postUsers = (annotate: boolean) => { + const batches: Array> = [] + const User = makeUser(batches) + const rpc = Rpc.make("getUsers", { + payload: S.Struct({ users: S.Array(User) }), + success: S.Array(S.String) + }) + const group = annotate ? annotateRpcParseOptions(RpcGroup.make(rpc)) : RpcGroup.make(rpc) + return Effect + .gen(function*() { + const httpEffect = yield* RpcServer.toHttpEffect(group).pipe( + Effect.provide(Layer.mergeAll( + group.toLayer({ + getUsers: ({ users }) => Effect.succeed(users) + }), + RpcSerialization.layerNdjson + )) + ) + const handler = HttpEffect.toWebHandler(httpEffect) + const response = yield* Effect.promise(() => + handler( + new Request("http://test/rpc", { + method: "POST", + body: `{"_tag":"Request","id":1,"tag":"getUsers","payload":{"users":[1,2,3]},"headers":[]}\n` + }) + ) + ) + const body = yield* Effect.promise(() => response.text()) + return { batches, body, exits: readExits(body) } + }) + .pipe(Effect.scoped) +} + +test("schema constructors do not carry parseOptions concurrency", () => { + expectNoParseOptions(S.Struct({ name: S.String })) + expectNoParseOptions(S.Struct({ name: S.String }).mapFields((fields) => ({ ...fields, extra: S.Finite }))) + expectNoParseOptions(S.Array(S.String)) + expectNoParseOptions(S.NonEmptyArray(S.String)) + expectNoParseOptions(S.Record(S.String, S.Finite)) + expectNoParseOptions(S.ReadonlySetFromArray(S.String)) + expectNoParseOptions(S.ReadonlyMapFromArray({ key: S.String, value: S.Finite })) + + class Person extends S.Class("Person")({ name: S.String }) {} + class Circle extends S.TaggedClass()("Circle", { radius: S.Finite }) {} + class Missing extends S.ErrorClass("Missing")({ reason: S.String }) {} + class Boom extends S.TaggedErrorClass()("Boom", { reason: S.String }) {} + + expectNoParseOptions(Person) + expectNoParseOptions(Person.mapFields((fields) => ({ ...fields, extra: S.Finite }))) + expectNoParseOptions(Circle) + expectNoParseOptions(Circle.mapFields((fields) => ({ ...fields }))) + expectNoParseOptions(Missing) + expectNoParseOptions(Boom) +}) + +test("schema array decode stays sequential unless the caller passes parse options", async () => { + const batches: Array> = [] + const User = makeUser(batches) + const schema = S.Array(User) + + expect(await Effect.runPromise(S.decodeUnknownEffect(schema)([1, 2, 3]))).toEqual(["user-1", "user-2", "user-3"]) + expect(batches).toEqual([[1], [2], [3]]) + + batches.length = 0 + expect(await Effect.runPromise(S.decodeUnknownEffectConcurrently(schema)([1, 2, 3]))).toEqual([ + "user-1", + "user-2", + "user-3" + ]) + expect(batches).toEqual([[1, 2, 3]]) +}) + +test("annotateRpcParseOptions sets DefaultParseOptions after prefix and middleware", () => { + class Marker extends RpcMiddleware.Service()("Marker") {} + const plain = RpcGroup.make(Rpc.make("ping", { success: S.String })) + const plainRpc = [...plain.requests.values()][0] + if (plainRpc === undefined) throw new Error("expected ping rpc") + expect(Context.getUnsafe(plainRpc.annotations, Rpc.ParseOptions).concurrency).toBeUndefined() + + const group = annotateRpcParseOptions(plain.prefix("Mod.").middleware(Marker)) + expect([...group.requests.keys()]).toEqual(["Mod.ping"]) + const rpc = group.requests.get("Mod.ping") + if (rpc === undefined) throw new Error("expected Mod.ping rpc") + expect(Context.getUnsafe(rpc.annotations, Rpc.ParseOptions)).toBe(DefaultParseOptions) + expect(rpc.middlewares.has(Marker)).toBe(true) +}) + +test("makeRpcGroupFromRequestsAndModuleName annotates every rpc", () => { + const { TaggedRequestFor } = makeRpcClient(stubMiddleware) + const Query = TaggedRequestFor("Users").Query + class GetStats extends Query()("GetStats", { ids: S.Array(S.String) }, { + success: S.Array(S.String) + }) {} + + const group = makeRpcGroupFromRequestsAndModuleName({ GetStats }, "Users") + const rpcs = [...group.requests.values()] + expect(rpcs.length).toBeGreaterThan(0) + for (const rpc of rpcs) { + expect(Context.getUnsafe(rpc.annotations, Rpc.ParseOptions)).toBe(DefaultParseOptions) + } +}) + +test("Rpc.ParseOptions makes server payload decode batch Effect.request calls", async () => { + const sequential = await Effect.runPromise(postUsers(false)) + expect(sequential.exits, sequential.body).toEqual([{ + _tag: "Success", + value: ["user-1", "user-2", "user-3"] + }]) + expect(sequential.batches).toEqual([[1], [2], [3]]) + + const concurrent = await Effect.runPromise(postUsers(true)) + expect(concurrent.exits, concurrent.body).toEqual([{ + _tag: "Success", + value: ["user-1", "user-2", "user-3"] + }]) + expect(concurrent.batches).toEqual([[1, 2, 3]]) +}) diff --git a/packages/infra/src/routing.ts b/packages/infra/src/routing.ts index a3034e945..7088abb64 100644 --- a/packages/infra/src/routing.ts +++ b/packages/infra/src/routing.ts @@ -10,7 +10,7 @@ import * as DataDependencies from "effect-app/DataDependencies" import * as Effect from "effect-app/Effect" import { type HttpHeaders } from "effect-app/http" import * as Layer from "effect-app/Layer" -import { Invalidation } from "effect-app/rpc" +import { annotateRpcParseOptions, Invalidation } from "effect-app/rpc" import { type GetEffectContext, type GetEffectError, type RpcContextMap } from "effect-app/rpc/RpcContextMap" import * as S from "effect-app/Schema" import { type TypeTestId } from "effect-app/TypeTest" @@ -622,36 +622,38 @@ export const makeRouter = = Layer.Layer ] } - const rpcs = RpcGroup - .make( - ...(Object.values(mapped) as Array<(typeof mapped)[keyof typeof mapped]>).map(([resource]) => { - const isStream = resource.stream - const isCommand = resource.type === "command" - return (isCommand - ? isStream - ? Invalidation.makeStreamRpc(resource._tag, { + const rpcs = annotateRpcParseOptions( + RpcGroup + .make( + ...(Object.values(mapped) as Array<(typeof mapped)[keyof typeof mapped]>).map(([resource]) => { + const isStream = resource.stream + const isCommand = resource.type === "command" + return (isCommand + ? isStream + ? Invalidation.makeStreamRpc(resource._tag, { + payload: resource, + success: resource.success, + error: resource.error, + stream: true as const + }) + : Invalidation.makeCommandRpc(resource._tag, { + payload: resource, + success: resource.success, + error: resource.error + }) + : Rpc.make(resource._tag, { payload: resource, - success: resource.success, + success: isStream ? resource.success : Invalidation.QueryResponseWithMetaData(resource.success), error: resource.error, - stream: true as const - }) - : Invalidation.makeCommandRpc(resource._tag, { - payload: resource, - success: resource.success, - error: resource.error - }) - : Rpc.make(resource._tag, { - payload: resource, - success: isStream ? resource.success : Invalidation.QueryResponseWithMetaData(resource.success), - error: resource.error, - stream: isStream - })) - .annotate(mw.requestContext, resource.config ?? {}) - .annotate(RequestTypeAnnotation, resource.type) - }) - ) - .prefix(`${meta.moduleName}.`) - .middleware(mw) + stream: isStream + })) + .annotate(mw.requestContext, resource.config ?? {}) + .annotate(RequestTypeAnnotation, resource.type) + }) + ) + .prefix(`${meta.moduleName}.`) + .middleware(mw) + ) const rpc = rpcs .toLayer(Effect.gen(function*() { diff --git a/patches/effect@4.0.5.patch b/patches/effect@4.0.5.patch new file mode 100644 index 000000000..86948828e --- /dev/null +++ b/patches/effect@4.0.5.patch @@ -0,0 +1,198 @@ +diff --git a/dist/rpc/Rpc.d.ts b/dist/rpc/Rpc.d.ts +index 1f301cd01341cd10f46fff7696602e70c35580e7..6c1b3ec7f554b8e4942e535c1558aaf0753d5784 100644 +--- a/dist/rpc/Rpc.d.ts ++++ b/dist/rpc/Rpc.d.ts +@@ -20,6 +20,7 @@ import type { Headers } from "../http/Headers.ts"; + import { type Pipeable } from "../Pipeable.ts"; + import type * as Queue from "../Queue.ts"; + import * as Schema from "../Schema.ts"; ++import type * as SchemaAST from "../SchemaAST.ts"; + import type { Stream } from "../Stream.ts"; + import type * as Struct from "../Struct.ts"; + import type { NoInfer } from "../Types.ts"; +@@ -639,6 +640,23 @@ export declare namespace Custom { + * @since 4.0.0 + */ + export declare const exitSchema: (self: R) => Schema.Exit, ErrorExitSchema, DefectSchema>; ++/** ++ * Annotation that sets the schema parse options used by `RpcClient` and ++ * `RpcServer` when encoding and decoding an RPC's payload, success values, ++ * stream chunks, and exits. ++ * ++ * **Details** ++ * ++ * The default value is `SchemaAST.defaultParseOptions`. Set `concurrency` to ++ * parse product elements concurrently, which lets decoding transformations that ++ * call `Effect.request` share `RequestResolver` batches. Annotate a single RPC ++ * with `annotate`, or every RPC in a group with `RpcGroup.annotateRpcs`. ++ * ++ * @stability unstable ++ * @category annotations ++ * @since 4.0.0 ++ */ ++export declare const ParseOptions: Context.Reference; + declare const WrapperTypeId = "~effect/rpc/Rpc/Wrapper"; + /** + * Wraps a handler result with execution options for the RPC server. +diff --git a/dist/rpc/Rpc.js b/dist/rpc/Rpc.js +index 0dfbfd54b641bc45df2a123db04530bb52ab25de..e9caeff023209d6b54ccb59fe23d535757b52ffa 100644 +--- a/dist/rpc/Rpc.js ++++ b/dist/rpc/Rpc.js +@@ -4,6 +4,7 @@ import { pipeArguments } from "../Pipeable.js"; + import * as Predicate from "../Predicate.js"; + import * as PrimaryKey from "../PrimaryKey.js"; + import * as Schema from "../Schema.js"; ++import * as SchemaAST from "../SchemaAST.js"; + import * as RpcSchema from "./RpcSchema.js"; + const TypeId = "~effect/rpc/Rpc"; + /** +@@ -267,6 +268,25 @@ export const exitSchema = self => { + exitSchemaCache.set(self, schema); + return schema; + }; ++/** ++ * Annotation that sets the schema parse options used by `RpcClient` and ++ * `RpcServer` when encoding and decoding an RPC's payload, success values, ++ * stream chunks, and exits. ++ * ++ * **Details** ++ * ++ * The default value is `SchemaAST.defaultParseOptions`. Set `concurrency` to ++ * parse product elements concurrently, which lets decoding transformations that ++ * call `Effect.request` share `RequestResolver` batches. Annotate a single RPC ++ * with `annotate`, or every RPC in a group with `RpcGroup.annotateRpcs`. ++ * ++ * @stability unstable ++ * @category annotations ++ * @since 4.0.0 ++ */ ++export const ParseOptions = /*#__PURE__*/Context.Reference("effect/rpc/Rpc/ParseOptions", { ++ defaultValue: () => SchemaAST.defaultParseOptions ++}); + const WrapperTypeId = "~effect/rpc/Rpc/Wrapper"; + /** + * Returns `true` when the value is an RPC `Wrapper`. +diff --git a/dist/rpc/RpcClient.js b/dist/rpc/RpcClient.js +index a03019a5599471cdf66ad52188fd55afdc8d9187..d78ae87f99db0fdb0bd0c81e7a41fc1fc411d64e 100644 +--- a/dist/rpc/RpcClient.js ++++ b/dist/rpc/RpcClient.js +@@ -440,10 +440,11 @@ const makeRpcSchemas = codecFor => { + return entry; + } + const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema); ++ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions); + entry = { +- decodeChunk: Option.map(streamSchemas, streamSchemas => Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success)))), +- encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema)), +- decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc))) ++ decodeChunk: Option.map(streamSchemas, streamSchemas => Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success)), parseOptions)), ++ encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema), parseOptions), ++ decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc)), parseOptions) + }; + cache.set(rpc, entry); + return entry; +diff --git a/dist/rpc/RpcServer.js b/dist/rpc/RpcServer.js +index 7509b4bd0ed2f126d8c66ce2727fdaa21ce3bec3..d70bf609e25e52b841d9a5758b78e9df22f050f5 100644 +--- a/dist/rpc/RpcServer.js ++++ b/dist/rpc/RpcServer.js +@@ -350,10 +350,11 @@ const rpcCodecs = (codecFor, rpc) => { + let codecs = shared ? sharedRpcCodecs.get(rpc) : undefined; + if (codecs === undefined) { + const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema); ++ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions); + codecs = { +- decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)), +- encodeChunk: Schema.encodeUnknownEffect(codecFor(Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any))), +- encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc))) ++ decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema), parseOptions), ++ encodeChunk: Schema.encodeUnknownEffect(codecFor(Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any)), parseOptions), ++ encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc)), parseOptions) + }; + if (shared) sharedRpcCodecs.set(rpc, codecs); + } +diff --git a/src/rpc/Rpc.ts b/src/rpc/Rpc.ts +index 146d477f647ae2abf8ab96b116d8d2d80162304c..a07fec273c170647218b28dc0732f1111af0bc54 100644 +--- a/src/rpc/Rpc.ts ++++ b/src/rpc/Rpc.ts +@@ -23,6 +23,7 @@ import * as Predicate from "../Predicate.ts" + import * as PrimaryKey from "../PrimaryKey.ts" + import type * as Queue from "../Queue.ts" + import * as Schema from "../Schema.ts" ++import * as SchemaAST from "../SchemaAST.ts" + import type { Stream } from "../Stream.ts" + import type * as Struct from "../Struct.ts" + import type { NoInfer } from "../Types.ts" +@@ -1194,6 +1195,26 @@ export const exitSchema = ( + return schema as any + } + ++/** ++ * Annotation that sets the schema parse options used by `RpcClient` and ++ * `RpcServer` when encoding and decoding an RPC's payload, success values, ++ * stream chunks, and exits. ++ * ++ * **Details** ++ * ++ * The default value is `SchemaAST.defaultParseOptions`. Set `concurrency` to ++ * parse product elements concurrently, which lets decoding transformations that ++ * call `Effect.request` share `RequestResolver` batches. Annotate a single RPC ++ * with `annotate`, or every RPC in a group with `RpcGroup.annotateRpcs`. ++ * ++ * @stability unstable ++ * @category annotations ++ * @since 4.0.0 ++ */ ++export const ParseOptions = Context.Reference("effect/rpc/Rpc/ParseOptions", { ++ defaultValue: () => SchemaAST.defaultParseOptions ++}) ++ + const WrapperTypeId = "~effect/rpc/Rpc/Wrapper" + + /** +diff --git a/src/rpc/RpcClient.ts b/src/rpc/RpcClient.ts +index 986b59c88bd8e0338ccca7854c2498d659bf2569..443623fc91844a1e944af3cd31f48f65ac7f0b9b 100644 +--- a/src/rpc/RpcClient.ts ++++ b/src/rpc/RpcClient.ts +@@ -809,13 +809,15 @@ const makeRpcSchemas = (codecFor: RpcSerialization.CodecFor) => { + return entry + } + const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema) ++ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions) + entry = { + decodeChunk: Option.map( + streamSchemas, +- (streamSchemas) => Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success))) ++ (streamSchemas) => ++ Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success)), parseOptions) + ), +- encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema)), +- decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) ++ encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema), parseOptions), ++ decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any)), parseOptions) + } + cache.set(rpc, entry) + return entry +diff --git a/src/rpc/RpcServer.ts b/src/rpc/RpcServer.ts +index 7f38c61ab1da9589d579a3b3722d4fa84045b5e1..01fedcf3f596f3853bcb04b54b5bdc0fe8e607cc 100644 +--- a/src/rpc/RpcServer.ts ++++ b/src/rpc/RpcServer.ts +@@ -513,14 +513,16 @@ const rpcCodecs = (codecFor: RpcSerialization.CodecFor, rpc: Rpc.AnyWithProps): + let codecs = shared ? sharedRpcCodecs.get(rpc) : undefined + if (codecs === undefined) { + const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema) ++ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions) + codecs = { +- decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)) as any, ++ decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema), parseOptions) as any, + encodeChunk: Schema.encodeUnknownEffect( + codecFor( + Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any) +- ) ++ ), ++ parseOptions + ) as any, +- encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) as any ++ encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any)), parseOptions) as any + } + if (shared) sharedRpcCodecs.set(rpc, codecs) + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46da1f23d..b31c3a65a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,7 @@ patchedDependencies: "@changesets/cli@2.31.0": c9b080d02343ec5517f97d61b809f935de0d61aa5406ff37449ef3e5fae12086 "@sendgrid/mail": 3d2dd1cd5f52d3eef1772e4be48776258a596f9277c00adf0a471f54fcd3e21a "@tanstack/query-core": c98953cf4906c12283dbf72aa23b2066fccbe5186c91cf2ae01043004cf0e3f2 + effect@4.0.5: a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8 ts-plugin-sort-import-suggestions: 4664506575d0f4e799401cb7140b6f15f34a6fe213da6314d68439f7e8d30561 typescript@6.0.3: d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54 @@ -255,13 +256,13 @@ importers: version: 0.87.5 "@effect/platform-node": specifier: 4.0.5 - version: 4.0.5(effect@4.0.5)(redis@6.2.1(@opentelemetry/api@1.9.1)) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1)) "@effect/tsgo": specifier: ^0.56.0 version: 0.56.0 "@effect/vitest": specifier: 4.0.5 - version: 4.0.5(effect@4.0.5)(vitest@5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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.27.2)(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 @@ -282,7 +283,7 @@ importers: version: 0.54.0 effect: specifier: 4.0.5 - version: 4.0.5 + version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) effect-app: specifier: workspace:* version: link:packages/effect-app @@ -327,10 +328,10 @@ importers: dependencies: "@effect/platform-node": specifier: 4.0.5 - version: 4.0.5(effect@4.0.5)(redis@6.2.1(@opentelemetry/api@1.9.1)) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1)) effect: specifier: 4.0.5 - version: 4.0.5 + version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) js-yaml: specifier: 4.2.0 version: 4.2.0 @@ -364,13 +365,13 @@ importers: devDependencies: "@effect/atom-vue": specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5)(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) "@effect/platform-node": specifier: 4.0.5 - version: 4.0.5(effect@4.0.5)(redis@6.2.1(@opentelemetry/api@1.9.1)) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1)) "@effect/vitest": specifier: 4.0.5 - version: 4.0.5(effect@4.0.5)(vitest@5.0.5(@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))) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) @@ -382,7 +383,7 @@ importers: 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=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) effect: specifier: ^4.0.5 - version: 4.0.5 + version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) typescript: specifier: ~6.0.3 version: 6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54) @@ -400,7 +401,7 @@ importers: version: 2.0.8 effect: specifier: ^4.0.5 - version: 4.0.5 + version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) nanoid: specifier: ^5.1.11 version: 5.1.11 @@ -541,7 +542,7 @@ importers: dependencies: "@effect/vitest": specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5)(vitest@5.0.5(@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))) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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 @@ -556,7 +557,7 @@ importers: version: 10.55.0(supports-color@7.2.0) effect: specifier: ^4.0.5 - version: 4.0.5 + version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) effect-app: specifier: workspace:* version: link:../effect-app @@ -584,7 +585,7 @@ importers: version: 7.9.5(supports-color@7.2.0) "@effect/sql-sqlite-node": specifier: 4.0.5 - version: 4.0.5(effect@4.0.5) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8)) "@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) @@ -631,10 +632,10 @@ importers: dependencies: "@effect/atom-vue": specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5)(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) "@effect/platform-browser": specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8)) "@formatjs/intl": specifier: ^4.1.12 version: 4.1.12 @@ -652,7 +653,7 @@ importers: version: 5.4.4 effect: specifier: ^4.0.5 - version: 4.0.5 + version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) effect-app: specifier: workspace:* version: link:../effect-app @@ -665,7 +666,7 @@ importers: devDependencies: "@effect/vitest": specifier: 4.0.5 - version: 4.0.5(effect@4.0.5)(vitest@5.0.5(@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))) + version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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 @@ -706,7 +707,7 @@ importers: version: 1.32.0(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) effect: specifier: ^4.0.5 - version: 4.0.5 + version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) effect-app: specifier: workspace:* version: link:../effect-app @@ -10312,39 +10313,39 @@ snapshots: "@dprint/win32-x64@0.54.0": optional: true - "@effect/atom-vue@4.0.5(effect@4.0.5)(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54)))": + "@effect/atom-vue@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54)))": dependencies: - effect: 4.0.5 + effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) vue: 3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54)) "@effect/language-service@0.87.5": {} - "@effect/platform-browser@4.0.5(effect@4.0.5)": + "@effect/platform-browser@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))": dependencies: - effect: 4.0.5 + effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) - "@effect/platform-node-shared@4.0.5(effect@4.0.5)": + "@effect/platform-node-shared@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))": dependencies: "@types/ws": 8.18.2 - effect: 4.0.5 + effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) ws: 8.22.0 transitivePeerDependencies: - bufferutil - utf-8-validate - "@effect/platform-node@4.0.5(effect@4.0.5)(redis@6.2.1(@opentelemetry/api@1.9.1))": + "@effect/platform-node@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1))": dependencies: - "@effect/platform-node-shared": 4.0.5(effect@4.0.5) - effect: 4.0.5 + "@effect/platform-node-shared": 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8)) + effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) redis: 6.2.1(@opentelemetry/api@1.9.1) undici: 8.11.2 transitivePeerDependencies: - bufferutil - utf-8-validate - "@effect/sql-sqlite-node@4.0.5(effect@4.0.5)": + "@effect/sql-sqlite-node@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))": dependencies: - effect: 4.0.5 + effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) "@effect/tsgo-darwin-arm64@0.56.0": optional: true @@ -10377,14 +10378,14 @@ snapshots: "@effect/tsgo-win32-arm64": 0.56.0 "@effect/tsgo-win32-x64": 0.56.0 - "@effect/vitest@4.0.5(effect@4.0.5)(vitest@5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)))": + "@effect/vitest@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)))": dependencies: - effect: 4.0.5 + effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) vitest: 5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)) - "@effect/vitest@4.0.5(effect@4.0.5)(vitest@5.0.5(@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@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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.5 + effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) vitest: 5.0.5(@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": @@ -12986,7 +12987,7 @@ snapshots: minimatch: 9.0.1 semver: 7.7.3 - effect@4.0.5: {} + effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8): {} electron-to-chromium@1.5.267: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ddf9fed8d..6aeae6011 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -34,3 +34,4 @@ patchedDependencies: typescript@6.0.3: patches/typescript.patch "@sendgrid/mail": patches/@sendgrid__mail.patch "@changesets/cli@2.31.0": patches/@changesets__cli@2.31.0.patch + effect@4.0.5: patches/effect@4.0.5.patch From 280d410345c7ec42e33edc4c474d5c063825ea52 Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:56:39 +0000 Subject: [PATCH 2/3] chore: update effect packages to 4.0.6 Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com> --- .changeset/effect-4.0.6.md | 9 ++ .changeset/rpc-parse-options.md | 2 +- package.json | 6 +- packages/cli/package.json | 4 +- packages/e2e/package.json | 8 +- packages/effect-app/package.json | 2 +- packages/infra/package.json | 6 +- packages/vue-components/package.json | 2 +- packages/vue/package.json | 8 +- patches/effect@4.0.5.patch | 198 --------------------------- pnpm-lock.yaml | 153 ++++++++++----------- pnpm-workspace.yaml | 15 +- 12 files changed, 111 insertions(+), 302 deletions(-) create mode 100644 .changeset/effect-4.0.6.md delete mode 100644 patches/effect@4.0.5.patch diff --git a/.changeset/effect-4.0.6.md b/.changeset/effect-4.0.6.md new file mode 100644 index 000000000..8c428dc04 --- /dev/null +++ b/.changeset/effect-4.0.6.md @@ -0,0 +1,9 @@ +--- +"effect-app": patch +"@effect-app/infra": patch +"@effect-app/vue": patch +"@effect-app/vue-components": patch +"@effect-app/cli": patch +--- + +Update Effect dependencies to 4.0.6. diff --git a/.changeset/rpc-parse-options.md b/.changeset/rpc-parse-options.md index 9a2e6f7fd..3757094b0 100644 --- a/.changeset/rpc-parse-options.md +++ b/.changeset/rpc-parse-options.md @@ -7,4 +7,4 @@ Adopt effect's `Rpc.ParseOptions` for RPC encode and decode concurrency, and dro Effect 4 ignores `parseOptions` annotations on schemas. Client and server RPC groups now carry `DefaultParseOptions` (unbounded unless `SCHEMA_CONCURRENCY` is set) via `annotateRpcParseOptions`, applied after prefix and middleware. `decodeEffectConcurrently` is unchanged for call sites that pass parse options themselves. -`effect@4.0.6` is not on npm yet, so this repo temporarily patches `effect@4.0.5` with the `Rpc.ParseOptions` backport from Effect-TS/effect#9665. Drop `patches/effect@4.0.5.patch` once `effect@4.0.6` can be installed. Published packages call `Rpc.ParseOptions` directly, so a consumer still on stock `effect@4.0.5` without that patch cannot import them. +Dependencies are on `effect@4.0.6`, which includes `Rpc.ParseOptions`. Peers require `effect@^4.0.6`. diff --git a/package.json b/package.json index bedc3c2e7..718a90841 100644 --- a/package.json +++ b/package.json @@ -50,15 +50,15 @@ "@effect-app/infra": "workspace:*", "@effect/language-service": "0.87.5", "@effect/tsgo": "^0.56.0", - "@effect/platform-node": "4.0.5", - "@effect/vitest": "4.0.5", + "@effect/platform-node": "4.0.6", + "@effect/vitest": "4.0.6", "@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.5", + "effect": "4.0.6", "effect-app": "workspace:*", "eslint": "^10.4.1", "json5": "^2.2.3", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1c03214af..d37c7323c 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.5", - "effect": "4.0.5", + "@effect/platform-node": "4.0.6", + "effect": "4.0.6", "js-yaml": "4.2.0" }, "devDependencies": { diff --git a/packages/e2e/package.json b/packages/e2e/package.json index ec255856d..d01a8a408 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -10,13 +10,13 @@ "effect-app": "workspace:*" }, "devDependencies": { - "@effect/atom-vue": "^4.0.5", - "@effect/platform-node": "4.0.5", - "@effect/vitest": "4.0.5", + "@effect/atom-vue": "^4.0.6", + "@effect/platform-node": "4.0.6", + "@effect/vitest": "4.0.6", "@tanstack/vue-query": "5.96.2", "@types/node": "25.9.1", "@vitejs/plugin-vue": "^6.0.7", - "effect": "^4.0.5", + "effect": "^4.0.6", "typescript": "~6.0.3", "vitest": "^5.0.5", "vue": "^3.5.35" diff --git a/packages/effect-app/package.json b/packages/effect-app/package.json index 6e4166e91..bce25ff25 100644 --- a/packages/effect-app/package.json +++ b/packages/effect-app/package.json @@ -25,7 +25,7 @@ "vitest": "^5.0.5" }, "peerDependencies": { - "effect": "^4.0.5" + "effect": "^4.0.6" }, "typesVersions": { "*": { diff --git a/packages/infra/package.json b/packages/infra/package.json index 33005e840..7d2c70add 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.5", + "@effect/sql-sqlite-node": "4.0.6", "@sentry/opentelemetry": "10.55.0", "@types/better-sqlite3": "^7.6.13", "@types/node": "25.9.1", @@ -39,12 +39,12 @@ "peerDependencies": { "@azure/cosmos": "^4.9.3", "@azure/service-bus": "^7.9.5", - "@effect/vitest": "^4.0.5", + "@effect/vitest": "^4.0.6", "@sendgrid/helpers": "^8.0.0", "@sendgrid/mail": "^8.1.6", "@sentry/node": "10.55.0", "@sentry/opentelemetry": "10.55.0", - "effect": "^4.0.5", + "effect": "^4.0.6", "jwt-decode": "^4.0.0", "redis": "^3.1.2", "redlock": "^4.2.0" diff --git a/packages/vue-components/package.json b/packages/vue-components/package.json index 8edcaaabf..2f98de8f9 100644 --- a/packages/vue-components/package.json +++ b/packages/vue-components/package.json @@ -25,7 +25,7 @@ }, "peerDependencies": { "@mdi/js": "^7.4.47", - "effect": "^4.0.5", + "effect": "^4.0.6", "intl-messageformat": "^11.2.7", "mdi-js": "^1.0.1", "primeflex": "^4.0.0", diff --git a/packages/vue/package.json b/packages/vue/package.json index 506abf975..a3abfbc29 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -18,14 +18,14 @@ "query-string": "^9.4.0" }, "peerDependencies": { - "@effect/atom-vue": "^4.0.5", - "@effect/platform-browser": "^4.0.5", + "@effect/atom-vue": "^4.0.6", + "@effect/platform-browser": "^4.0.6", "@sentry/browser": "^10.55.0", - "effect": "^4.0.5", + "effect": "^4.0.6", "vue": "^3.5.35" }, "devDependencies": { - "@effect/vitest": "4.0.5", + "@effect/vitest": "4.0.6", "@formatjs/icu-messageformat-parser": "^3.5.10", "@types/node": "25.9.1", "@vitejs/plugin-vue": "^6.0.7", diff --git a/patches/effect@4.0.5.patch b/patches/effect@4.0.5.patch deleted file mode 100644 index 86948828e..000000000 --- a/patches/effect@4.0.5.patch +++ /dev/null @@ -1,198 +0,0 @@ -diff --git a/dist/rpc/Rpc.d.ts b/dist/rpc/Rpc.d.ts -index 1f301cd01341cd10f46fff7696602e70c35580e7..6c1b3ec7f554b8e4942e535c1558aaf0753d5784 100644 ---- a/dist/rpc/Rpc.d.ts -+++ b/dist/rpc/Rpc.d.ts -@@ -20,6 +20,7 @@ import type { Headers } from "../http/Headers.ts"; - import { type Pipeable } from "../Pipeable.ts"; - import type * as Queue from "../Queue.ts"; - import * as Schema from "../Schema.ts"; -+import type * as SchemaAST from "../SchemaAST.ts"; - import type { Stream } from "../Stream.ts"; - import type * as Struct from "../Struct.ts"; - import type { NoInfer } from "../Types.ts"; -@@ -639,6 +640,23 @@ export declare namespace Custom { - * @since 4.0.0 - */ - export declare const exitSchema: (self: R) => Schema.Exit, ErrorExitSchema, DefectSchema>; -+/** -+ * Annotation that sets the schema parse options used by `RpcClient` and -+ * `RpcServer` when encoding and decoding an RPC's payload, success values, -+ * stream chunks, and exits. -+ * -+ * **Details** -+ * -+ * The default value is `SchemaAST.defaultParseOptions`. Set `concurrency` to -+ * parse product elements concurrently, which lets decoding transformations that -+ * call `Effect.request` share `RequestResolver` batches. Annotate a single RPC -+ * with `annotate`, or every RPC in a group with `RpcGroup.annotateRpcs`. -+ * -+ * @stability unstable -+ * @category annotations -+ * @since 4.0.0 -+ */ -+export declare const ParseOptions: Context.Reference; - declare const WrapperTypeId = "~effect/rpc/Rpc/Wrapper"; - /** - * Wraps a handler result with execution options for the RPC server. -diff --git a/dist/rpc/Rpc.js b/dist/rpc/Rpc.js -index 0dfbfd54b641bc45df2a123db04530bb52ab25de..e9caeff023209d6b54ccb59fe23d535757b52ffa 100644 ---- a/dist/rpc/Rpc.js -+++ b/dist/rpc/Rpc.js -@@ -4,6 +4,7 @@ import { pipeArguments } from "../Pipeable.js"; - import * as Predicate from "../Predicate.js"; - import * as PrimaryKey from "../PrimaryKey.js"; - import * as Schema from "../Schema.js"; -+import * as SchemaAST from "../SchemaAST.js"; - import * as RpcSchema from "./RpcSchema.js"; - const TypeId = "~effect/rpc/Rpc"; - /** -@@ -267,6 +268,25 @@ export const exitSchema = self => { - exitSchemaCache.set(self, schema); - return schema; - }; -+/** -+ * Annotation that sets the schema parse options used by `RpcClient` and -+ * `RpcServer` when encoding and decoding an RPC's payload, success values, -+ * stream chunks, and exits. -+ * -+ * **Details** -+ * -+ * The default value is `SchemaAST.defaultParseOptions`. Set `concurrency` to -+ * parse product elements concurrently, which lets decoding transformations that -+ * call `Effect.request` share `RequestResolver` batches. Annotate a single RPC -+ * with `annotate`, or every RPC in a group with `RpcGroup.annotateRpcs`. -+ * -+ * @stability unstable -+ * @category annotations -+ * @since 4.0.0 -+ */ -+export const ParseOptions = /*#__PURE__*/Context.Reference("effect/rpc/Rpc/ParseOptions", { -+ defaultValue: () => SchemaAST.defaultParseOptions -+}); - const WrapperTypeId = "~effect/rpc/Rpc/Wrapper"; - /** - * Returns `true` when the value is an RPC `Wrapper`. -diff --git a/dist/rpc/RpcClient.js b/dist/rpc/RpcClient.js -index a03019a5599471cdf66ad52188fd55afdc8d9187..d78ae87f99db0fdb0bd0c81e7a41fc1fc411d64e 100644 ---- a/dist/rpc/RpcClient.js -+++ b/dist/rpc/RpcClient.js -@@ -440,10 +440,11 @@ const makeRpcSchemas = codecFor => { - return entry; - } - const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema); -+ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions); - entry = { -- decodeChunk: Option.map(streamSchemas, streamSchemas => Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success)))), -- encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema)), -- decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc))) -+ decodeChunk: Option.map(streamSchemas, streamSchemas => Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success)), parseOptions)), -+ encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema), parseOptions), -+ decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc)), parseOptions) - }; - cache.set(rpc, entry); - return entry; -diff --git a/dist/rpc/RpcServer.js b/dist/rpc/RpcServer.js -index 7509b4bd0ed2f126d8c66ce2727fdaa21ce3bec3..d70bf609e25e52b841d9a5758b78e9df22f050f5 100644 ---- a/dist/rpc/RpcServer.js -+++ b/dist/rpc/RpcServer.js -@@ -350,10 +350,11 @@ const rpcCodecs = (codecFor, rpc) => { - let codecs = shared ? sharedRpcCodecs.get(rpc) : undefined; - if (codecs === undefined) { - const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema); -+ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions); - codecs = { -- decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)), -- encodeChunk: Schema.encodeUnknownEffect(codecFor(Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any))), -- encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc))) -+ decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema), parseOptions), -+ encodeChunk: Schema.encodeUnknownEffect(codecFor(Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any)), parseOptions), -+ encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc)), parseOptions) - }; - if (shared) sharedRpcCodecs.set(rpc, codecs); - } -diff --git a/src/rpc/Rpc.ts b/src/rpc/Rpc.ts -index 146d477f647ae2abf8ab96b116d8d2d80162304c..a07fec273c170647218b28dc0732f1111af0bc54 100644 ---- a/src/rpc/Rpc.ts -+++ b/src/rpc/Rpc.ts -@@ -23,6 +23,7 @@ import * as Predicate from "../Predicate.ts" - import * as PrimaryKey from "../PrimaryKey.ts" - import type * as Queue from "../Queue.ts" - import * as Schema from "../Schema.ts" -+import * as SchemaAST from "../SchemaAST.ts" - import type { Stream } from "../Stream.ts" - import type * as Struct from "../Struct.ts" - import type { NoInfer } from "../Types.ts" -@@ -1194,6 +1195,26 @@ export const exitSchema = ( - return schema as any - } - -+/** -+ * Annotation that sets the schema parse options used by `RpcClient` and -+ * `RpcServer` when encoding and decoding an RPC's payload, success values, -+ * stream chunks, and exits. -+ * -+ * **Details** -+ * -+ * The default value is `SchemaAST.defaultParseOptions`. Set `concurrency` to -+ * parse product elements concurrently, which lets decoding transformations that -+ * call `Effect.request` share `RequestResolver` batches. Annotate a single RPC -+ * with `annotate`, or every RPC in a group with `RpcGroup.annotateRpcs`. -+ * -+ * @stability unstable -+ * @category annotations -+ * @since 4.0.0 -+ */ -+export const ParseOptions = Context.Reference("effect/rpc/Rpc/ParseOptions", { -+ defaultValue: () => SchemaAST.defaultParseOptions -+}) -+ - const WrapperTypeId = "~effect/rpc/Rpc/Wrapper" - - /** -diff --git a/src/rpc/RpcClient.ts b/src/rpc/RpcClient.ts -index 986b59c88bd8e0338ccca7854c2498d659bf2569..443623fc91844a1e944af3cd31f48f65ac7f0b9b 100644 ---- a/src/rpc/RpcClient.ts -+++ b/src/rpc/RpcClient.ts -@@ -809,13 +809,15 @@ const makeRpcSchemas = (codecFor: RpcSerialization.CodecFor) => { - return entry - } - const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema) -+ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions) - entry = { - decodeChunk: Option.map( - streamSchemas, -- (streamSchemas) => Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success))) -+ (streamSchemas) => -+ Schema.decodeUnknownEffect(codecFor(Schema.NonEmptyArray(streamSchemas.success)), parseOptions) - ), -- encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema)), -- decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) -+ encodePayload: Schema.encodeEffect(codecFor(rpc.payloadSchema), parseOptions), -+ decodeExit: Schema.decodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any)), parseOptions) - } - cache.set(rpc, entry) - return entry -diff --git a/src/rpc/RpcServer.ts b/src/rpc/RpcServer.ts -index 7f38c61ab1da9589d579a3b3722d4fa84045b5e1..01fedcf3f596f3853bcb04b54b5bdc0fe8e607cc 100644 ---- a/src/rpc/RpcServer.ts -+++ b/src/rpc/RpcServer.ts -@@ -513,14 +513,16 @@ const rpcCodecs = (codecFor: RpcSerialization.CodecFor, rpc: Rpc.AnyWithProps): - let codecs = shared ? sharedRpcCodecs.get(rpc) : undefined - if (codecs === undefined) { - const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema) -+ const parseOptions = Context.get(rpc.annotations, Rpc.ParseOptions) - codecs = { -- decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema)) as any, -+ decode: Schema.decodeUnknownEffect(codecFor(rpc.payloadSchema), parseOptions) as any, - encodeChunk: Schema.encodeUnknownEffect( - codecFor( - Schema.NonEmptyArray(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any) -- ) -+ ), -+ parseOptions - ) as any, -- encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any))) as any -+ encodeExit: Schema.encodeUnknownEffect(codecFor(Rpc.exitSchema(rpc as any)), parseOptions) as any - } - if (shared) sharedRpcCodecs.set(rpc, codecs) - } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b31c3a65a..8b826ba7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,7 +225,6 @@ patchedDependencies: "@changesets/cli@2.31.0": c9b080d02343ec5517f97d61b809f935de0d61aa5406ff37449ef3e5fae12086 "@sendgrid/mail": 3d2dd1cd5f52d3eef1772e4be48776258a596f9277c00adf0a471f54fcd3e21a "@tanstack/query-core": c98953cf4906c12283dbf72aa23b2066fccbe5186c91cf2ae01043004cf0e3f2 - effect@4.0.5: a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8 ts-plugin-sort-import-suggestions: 4664506575d0f4e799401cb7140b6f15f34a6fe213da6314d68439f7e8d30561 typescript@6.0.3: d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54 @@ -255,14 +254,14 @@ importers: specifier: 0.87.5 version: 0.87.5 "@effect/platform-node": - specifier: 4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1)) + specifier: 4.0.6 + version: 4.0.6(effect@4.0.6)(redis@6.2.1(@opentelemetry/api@1.9.1)) "@effect/tsgo": specifier: ^0.56.0 version: 0.56.0 "@effect/vitest": - specifier: 4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0))) + specifier: 4.0.6 + version: 4.0.6(effect@4.0.6)(vitest@5.0.5(@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.27.2)(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 @@ -282,8 +281,8 @@ importers: specifier: ^0.54.0 version: 0.54.0 effect: - specifier: 4.0.5 - version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + specifier: 4.0.6 + version: 4.0.6 effect-app: specifier: workspace:* version: link:packages/effect-app @@ -327,11 +326,11 @@ importers: packages/cli: dependencies: "@effect/platform-node": - specifier: 4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1)) + specifier: 4.0.6 + version: 4.0.6(effect@4.0.6)(redis@6.2.1(@opentelemetry/api@1.9.1)) effect: - specifier: 4.0.5 - version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + specifier: 4.0.6 + version: 4.0.6 js-yaml: specifier: 4.2.0 version: 4.2.0 @@ -364,14 +363,14 @@ importers: version: link:../effect-app devDependencies: "@effect/atom-vue": - specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) + specifier: ^4.0.6 + version: 4.0.6(effect@4.0.6)(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) "@effect/platform-node": - specifier: 4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1)) + specifier: 4.0.6 + version: 4.0.6(effect@4.0.6)(redis@6.2.1(@opentelemetry/api@1.9.1)) "@effect/vitest": - specifier: 4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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: 4.0.6 + version: 4.0.6(effect@4.0.6)(vitest@5.0.5(@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=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) @@ -382,8 +381,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=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) effect: - specifier: ^4.0.5 - version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + specifier: ^4.0.6 + version: 4.0.6 typescript: specifier: ~6.0.3 version: 6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54) @@ -400,8 +399,8 @@ importers: specifier: ^2.0.8 version: 2.0.8 effect: - specifier: ^4.0.5 - version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + specifier: ^4.0.6 + version: 4.0.6 nanoid: specifier: ^5.1.11 version: 5.1.11 @@ -541,8 +540,8 @@ importers: packages/infra: dependencies: "@effect/vitest": - specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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: ^4.0.6 + version: 4.0.6(effect@4.0.6)(vitest@5.0.5(@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 @@ -556,8 +555,8 @@ importers: specifier: 10.55.0 version: 10.55.0(supports-color@7.2.0) effect: - specifier: ^4.0.5 - version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + specifier: ^4.0.6 + version: 4.0.6 effect-app: specifier: workspace:* version: link:../effect-app @@ -584,8 +583,8 @@ importers: specifier: ^7.9.5 version: 7.9.5(supports-color@7.2.0) "@effect/sql-sqlite-node": - specifier: 4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8)) + specifier: 4.0.6 + version: 4.0.6(effect@4.0.6) "@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) @@ -631,11 +630,11 @@ importers: packages/vue: dependencies: "@effect/atom-vue": - specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) + specifier: ^4.0.6 + version: 4.0.6(effect@4.0.6)(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) "@effect/platform-browser": - specifier: ^4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8)) + specifier: ^4.0.6 + version: 4.0.6(effect@4.0.6) "@formatjs/intl": specifier: ^4.1.12 version: 4.1.12 @@ -652,8 +651,8 @@ importers: specifier: ^5.4.4 version: 5.4.4 effect: - specifier: ^4.0.5 - version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + specifier: ^4.0.6 + version: 4.0.6 effect-app: specifier: workspace:* version: link:../effect-app @@ -665,8 +664,8 @@ importers: version: 3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54)) devDependencies: "@effect/vitest": - specifier: 4.0.5 - version: 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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: 4.0.6 + version: 4.0.6(effect@4.0.6)(vitest@5.0.5(@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 @@ -706,8 +705,8 @@ importers: specifier: ^1.32.0 version: 1.32.0(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54))) effect: - specifier: ^4.0.5 - version: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + specifier: ^4.0.6 + version: 4.0.6 effect-app: specifier: workspace:* version: link:../effect-app @@ -1394,12 +1393,12 @@ packages: cpu: [x64] os: [win32] - "@effect/atom-vue@4.0.5": + "@effect/atom-vue@4.0.6": resolution: { - integrity: sha512-GYnHSbdDE+2Q3AgQSNIxw0lsAUQngeP5RCW2xW9YH8KZ0eLq5DNUk3aHZyZy72M2KgM9dEU/z//vwMDBDB/c2g==, + integrity: sha512-JpbRYTBmVYyOBVVqnuzqgcoGbuBrw7r6skx6Al8YB2NFLUGskWqfJey+TY+fACr0qoUsGuDhlWYkzaiEm8/qmw==, } peerDependencies: - effect: ^4.0.5 + effect: ^4.0.6 vue: ^3.5.35 "@effect/language-service@0.87.5": @@ -1408,36 +1407,36 @@ packages: } hasBin: true - "@effect/platform-browser@4.0.5": + "@effect/platform-browser@4.0.6": resolution: { - integrity: sha512-o3H75xA91nkgIWOcp0XouIxxWxJEOFkTf7gG4qkQtZfy0tq46hJBI0U8jrUqsPruo2vCCNfM0yCoj5AG7F5BMQ==, + integrity: sha512-i4bf1+FlUnZypqK0pQ5cq46KhroTgqFGktFn2WorwIk+yc+RuZO6qsF+Sk6lY5uHwdHTOLGMlnlXSJ8u1UG42g==, } peerDependencies: - effect: ^4.0.5 + effect: ^4.0.6 - "@effect/platform-node-shared@4.0.5": + "@effect/platform-node-shared@4.0.6": resolution: { - integrity: sha512-a8rCdEkfZBGe9C6bsIjxVZuRp73svIyK5IHwHdDAVsvZXjrbkxusfI+rZ8WlX31RP1mKizwI/E72GQTNfdMWHw==, + integrity: sha512-0PhyC4uG30xGtsajnxjkcBWfTjNRzUh4ZKl1Kvo/MNyCTb+W2O+fBzf7wCRCoqt+nLudiq4V7vX40Q8XIBuMaQ==, } engines: { node: ">=18.0.0" } peerDependencies: - effect: ^4.0.5 + effect: ^4.0.6 - "@effect/platform-node@4.0.5": + "@effect/platform-node@4.0.6": resolution: { - integrity: sha512-v4KD5fWvEUa4QmWFpZFbBsRhmfnTP6ETCME2r9ifyXVDPjhfpxpTZl41prjgQFMqZBHdWWCl7YC5ImbXv5ui5g==, + integrity: sha512-gXvGHAy0/8PZbWvMMguloDUyeSP+rzYEo3FSEqUuGduxo9ORcGioDxPI+GOLCIFfam7VuNkmSUxBOiRMIrMrBw==, } engines: { node: ">=18.0.0" } peerDependencies: - effect: ^4.0.5 + effect: ^4.0.6 redis: ">=5.0.0 <7.0.0" - "@effect/sql-sqlite-node@4.0.5": + "@effect/sql-sqlite-node@4.0.6": resolution: { - integrity: sha512-SHza5qNwiI4dRtUk45wvUfvZq7vB950DW2wD6npzt2aH/tesFfPlZJSDBIvwhazoK+gvDknBOONfKyFZsIF+tg==, + integrity: sha512-mYWr1Ppoz3/XrY+ZBnYfdvZvdcAYJTO6VcWoNetBX5n2c0VKHqg9S8WO6EpgtV5WiR8/yHHmu3qkfAGHJmalcA==, } peerDependencies: - effect: ^4.0.5 + effect: ^4.0.6 "@effect/tsgo-darwin-arm64@0.56.0": resolution: { @@ -1494,12 +1493,12 @@ packages: } hasBin: true - "@effect/vitest@4.0.5": + "@effect/vitest@4.0.6": resolution: { - integrity: sha512-q64paZ/i4V2NiRhTyqCmT2KiDbFjlxc2EZrZca8GGt+Nf8844946HyJM5bPk/q+YeRkFSxIrlCs+J+LSfky/wA==, + integrity: sha512-oKNbtyUHK5+0o730CXwojULEu7f3+c235pYFD4U5T//o+hHrJbYbZU4bRkcv936tPr5IOnmujeyQiC/LuJJjHw==, } peerDependencies: - effect: ^4.0.5 + effect: ^4.0.6 vitest: ">=5.0.0 <6.0.0" "@emnapi/core@1.10.0": @@ -5830,9 +5829,9 @@ packages: engines: { node: ">=14" } hasBin: true - effect@4.0.5: + effect@4.0.6: resolution: { - integrity: sha512-BY4/28NlpOd99yzmV741xbVGmp+1U7tks4oB7F/Png64bTa5tm5eCA6W0RiXsnuLDDoJpjfVSo4Dnq4BZb/89Q==, + integrity: sha512-Tc3mkzuLR6t6P9dhLt0D2GrxYtmU5td4XEJEiXCmILz53IRIfJyHsKZWjTA3xEjiwpNTtXBgI4iqYPxMztBDrw==, } electron-to-chromium@1.5.267: @@ -9125,9 +9124,9 @@ packages: } engines: { node: ">=20.18.1" } - undici@8.11.2: + undici@8.12.3: resolution: { - integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==, + integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==, } engines: { node: ">=22.19.0" } @@ -10313,39 +10312,39 @@ snapshots: "@dprint/win32-x64@0.54.0": optional: true - "@effect/atom-vue@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54)))": + "@effect/atom-vue@4.0.6(effect@4.0.6)(vue@3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54)))": dependencies: - effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + effect: 4.0.6 vue: 3.5.35(typescript@6.0.3(patch_hash=d2a2f8424c52b9d75deeb15b1f2367352c31126ecb19c4b469da7b943f946a54)) "@effect/language-service@0.87.5": {} - "@effect/platform-browser@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))": + "@effect/platform-browser@4.0.6(effect@4.0.6)": dependencies: - effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + effect: 4.0.6 - "@effect/platform-node-shared@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))": + "@effect/platform-node-shared@4.0.6(effect@4.0.6)": dependencies: "@types/ws": 8.18.2 - effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + effect: 4.0.6 ws: 8.22.0 transitivePeerDependencies: - bufferutil - utf-8-validate - "@effect/platform-node@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(redis@6.2.1(@opentelemetry/api@1.9.1))": + "@effect/platform-node@4.0.6(effect@4.0.6)(redis@6.2.1(@opentelemetry/api@1.9.1))": dependencies: - "@effect/platform-node-shared": 4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8)) - effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + "@effect/platform-node-shared": 4.0.6(effect@4.0.6) + effect: 4.0.6 redis: 6.2.1(@opentelemetry/api@1.9.1) - undici: 8.11.2 + undici: 8.12.3 transitivePeerDependencies: - bufferutil - utf-8-validate - "@effect/sql-sqlite-node@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))": + "@effect/sql-sqlite-node@4.0.6(effect@4.0.6)": dependencies: - effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + effect: 4.0.6 "@effect/tsgo-darwin-arm64@0.56.0": optional: true @@ -10378,14 +10377,14 @@ snapshots: "@effect/tsgo-win32-arm64": 0.56.0 "@effect/tsgo-win32-x64": 0.56.0 - "@effect/vitest@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)))": + "@effect/vitest@4.0.6(effect@4.0.6)(vitest@5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)))": dependencies: - effect: 4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + effect: 4.0.6 vitest: 5.0.5(@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.27.2)(sass@1.100.0)(terser@5.45.0)(tsx@4.22.4)(yaml@2.9.0)) - "@effect/vitest@4.0.5(effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8))(vitest@5.0.5(@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@4.0.6(effect@4.0.6)(vitest@5.0.5(@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.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8) + effect: 4.0.6 vitest: 5.0.5(@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": @@ -12987,7 +12986,7 @@ snapshots: minimatch: 9.0.1 semver: 7.7.3 - effect@4.0.5(patch_hash=a26412103069235122ff372aa85f452d503744f7345e40097d762afbb5fd6cc8): {} + effect@4.0.6: {} electron-to-chromium@1.5.267: optional: true @@ -15598,7 +15597,7 @@ snapshots: undici@7.25.0: {} - undici@8.11.2: {} + undici@8.12.3: {} unist-util-is@6.0.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6aeae6011..ce9067c29 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -17,13 +17,13 @@ minimumReleaseAgeExclude: - "@effect/tsgo-win32-x64@0.56.0" - "@effect/tsgo@0.56.0" - "@effect/language-service@0.87.5" - - "@effect/atom-vue@4.0.5" - - "@effect/platform-browser@4.0.5" - - "@effect/platform-node-shared@4.0.5" - - "@effect/platform-node@4.0.5" - - "@effect/sql-sqlite-node@4.0.5" - - "@effect/vitest@4.0.5" - - effect@4.0.5 + - "@effect/atom-vue@4.0.6" + - "@effect/platform-browser@4.0.6" + - "@effect/platform-node-shared@4.0.6" + - "@effect/platform-node@4.0.6" + - "@effect/sql-sqlite-node@4.0.6" + - "@effect/vitest@4.0.6" + - effect@4.0.6 overrides: date-fns: ^4.4.0 fast-check: ^4.9.0 @@ -34,4 +34,3 @@ patchedDependencies: typescript@6.0.3: patches/typescript.patch "@sendgrid/mail": patches/@sendgrid__mail.patch "@changesets/cli@2.31.0": patches/@changesets__cli@2.31.0.patch - effect@4.0.5: patches/effect@4.0.5.patch From 6aeb809c5bbf0737ba3809d192502fd44d0b4ff7 Mon Sep 17 00:00:00 2001 From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 05:56:47 +0000 Subject: [PATCH 3/3] Squashed 'repos/effect/' changes from 7f524e25cf..3b28bf686b 3b28bf686b Version Packages (#9197) 7e60c1ae62 Capitalize the first Unicode code point in String.capitalize (#9750) 3d84e19f8f Fix cluster shard release stalling on a defect restart (#9716) afa5da3c8a Fix multipart Content-Disposition filename decoding and parse errors (#9699) 7a8ce0098a feat: add native McpClient (#8862) 3e48f9104c Skip the async resource leak test on Bun (#9714) 6e6509e840 feat(sql-pg): add onAcquire hook to PgClient (#9738) 85e2aeee68 Add typed HTTP date and Retry-After header values to Headers (#9709) 5b7d2b5c90 fix(ai): preserve composed tool failure causes (#9741) e618f1050f Require a trusted proxy policy for forwarded headers (#9701) 4ec78b94ea Use the resolved PgClient connection for PgMigrator schema dumps (#9734) c8d26a4673 Fix Equivalence.combineAll consuming one-shot iterables after the first comparison (#9721) 8e9a15f50c test: cover the remaining untested Chunk exports (#9739) 8b185807bb Fix Schema.Tuple encoding leaving holes for omitted trailing optional elements (#9724) 5676a2ac5a Fix Fiber.runIn and Effect.forkIn hanging when the fiber closes its scope (#9729) 5b310286b8 Disable flaky MSSQL integration tests in CI (#9745) c6ee48cb43 docs(Record): document key argument passed to reduce callback (#9703) a90e133ed3 Restore missing node:http2 import in DenoHttpServer tests (#9747) c2552ec2e0 Fix Cause.pretty overflowing the stack on a cyclic error cause chain (#9727) 5739878ded Fix DateTime.formatIsoDate dropping the day for extended years (#9719) 99a4616264 feat(rpc): add opt-in request batching to the HTTP client protocol (#9656) 8d05e2861e Require Deno 2.9.3 for @effect/platform-deno to exclude HTTP/2 cancellation crashes (#9702) 9990b3977a Fix HttpApiClient and OpenApi keeping regex constraints in path params (#9726) 048e7c8712 Fix HttpClient.followRedirects dying on an invalid Location header (#9728) 392aa1cb27 Fix Schema.toFormatter throwing on symbol collections and reordering Chunks (#9720) 7f3d3dfbb7 feat(encoding): add Base64Url.encodePadded and a padding option to SchemaGetter.encodeBase64Url (#9718) ed4da351fb Wake cluster storage reads when an entity registers (#9732) db1046d1fa Encode HttpClientRequest.basicAuth credentials as UTF-8 (#9698) 28acdf3820 Fix zoneFromString rejecting GMT and accept compact UTC offsets (#9654) 66db624670 Add UserDirectories for resolving an application's directories (#8351) abcfc80218 Support Effects in vitest beforeAll and beforeEach hooks (#9675) d7a51002be Trim Bun and Deno HTTP client layerOptions tests (#9737) 688d30275f Publish only referenced `$defs` in MCP tool schemas (#8553) db130e6455 Support anchors and aliases inside YAML flow collections (#9652) 3da7c0aaba Trace SQL connection wait and sql-pg connects (#8676) 22bf7812aa fix(httpapi): reuse named schemas across response statuses (#6837) 97bfd84bb1 Support SQL Server in cluster SqlMessageStorage (#6441) 1121334ce6 fix(http): expose the original Web response from fetch client responses (#9673) 6ee26b16b5 fix(ai): interrupt Toolkit handlers on stream close and add opt-in PreliminaryBuffer (#8065) 6093cfbbb6 Guard AsyncResult equality against unrelated Equal values (#9743) d76a9411d5 Expose the underlying ClickHouse client (#9742) fa648ac4b4 fix(openapi-generator): honor explode=false for form query arrays (#8377) 3e1a035d9c Fix ClusterCron calculateNextRunFromPrevious not resuming after a lost run (#9677) 343a149fa7 docs: preserve middleware types when using Effect.fn (#8691) 2e8aae0080 feat(cli): allow Command.withExamples to derive examples from the command path (#9666) 024d7256e7 Support non-security cookie parameters in openapi-generator HttpClient output (#9695) 5af26f46cf test: cover the remaining BigInt exports (#9735) 268e09c94c fix(schema): preserve variants in Model.Class extend (#9668) 274f4a4dac fix(rpc): add Rpc.ParseOptions annotation for RPC codecs (#9665) 7a998d985f test: cover Option.fromUndefinedOr, fromNullOr, flatten, zipLeft, zipRight, composeK, toArray (#9736) e7162eae7f fix(openapi-generator): render integers as Schema.Int and only emit decodeError when used (#9694) 8727e5d2be Fix Stream.switchMap leaking scopes of inner streams replaced before they start (#9722) ad7497bbc1 Add opt-in terminal capability detection for the CLI (#8350) 5fa5fa5b9b fix(yaml): support multiline flow collections (#9700) 47487bffce Add typed Unix socket and TLS layers for Bun and Deno HTTP clients (#9704) f5ae27827a Fix ManagedRuntime interrupting requests that dispose it synchronously (#9723) 7e94505edf Fix PersistedQueue SQL Server deadlocks in offers and claims (#9715) ce13a4872e feat(ai): decode response parts without a toolkit (#9674) 6250d31d45 Fix NodeTerminal.readLine not echoing input or handling Ctrl+C on a TTY (#8408) 0bccc9c3dd Fix named-zone DateTime turning year zero and BCE dates into CE years (#9725) bc873ec495 Simplify JSON Schema annotation collection 6f6f45f480 Improve JSON Schema generation fallbacks 81fc8d9c78 Kill ClickHouse streamed queries that are not read to the end (#9712) bb49b9a15c Support HTTP/2 servers in NodeHttpServer (#9710) 0fae934a13 Destroy captured fiber AsyncResource after resuming (#9692) 84c8dc47b0 docs: explain followRedirects with FetchHttpClient (#9690) 47940c55c9 feat(cli): add Flag.withNegation and Flag.withoutNegation (#9650) 2395eeead2 Stop BunHttpServer interrupting upgraded WebSocket requests on close (#9676) f3bbbb448d Honor the Array.unionWith callback contract (#9696) 95752db5eb fix(openapi): pass custom schema annotations (#7340) b334ccf361 Add FileSystem.lstat support (#6428) ab5c1a8a35 fix(ai-openai): support safety_identifier and prompt_cache_retention request options (#9670) 225f2c6b8f fix(ai-openai): support prompt cache breakpoints on image and file input content (#9667) 5107bb6997 Handle Option from Number.parse in schema migration example (#6811) ca6d1cc5f8 Add Command.validate for rules across CLI flags (#9655) b53e8503d1 test: add file system test suite (#6555) c29ddd899c Add streaming request payload support to HttpApi (#9575) 25efaef0d8 fix(cluster): keep interrupts sent while a workflow run saves its Suspended reply (#9645) a9f0ca68aa fix(sql-pg): add acquireTimeout to bound the wait for a pooled connection (#9641) 64b5e39625 Keep agent-written changesets to one sentence (#9643) 055dd69c35 Update the release queue action (#9638) 8922816c99 Keep binary strings in openapi-generator httpapi JSON components (#9632) cff52c4a21 Start Deno SocketRunner test runners on ephemeral ports (#9637) cd2cdd5f75 Auto-rebase release branch pull requests in the release queue (#9635) fee3e10fd3 Let barrel codegen read module docs after triple-slash directives (#9633) c7072e4635 Memoize composed layers by identity (#9634) a4b38dde7f Match finite number strings in linear time (#9596) 415f0053eb Validate Schema.isBase64 / isBase64Url in linear time (#9593) 66ec7574d2 Fix cachedInvalidateWithTTL ignoring an invalidation issued while the effect is running (#9617) 22a84017e7 Fix Chunk.get throwing on a fractional index of a one-element chunk (#9611) 20e844c7ef Fix Config.schema dropping record keys for non-string key schemas (#9621) 1de10559e7 Fix Yaml block scalars rejecting tabs and dropping whitespace-only lines (#9608) c477a0a801 Fix Ini.parse dropping a trailing backslash in unquoted values (#9609) d9e2d12217 Fence SQL PersistedQueue settlements by acquisition (#9606) 2dda6cefc6 Fix Path.fromFileUrl throwing a URIError instead of failing with BadArgument (#9615) 70aad3ffe6 Fix quadratic Stream.concat chains that recurse through Stream.suspend (#9622) 0d50570bbf Fix Effect.repeat turning a Done failure from the repeated effect into a success (#9618) a9e66d8890 Fix Stream.retry slowing down with every retry (#9623) ee5c0d5f43 Fix quadratic Stream.zip when chunk sizes differ between sides (#9624) 1b740862e3 Keep typed SQL errors when SQLite rolls back a transaction itself (#9599) 30319d506c Retain an Effect atom's exposed success after asynchronous failure (#9607) 683ed2cb49 Preserve chat file bytes and URLs across JSON persistence and export (#9602) 25f29be77b Install runMain signal handlers before synchronous startup (#9601) 78d198818d Wait for replaced FiberMap and FiberHandle fibers during scope cleanup (#9603) a301f5d4ab fix(platform-deno): keep signal listeners until the main fiber exits (#9597) 8d52668533 Fix Yaml block scalar indentation picking up less indented comments (#9581) 59b63967c1 Fix DateTime.make rejecting lowercase z and gmt designators (#9582) 88bc68001c Fail SQL migrations with a typed MigrationError instead of a defect (#9598) fdb12dcd61 Fix stale refreshes renewing persisted request values (#9600) b72707c293 Correct Effect recovery and AbortSignal documentation (#9604) bbedfc71c0 Synchronize active durable race test on execution completion (#9579) 62e36b8323 Drive failure race test until execution completes (#9580) 41a1fe74e6 Read effect_cluster_entities on snapshot instead of a 1s loop (#9577) a3c56e5b5e Fix pre-response handlers accumulating across retries (#9569) feb6ce59a3 Add always-on per-type cluster mailbox size max gauge (#9578) a733eba9f6 Add SqlClient.BeginTransaction for per-transaction BEGIN statements (#9576) 5a2ed53238 Add per-entity cluster mailbox size metric and Metric series removal (#9572) 3a5e65e409 Fix request resolver caller context and document batching semantics (#9568) 9a680d6985 Add HTTP/1.1 upgrades to HttpClient, the Node server, and HttpApi (#9339) 9972b4ff2b Fix Node HTTP uploads ending when response headers arrive (#9566) b4a060b95a Fix RPC NDJSON media type and reject malformed frames (#9562) ab0f384887 Defer Node HTTP 100 Continue until request body consumption (#9564) 3934c0c618 Fix oversized buffered request bodies returning the wrong status (#9565) 84c34057ce Fix undeclared respondable errors in HttpApiBuilder (#9563) 5a2d19f60e Take the host header from the HTTP/2 authority in platform adapters (#9375) 2800a0910f Flatten redundant nested anyOf in JSON Schema output (#9549) d4425165ac Fix Bun streaming response cleanup after client abort (#9559) da14b1f18d Add opt-in typed reactivity keys (#9560) 9ae671adf3 Fix request scope leaks on early response stream cancellation (#9557) 44c8c1d1c6 Fix EmbeddingModel.embed batching callers with different contexts (#9552) 4b8a50db96 Report UTF-16 input to Xml.decode as Unsupported (#9531) 0b9842b76b Load HTTP API documentation assets lazily (#9548) b9f73f37c4 Fix Cron.prev skipping an occurrence earlier in the same second (#9539) 99e9bd98a3 Restore generic composition in Schema.toTaggedUnion (#9556) b02c1cde36 Fix Cause.prettyErrors dropping falsy error causes (#9540) 5521ca34cf ci: mitigate Tstyche registry timeouts (#9558) 9036440edc Fix unread HTTP response cleanup errors on Cloudflare Workers (#9547) 1f281878ef Fix HashMap.set keeping the old value when the new one is equal (#9536) 0b50cfc644 Fix TOML empty quoted keys being rejected (#9538) 76ec9120f4 Fix RequestResolver.withSpan ignoring options returned by an options function (#9541) d978cca029 Add TextDiff module for minimal edit scripts and unified diffs (#9434) 016354639a Add Cloudflare AI Gateway support (#9528) eddf94ac9e Refresh migration guidance for defect recovery and event log batches (#9555) 9ed2bf18da Fix RequestResolver.withCache dropping the wrapped resolver's preCheck (#9542) 4cddd3c41e Fix Struct parsers sharing output across reruns of a suspended Effect (#9494) ba2b00c065 EventLog: add EventRow for convergent row updates (#9523) 3a1e76a43a test(cluster): step the clock only until defect recovery completes (#9337) 41f08b75ab fix(effect): apply Hydration.hydrate entries immediately for atoms the registry already holds (#9518) 8215811d51 Migrate SQL notifications and invalidations to SqlClient.afterCommit (#9292) af2756dd1d Fix request registration before synchronous batch delays (#9439) 43e5e198d1 Retry EventLog remote replay batches that fail to decode or handle (#9526) 716f8dfa17 Fix Atom.Serializable JSON codec types (#9524) 561d2a1ddf Generate EventLog entry ids with a hybrid logical clock (#9521) f8bf7f4bcc Add skipInitialRun option for seeded effect atoms (#9522) 2fadc654aa Add streaming zip encoding and decoding (#9419) 8207d1e91e Add JsonArray encoding for streaming large JSON arrays (#9431) 97909b89b1 Fix dependency tracking for plain hydrated atoms (#9519) 0611a657f7 Add streaming XML decoding and encoding (#9420) 17abfbda91 Pin Deno CI to 2.9.6 to avoid fork teardown regression (#9525) 8fe2dcc213 EventLog: order conflicts by entry id across journals and server (#9520) 93abf02087 Fix RpcClient retaining requests that fail without a reply (#9460) 9ff1a73d99 Repeat stalled shard release warnings and add a releasing-shards gauge (#9513) 3314fc30c2 Apply defect precedence to typed-error recovery operators (#9516) dc923b847a Allow interruption while acquiring SQL transaction connections (#9517) ba4e07daec fix(platform-deno): answer requests when building the Response throws in DenoHttpServer (#9414) 29bee6fd4a fix(platform-node): send FormData request bodies from NodeHttpClient.layerUndici (#9413) 48102ac592 Fix retry classification for AI provider response decoding failures (#9430) 6e614e6feb Report the real scheme and an absolute originalUrl for server requests (#9415) 554731500e Speed up assignProperties with an Object.assign fast path (#9511) fed70de0df Add settled decision outcomes to preserve valid sibling answers (#9437) bcb3a86335 Warn about stalled forced shard releases (#9475) adc2dfa36c Use Effect.fnUntraced for Anthropic test helper (#9515) 447fdbf27a fix(ai-openrouter): send cache_control on assistant text and tool results (#9502) 7f823c10d2 Fix response framing for bodyless statuses and mismatched content-length (#9417) 41043e9f0d Enforce Effect.fnUntraced in tests (#9512) d1201f0425 Release NodeSink writables on upstream failure and interruption (#9402) 8e9616a2b6 fix(ai-anthropic): send cache_control on assistant content blocks (#9500) 916e70968a Add shared helpers for working with byte arrays (#9441) 73b3a6e3c3 Fix PubSub replay nodes keeping values alive until the next full GC (#9498) be026768ee Fix reused PubSub.takeUpTo taking fewer messages after replay (#9495) e77372a8a2 Pipeline event log remote write chunks (#9508) 1c70828474 Fix Iterable.dedupeAdjacent overflowing the stack on long runs (#9493) 28b293107d Fix NodeSink hanging after normal writable finish (#9429) 0317f202dd fix(platform-node): answer non-final statuses with a 500 in NodeHttpServer (#9412) a8086fa26a Clean up event log replay internals (#9507) 8a5507ba00 Fix snowflake id rounding in SQLite message storage (#9485) 5f8a6e5108 Fix unhandled ws errors when closing pending WebSocket handshakes (#9466) 73881c77dc Document how custom event journals decode EventLog payloads (#9465) 4577a28bcd Read nullable single-member allOf as nullable in OpenAPI 3.0 schemas (#9462) 61a31f4147 Reject effectful returns from synchronous Vitest properties (#9384) b487160e95 Correct v4 documentation and bound the production retry example (#9406) 679f956c4a Enforce Effect.fnUntraced for functions returning Effect.gen (#9506) 23460fad70 Fix persisted cache recovery after schema changes (#9398) 82648a21bf Return response id, model id and provider metadata from DecisionModel.decide (#9422) 2bc97faa0e Keep a leading BOM in Tar entry names (#9428) 1855f1d359 Preserve upstream errors in Channel.fromTransformStream (#9418) e33e867ef5 Report openrouter as the provider name of OpenRouterLanguageModel.model (#9421) 38288951f3 Remove stale @effect/tsgo 0.51.0 release-age exclusions (#9505) 26de86100c Avoid loading undici during Node HTTP startup (#9458) 294c72528a Update @effect/tsgo to 0.53.4 (#9504) 74532b6a3a Update repository dependencies (#9503) 89eaaf8581 Fix Socket.readerString corrupting characters split across frames (#9487) 631c5d6d9a Fix Stream.zipWithArray dropping leftovers when both sides have some (#9492) daac06f79a Fix Effect.callback keeping captured values alive until the next full GC (#9499) 41cf29b5d8 fix(platform-node): use native realpath on Windows (#9489) 02d7fc5830 EventLog: add forward and groupBatchCompaction for compaction that only drops entries (#9491) a808972626 Fix ConfigProvider module loading on Safari without regex lookbehind (#9490) d84ec3fab0 Preserve schema filter error paths with message annotations (#9409) 0c8b446709 EventJournal: track remote sources per entry instead of per remote (#9482) 8dbff2bde0 Fix Deno MSSQL fixture TLS compatibility (#9484) 6af3a47d5b Acknowledge event log writes and stop echoing them to the writer (#9477) b04191b294 Encrypt event log entries in batches (#9479) 84aeede040 Skip decrypting known remote entries in encrypted EventLog replay (#9478) 7282d5f1bc Add upload checkpoints and secondary indexes to SqlEventJournal (#9472) 3dd438810d Reduce per-entry overhead in EventLog remote replay (#9480) e8482cad96 Index unencrypted server conflict detection per batch (#9476) 77bbf16e50 Decode eventlog entries from validated fields (#9473) e04563dcd6 Speed up UUID formatting and cache event journal entry IDs (#9471) 5ae85fa8ac Index IndexedDB journal conflict lookups by event and primary key (#9474) 916f843622 Cache decoded conflict payloads within remote replay batches (#9470) 5a8e248154 Fix duplicate encrypted SQL event broadcasts (#9469) eaef09c11b Resume remote event log changes from the committed sequence (#9468) 7f32606baa Fix IndexedDB event journal upload checkpoints (#9457) 4d86d1b888 Start callback registration before the first stream pull (#9411) 6d8c8e2075 Fix GraphQL resolver batch limits for interleaved batch keys and URLs (#9401) afb5f326cc Flush Channel.decodeText at the end of the input (#9436) 25912db9d4 Preserve EventLog remote replay and compactor write order (#9393) 4b437bf464 Give finalizer and concurrent defects precedence over typed failures (#9433) 99e365d61a Prevent scoped forks from running after scope closure (#9395) 954067da7f Fix ClusterCron initial scheduling recovery (#9407) aac8d21911 Add EventLog.writeMany and monotonic entry ids (#9452) 60d2c25aa0 Fix metric reader cleanup after partial binding failure (#9389) 7c982281cf Fix effectify discarding synchronous throws after callbacks (#9403) c10335be12 Fix suspended cluster workflow scope cleanup (#9394) 658837c3b2 Preserve input order in concurrent Effect.filter and record Effect.all (#9400) fac2191196 Fix oversized rate limiter delay-mode accounting (#9390) f260830586 Avoid DevTools startup timeout with passive servers (#9410) 05c3409559 Replay remote EventLog batches in one journal callback and handler transaction (#9448) bb611522fc Restart seeded random sequences on each effect evaluation (#9385) d4f71bc425 Fix unordered stream mapping with infinite concurrency (#9383) d7987c78d5 Restrict dual JSDoc propagation to public export types (#9382) 8c23e46a82 Respect upstream demand in Stream.toReadableStream (#9396) c66241e791 Fix Stream atom mutation invalidation on unsuccessful termination (#9397) 0c477fe417 Propagate ScopedCache bulk cleanup defects (#9391) f9a7f056ba Fix TestClock warnings hidden by TestConsole (#9392) 5a13d9d1b7 fix(platform-node): answer requests when writeHead throws in NodeHttpServer (#9380) 06bc532b71 Fix singleton synchronization initialization during Sharding startup (#9443) 7711fe3d34 Fix ClickHouse Date parameter type inference (#9451) 8b40fbc93c Fix Resource.auto initial refresh scheduling (#9386) c6f9e4cbe6 Fix PubSub.takeBetween throwing on a large backlog (#9446) 915117b523 Fix silent 404s when a path parameter is longer than 100 characters (#9447) f0ccbb6917 Wait for in-progress disposal on overlapping runtime and web handler dispose (#9334) 4067e64287 Batch EventLog replay reactivity invalidations per remote batch (#9445) 88a33b4122 Speed up EventJournal timestamp decoding and memory import ordering (#9444) 4d003f90ca Answer CONNECT with 501 in the Deno and Bun HTTP servers (#9416) ee27718a02 Return typed errors for invalid bigint conversions (#9404) 3f3e119ad6 fix(testing): compare schema round trips with schema equivalence (#9379) 13fc07edeb Capture synchronous root schema callback throws as defects (#9318) 7c7b8eda54 Isolate Standard Schema adapter options per call (#9317) 4843d05d6f docs(persistence): explain multi-table store id restrictions (#9376) 739d3d1872 Answer fresh local cluster requests without a storage readback (#9336) c9a1e72c59 Fix fallback for invalid atom hydration (#9320) ae9628d7a3 Fix NodeSink settling on silent writable destruction (#9333) 3c4f88aa4d Block nested internal modules in package exports (#9326) 28813f53a5 Deduplicate reactive handlers across matching invalidation keys (#9325) e9749b04c1 Prevent stale atom hydration from replacing newer client values (#9319) d8918cbd0e docs: correct v4 behavior and stale API references (#9331) 19b63eb81b Fix disposal library references in published declarations (#9329) d5c1e5bd42 Fix RcMap owner shutdown discarding release defects (#9328) 2595d831f7 Defer Effect.fromOption error factories until execution (#9324) 931379911a Stop failed reactive queries from rerunning on invalidation (#9322) 74a759699a Preserve runtime-owned layers when injecting atom test layers (#9313) 5462ed7b3a Fix Cause serialization and schema error paths to use reasons (#9323) 0a7978ea8e Fix HTTP RPC cancellation before the first response (#9297) 4bdcc1f070 Accept SemVer.Input in range constructors and matching (#9260) 34ec4407a1 Fix logging an HTTP response locking its body (#9293) ad6901857e Run SqlClient callbacks after the outermost transaction commits (#9289) 1af11fda1b Fix AtomHttpApi query response mode inference (#9290) c53c194511 Fix CLI lexing of clustered short options with an inline value (#9287) 819d2e1a2b Fix OTEL_RESOURCE_ATTRIBUTES parsing dropping values with equals signs (#9278) f30db155b4 Map EPERM errno to PermissionDenied in Node and Deno error handlers (#9283) da91aae843 Fix WorkerRunner onError reading wrong property on ErrorEvent (#9280) e8b24ef951 Treat --=value as a plain value in the CLI lexer (#9285) 090923611b Handle large callback results in Stream.mapAccum (#9274) 463eca3609 Correct Number.nextPow2 at floating-point boundaries (#9270) eab7e6acfc Preserve Trie iterator progress (#9271) a706d73db8 Collect large chunks without exceeding argument limits (#9272) d45379064a Fix OpenRouter strict default for OpenAI-transformed response schemas (#9263) 383997a33f Give durable Sharding registration tests more timeout headroom (#9268) b15d6316ab Cache pnpm store on GitHub-hosted runners (#9269) 14db221f8f Fix Duration.fromInputUnsafe losing precision for large tuple and object inputs (#9258) b4378736a2 Refresh migration guidance for stat metadata, matching and OpenAI schemas (#9266) 8fbb39eb92 Fix cross-service AtomHttpApi and AtomRpc mutation collisions (#9267) 0dbbea65a9 Fix static directory index trailing-slash redirects (#9264) 7cccb93857 Fix nanosecond precision in decimal Duration strings (#9261) cc0090657e Add shard release duration timer (#9256) 51930c0683 Fix typed multipart errors for missing boundaries (#9262) e24cd76392 Fix NodeHttpServer ending a failed streamed response as if complete (#9255) 0386f88295 Fix duplicate workflow compensations after memory-engine replays (#9239) 4839c601bd fix(effect): Made catchIf NoInfer Refinement's first arg with generic Err param (#9252) fad975b13e change changeset to patch 191308bd0f feat(schema): add Schema.toCodecGoJson (#9245) 9a5331db94 Add Semantic Versioning types and range support (#9243) 4b96570ed2 Preserve literal member inference in Schema.Enum (#9170) f0e941ee2f Fix out-of-order unencrypted event-log replication (#9225) fdc8c19362 Use live deadlines for PostgreSQL fixture teardown (#9246) a299341742 Fix NodeStream hangs on premature readable closure (#9228) 9c960111de Fix Redis PersistedQueue data loss on lone-surrogate payloads (#9235) 7f504a35ed Fix ClickHouse migrator ledger creation (#9248) 7886b84765 Add JSON Schema custom annotation imports (#9247) 5a67e0b3a8 Prevent PGlite connection starvation in SQL runner storage (#9222) 425cedb580 Fix memory workflow completion after unsafe interruption (#9234) 00261cc12c Fix DevTools client reconnection after socket failures (#9226) 09ba1f5995 Fix cluster shards gauge after shard release (#9244) 12b2726651 Fix provider-nested OpenAI error metadata (#9242) 1e949935a1 Fix RPC connection defects failing synchronously started requests (#9232) e9c842bc29 Fix SQL persistence expiry checks for reused reads (#9240) 11bb31fec0 Preserve Web stream cancellation failures (#9191) c3983f4db2 Fix fractional Redis fixed-window rate limits (#9241) 5a3ebc0821 Fix absolute HTTP URLs in opaque-origin documents (#9203) bfb32a3cf5 Report Atom scope finalizer failures (#9190) ac68940f7e Fix descendant drain timing after slow child-process exit (#9238) 403298ac45 Fix in-memory workflow safe interruption on completion (#9237) da33572776 Fix remote SQL journal conflicts and failed-write persistence (#9233) 3ec8100c79 Fix Resource.auto refresh loop stopping after failure (#9230) a049d4ad25 Fix failed session-auth binding lookup caching (#9224) 8037a65f40 Fix undecodable event payloads stalling group compaction (#9227) 973d599a3e Fix OTLP attributes with integers outside the int64 range (#9199) 2f066a58e9 Fix OTLP JSON export writing NaN and Infinity as null (#9198) 1e7909beb0 feat(encoding): add streaming tar codec (#9148) bc07bd4e86 Bound PostgreSQL fixture teardown and retry interrupted waits (#9188) a5e215529f Bound Node PostgreSQL fixture teardown waits (#9189) git-subtree-dir: repos/effect git-subtree-split: 3b28bf686bf26e9700edd39c025aff2744dc5303 --- {repos/effect/.agents => .agents}/AGENTS.md | 0 .../skills/ai-docs/SKILL.md | 0 .../skills/bundle-analysis/SKILL.md | 0 .../skills/changesets/SKILL.md | 9 +- .agents/skills/changesets/authoring.md | 56 + .../skills/ci-maintenance/SKILL.md | 0 .../ci-maintenance/privileged-workflows.md | 0 .../skills/dependency-maintenance/SKILL.md | 0 .../coordinated-upgrades.md | 0 .../dependency-maintenance/manifest-roles.md | 0 .../skills/effect-development/SKILL.md | 0 .../skills/jsdocs/SKILL.md | 0 .../skills/jsdocs/categories.md | 0 .../skills/jsdocs/declarations.md | 0 .../skills/jsdocs/examples.md | 0 .../skills/migration-guidance/SKILL.md | 0 .../skills/migration-guidance/annotations.md | 0 .../skills/migration-guidance/generation.md | 0 .../skills/package-development/SKILL.md | 0 .../package-development/dependencies.md | 0 .../skills/package-development/publishing.md | 0 .../package-development/registration.md | 0 .../skills/performance-analysis/SKILL.md | 0 .../skills/performance-analysis/runtime.md | 0 .../skills/performance-analysis/types.md | 0 .../skills/pull-requests/SKILL.md | 0 .../skills/test-development/SKILL.md | 0 .../test-development/displayed-types.md | 0 .../skills/test-development/runtime.md | 0 .../skills/test-development/types.md | 0 .../skills/vendored-assets/SKILL.md | 0 .../skills/vendored-assets/browser-assets.md | 0 .changeset/README.md | 8 - .changeset/config.json | 59 +- .changeset/node-24-22-1.md | 5 - .changeset/update-effect-4.0.5.md | 6 - .claude/settings.json | 18 - .devcontainer/Dockerfile | 42 - .devcontainer/devcontainer.json | 14 - .devcontainer/humanlog.sh | 2 - .devcontainer/usepnpm.sh | 26 - .envrc | 4 - .githooks/README.md | 49 - .githooks/post-checkout | 98 - .githooks/pre-commit | 12 - .githooks/pre-push | 17 - .github/FUNDING.yml | 3 - .../actions/setup/action.yaml | 2 + .github/npm-trusted-publishing.md | 85 - .../workflows/ai-codegen.yml | 0 .../workflows/bundle-comment.yml | 0 .../.github => .github}/workflows/check.yml | 11 +- .github/workflows/ci.yml | 179 - .github/workflows/claude.yml | 49 - .../.github => .github}/workflows/cluster.yml | 0 .../workflows/effect-bot.yml | 0 .../workflows/npm-promote.yml | 0 .github/workflows/publish-snapshot.yml | 105 - .github/workflows/publish-wiki.yml | 18 - .../workflows/release-queue.yml | 2 +- .github/workflows/release.yaml | 94 - .../.github => .github}/workflows/release.yml | 0 .../workflows/snapshot.yml | 0 .gitignore | 89 +- .ncurc.json | 20 - .npmrc | 1 - .nvmrc | 1 - .oxlintrc.json | 76 +- .specs/Update-to-latest-Effect.md | 23 - .specs/changeset-publishing.md | 138 - .vscode/extensions.json | 5 +- .vscode/launch.json | 22 - .vscode/model.code-snippets | 77 - .vscode/service.code-snippets | 14 - .vscode/settings.json | 84 +- .vscode/tasks.json | 38 - AGENTS.md | 217 - CLAUDE.md | 3 - repos/effect/LICENSE => LICENSE | 0 repos/effect/LLMS.md => LLMS.md | 0 repos/effect/MIGRATION.md => MIGRATION.md | 0 README.md | 211 +- {repos/effect/ai-docs => ai-docs}/README.md | 0 .../effect/ai-docs => ai-docs}/package.json | 2 +- .../src/01_effect/01_basics/01_effect-gen.ts | 0 .../src/01_effect/01_basics/02_effect-fn.ts | 0 .../01_basics/10_creating-effects.ts | 0 .../src/01_effect/01_basics/index.md | 0 .../01_effect/02_schema/10_schema-basics.ts | 0 .../src/01_effect/02_schema/index.md | 0 .../src/01_effect/03_services/01_service.ts | 0 .../src/01_effect/03_services/10_reference.ts | 0 .../03_services/20_layer-composition.ts | 0 .../01_effect/03_services/20_layer-unwrap.ts | 0 .../src/01_effect/03_services/index.md | 0 .../01_effect/04_errors/01_error-handling.ts | 0 .../src/01_effect/04_errors/10_catch-tags.ts | 0 .../01_effect/04_errors/20_reason-errors.ts | 0 .../src/01_effect/04_errors/index.md | 0 .../05_resources/10_acquire-release.ts | 0 .../05_resources/20_layer-side-effects.ts | 0 .../01_effect/05_resources/30_layer-map.ts | 0 .../src/01_effect/05_resources/index.md | 0 .../src/01_effect/06_running/10_run-main.ts | 0 .../01_effect/06_running/20_layer-launch.ts | 0 .../src/01_effect/06_running/index.md | 0 .../src/01_effect/07_pubsub/10_pubsub.ts | 0 .../src/01_effect/07_pubsub/index.md | 0 .../src/03_stream/10_creating-streams.ts | 0 .../src/03_stream/20_consuming-streams.ts | 0 .../src/03_stream/30_encoding.ts | 0 .../src/03_stream/index.md | 0 .../src/04_integration/10_managed-runtime.ts | 0 .../src/04_integration/index.md | 0 .../src/05_batching/10_request-resolver.ts | 0 .../src/05_batching/index.md | 0 .../src/06_schedule/10_schedules.ts | 9 +- .../src/06_schedule/index.md | 0 .../07_datetime/10_creating-and-formatting.ts | 0 .../src/07_datetime/20_time-zones.ts | 0 .../src/07_datetime/index.md | 0 .../src/08_observability/10_logging.ts | 0 .../src/08_observability/20_otlp-tracing.ts | 0 .../src/08_observability/index.md | 0 .../src/09_testing/10_effect-tests.ts | 0 .../src/09_testing/20_layer-tests.ts | 0 .../src/09_testing/index.md | 0 .../src/10_predicate/01_basics.ts | 0 .../src/10_predicate/index.md | 0 .../src/40_sql/10_basics.ts | 0 .../ai-docs => ai-docs}/src/40_sql/index.md | 0 .../src/50_http-client/10_basics.ts | 0 .../src/50_http-client/index.md | 0 .../src/51_http-server/10_basics.ts | 0 .../src/51_http-server/20_testing.ts | 0 .../src/51_http-server/fixtures/api/Api.ts | 0 .../fixtures/api/Authorization.ts | 0 .../src/51_http-server/fixtures/api/System.ts | 0 .../src/51_http-server/fixtures/api/Users.ts | 0 .../51_http-server/fixtures/domain/User.ts | 0 .../fixtures/domain/UserErrors.ts | 0 .../fixtures/server/Authorization.ts | 0 .../51_http-server/fixtures/server/Users.ts | 0 .../fixtures/server/Users/http.ts | 0 .../src/51_http-server/index.md | 0 .../src/52_graphql/10_basics.ts | 0 .../src/52_graphql/20_testing.ts | 0 .../src/52_graphql/30_batching.ts | 0 .../src/52_graphql/40_aliased-batching.ts | 0 .../src/52_graphql/50_errors-and-retries.ts | 0 .../src/52_graphql/index.md | 0 .../10_working-with-child-processes.ts | 0 .../src/60_child-process/index.md | 0 .../src/70_cli/10_basics.ts | 0 .../ai-docs => ai-docs}/src/70_cli/index.md | 0 .../src/71_ai/10_language-model.ts | 0 .../ai-docs => ai-docs}/src/71_ai/20_tools.ts | 0 .../ai-docs => ai-docs}/src/71_ai/30_chat.ts | 0 .../src/71_ai/fixtures/domain/LaunchPlan.ts | 0 .../ai-docs => ai-docs}/src/71_ai/index.md | 0 .../src/80_cluster/10_entities.ts | 0 .../src/80_cluster/index.md | 0 .../effect/ai-docs => ai-docs}/src/index.md | 0 .../effect/ai-docs => ai-docs}/tsconfig.json | 4 +- assets/dev/effect-app-apple-touch-180.png | Bin 11603 -> 0 bytes repos/effect/deno.json => deno.json | 0 repos/effect/dprint.json => dprint.json | 0 dprint.jsonc | 6 - flake.lock | 46 +- flake.nix | 122 +- .../jsdocs.config.json => jsdocs.config.json | 0 lint-staged.config.js | 29 - .../annotations/README.md | 0 .../annotations/effect__Arbitrary.yaml | 0 .../annotations/effect__Array.yaml | 0 .../annotations/effect__BigDecimal.yaml | 0 .../annotations/effect__BigInt.yaml | 0 .../annotations/effect__Brand.yaml | 0 .../annotations/effect__Cache.yaml | 0 .../annotations/effect__Cause.yaml | 3 +- .../annotations/effect__Channel.yaml | 17 +- .../effect__ChildExecutorDecision.yaml | 0 .../annotations/effect__Chunk.yaml | 0 .../annotations/effect__Clock.yaml | 0 .../annotations/effect__Config.yaml | 0 .../annotations/effect__ConfigError.yaml | 0 .../annotations/effect__ConfigProvider.yaml | 0 .../effect__ConfigProviderPathPatch.yaml | 0 .../annotations/effect__Console.yaml | 0 .../annotations/effect__Context.yaml | 0 .../annotations/effect__Cron.yaml | 0 .../annotations/effect__Data.yaml | 0 .../annotations/effect__DateTime.yaml | 0 .../annotations/effect__DefaultServices.yaml | 0 .../annotations/effect__Deferred.yaml | 0 .../annotations/effect__Differ.yaml | 0 .../annotations/effect__Duration.yaml | 0 .../annotations/effect__Effect.yaml | 85 +- .../annotations/effect__Effectable.yaml | 0 .../annotations/effect__Either.yaml | 0 .../annotations/effect__Encoding.yaml | 0 .../annotations/effect__Equal.yaml | 0 .../annotations/effect__Equivalence.yaml | 0 .../annotations/effect__ExecutionPlan.yaml | 0 .../effect__ExecutionStrategy.yaml | 0 .../annotations/effect__Exit.yaml | 9 + .../annotations/effect__FastCheck.yaml | 0 .../annotations/effect__Fiber.yaml | 0 .../annotations/effect__FiberHandle.yaml | 0 .../annotations/effect__FiberId.yaml | 0 .../annotations/effect__FiberMap.yaml | 0 .../annotations/effect__FiberRef.yaml | 0 .../annotations/effect__FiberRefs.yaml | 0 .../annotations/effect__FiberRefsPatch.yaml | 0 .../annotations/effect__FiberSet.yaml | 0 .../annotations/effect__FiberStatus.yaml | 0 .../annotations/effect__Function.yaml | 0 .../annotations/effect__GlobalValue.yaml | 0 .../annotations/effect__Graph.yaml | 0 .../annotations/effect__GroupBy.yaml | 0 .../annotations/effect__Hash.yaml | 0 .../annotations/effect__HashMap.yaml | 0 .../annotations/effect__HashSet.yaml | 0 .../annotations/effect__Inspectable.yaml | 0 .../annotations/effect__Iterable.yaml | 0 .../annotations/effect__JSONSchema.yaml | 0 .../annotations/effect__KeyedPool.yaml | 0 .../annotations/effect__Layer.yaml | 4 +- .../annotations/effect__LayerMap.yaml | 0 .../annotations/effect__List.yaml | 0 .../annotations/effect__LogLevel.yaml | 0 .../annotations/effect__LogSpan.yaml | 0 .../annotations/effect__Logger.yaml | 0 .../annotations/effect__Mailbox.yaml | 0 .../annotations/effect__ManagedRuntime.yaml | 0 .../annotations/effect__Match.yaml | 18 +- .../annotations/effect__MergeDecision.yaml | 0 .../annotations/effect__MergeState.yaml | 0 .../annotations/effect__MergeStrategy.yaml | 0 .../annotations/effect__Metric.yaml | 0 .../annotations/effect__MetricBoundaries.yaml | 0 .../annotations/effect__MetricHook.yaml | 0 .../annotations/effect__MetricKey.yaml | 0 .../annotations/effect__MetricKeyType.yaml | 0 .../annotations/effect__MetricLabel.yaml | 0 .../annotations/effect__MetricPair.yaml | 0 .../annotations/effect__MetricPolling.yaml | 0 .../annotations/effect__MetricRegistry.yaml | 0 .../annotations/effect__MetricState.yaml | 0 .../annotations/effect__Micro.yaml | 0 .../annotations/effect__ModuleVersion.yaml | 0 .../annotations/effect__MutableHashMap.yaml | 0 .../annotations/effect__MutableHashSet.yaml | 0 .../annotations/effect__MutableList.yaml | 0 .../annotations/effect__MutableQueue.yaml | 0 .../annotations/effect__MutableRef.yaml | 0 .../annotations/effect__Number.yaml | 0 .../annotations/effect__Option.yaml | 0 .../annotations/effect__Order.yaml | 0 .../annotations/effect__Ordering.yaml | 0 .../annotations/effect__ParseResult.yaml | 0 .../effect__PartitionedSemaphore.yaml | 0 .../annotations/effect__Pipeable.yaml | 0 .../annotations/effect__Pool.yaml | 0 .../annotations/effect__Predicate.yaml | 0 .../annotations/effect__Pretty.yaml | 0 .../annotations/effect__PubSub.yaml | 0 .../annotations/effect__Queue.yaml | 0 .../annotations/effect__Random.yaml | 0 .../annotations/effect__RateLimiter.yaml | 0 .../annotations/effect__RcMap.yaml | 0 .../annotations/effect__RcRef.yaml | 0 .../annotations/effect__Readable.yaml | 0 .../annotations/effect__Record.yaml | 0 .../annotations/effect__RedBlackTree.yaml | 0 .../annotations/effect__Redacted.yaml | 0 .../annotations/effect__Ref.yaml | 0 .../annotations/effect__Reloadable.yaml | 0 .../annotations/effect__Request.yaml | 0 .../annotations/effect__RequestBlock.yaml | 0 .../annotations/effect__RequestResolver.yaml | 0 .../annotations/effect__Resource.yaml | 0 .../annotations/effect__Runtime.yaml | 0 .../annotations/effect__RuntimeFlags.yaml | 0 .../effect__RuntimeFlagsPatch.yaml | 0 .../annotations/effect__STM.yaml | 0 .../annotations/effect__Schedule.yaml | 0 .../annotations/effect__ScheduleDecision.yaml | 0 .../annotations/effect__ScheduleInterval.yaml | 0 .../effect__ScheduleIntervals.yaml | 0 .../annotations/effect__Scheduler.yaml | 0 .../annotations/effect__Schema.yaml | 0 .../annotations/effect__SchemaAST.yaml | 0 .../annotations/effect__Scope.yaml | 0 .../annotations/effect__ScopedCache.yaml | 0 .../annotations/effect__ScopedRef.yaml | 0 .../annotations/effect__Secret.yaml | 0 .../effect__SingleProducerAsyncInput.yaml | 0 .../annotations/effect__Sink.yaml | 17 +- .../annotations/effect__SortedMap.yaml | 0 .../annotations/effect__SortedSet.yaml | 0 .../annotations/effect__Stream.yaml | 46 +- .../annotations/effect__StreamEmit.yaml | 0 .../effect__StreamHaltStrategy.yaml | 0 .../annotations/effect__Streamable.yaml | 0 .../annotations/effect__Struct.yaml | 0 .../annotations/effect__Subscribable.yaml | 0 .../annotations/effect__SubscriptionRef.yaml | 0 .../annotations/effect__Supervisor.yaml | 0 .../annotations/effect__Symbol.yaml | 0 .../annotations/effect__SynchronizedRef.yaml | 0 .../annotations/effect__TArray.yaml | 0 .../annotations/effect__TDeferred.yaml | 0 .../annotations/effect__TMap.yaml | 0 .../annotations/effect__TPriorityQueue.yaml | 0 .../annotations/effect__TPubSub.yaml | 0 .../annotations/effect__TQueue.yaml | 0 .../annotations/effect__TRandom.yaml | 0 .../annotations/effect__TReentrantLock.yaml | 0 .../annotations/effect__TRef.yaml | 0 .../annotations/effect__TSemaphore.yaml | 0 .../annotations/effect__TSet.yaml | 0 .../annotations/effect__TSubscriptionRef.yaml | 0 .../annotations/effect__Take.yaml | 0 .../annotations/effect__TestAnnotation.yaml | 0 .../effect__TestAnnotationMap.yaml | 0 .../annotations/effect__TestAnnotations.yaml | 0 .../annotations/effect__TestClock.yaml | 0 .../annotations/effect__TestConfig.yaml | 0 .../annotations/effect__TestContext.yaml | 0 .../annotations/effect__TestLive.yaml | 0 .../annotations/effect__TestServices.yaml | 0 .../annotations/effect__TestSized.yaml | 0 .../annotations/effect__Tracer.yaml | 0 .../annotations/effect__Trie.yaml | 0 .../annotations/effect__Tuple.yaml | 0 .../annotations/effect__Types.yaml | 0 .../effect__UpstreamPullRequest.yaml | 0 .../effect__UpstreamPullStrategy.yaml | 0 .../annotations/effect__Utils.yaml | 0 .../effect__ai-amazon-bedrock.yaml | 0 ...i-amazon-bedrock__AmazonBedrockClient.yaml | 0 ...i-amazon-bedrock__AmazonBedrockConfig.yaml | 0 ...n-bedrock__AmazonBedrockLanguageModel.yaml | 0 ...i-amazon-bedrock__AmazonBedrockSchema.yaml | 0 ..._ai-amazon-bedrock__AmazonBedrockTool.yaml | 0 ...i-amazon-bedrock__EventStreamEncoding.yaml | 0 .../effect__ai-amazon-bedrock__index.yaml | 0 ...effect__ai-anthropic__AnthropicClient.yaml | 0 ...effect__ai-anthropic__AnthropicConfig.yaml | 0 ..._ai-anthropic__AnthropicLanguageModel.yaml | 0 ...ect__ai-anthropic__AnthropicTokenizer.yaml | 0 .../effect__ai-anthropic__AnthropicTool.yaml | 0 .../effect__ai-anthropic__Generated.yaml | 0 .../effect__ai-anthropic__index.yaml | 0 .../annotations/effect__ai-google.yaml | 0 .../effect__ai-google__Generated.yaml | 0 .../effect__ai-google__GoogleClient.yaml | 0 .../effect__ai-google__GoogleConfig.yaml | 0 ...ffect__ai-google__GoogleLanguageModel.yaml | 0 .../effect__ai-google__GoogleTool.yaml | 0 .../annotations/effect__ai-google__index.yaml | 0 .../effect__ai-openai__Generated.yaml | 336 +- .../effect__ai-openai__OpenAiClient.yaml | 0 .../effect__ai-openai__OpenAiConfig.yaml | 0 ...fect__ai-openai__OpenAiEmbeddingModel.yaml | 0 ...ffect__ai-openai__OpenAiLanguageModel.yaml | 0 .../effect__ai-openai__OpenAiTelemetry.yaml | 0 .../effect__ai-openai__OpenAiTokenizer.yaml | 0 .../effect__ai-openai__OpenAiTool.yaml | 0 .../annotations/effect__ai-openai__index.yaml | 0 .../effect__ai-openrouter__Generated.yaml | 0 ...fect__ai-openrouter__OpenRouterClient.yaml | 0 ...fect__ai-openrouter__OpenRouterConfig.yaml | 0 ...i-openrouter__OpenRouterLanguageModel.yaml | 0 .../effect__ai-openrouter__index.yaml | 0 .../annotations/effect__ai.yaml | 0 .../annotations/effect__ai__AiError.yaml | 8 +- .../annotations/effect__ai__Chat.yaml | 0 .../effect__ai__EmbeddingModel.yaml | 0 .../annotations/effect__ai__IdGenerator.yaml | 0 .../effect__ai__LanguageModel.yaml | 0 .../annotations/effect__ai__McpSchema.yaml | 0 .../annotations/effect__ai__McpServer.yaml | 0 .../annotations/effect__ai__Model.yaml | 0 .../annotations/effect__ai__Prompt.yaml | 0 .../annotations/effect__ai__Response.yaml | 0 .../annotations/effect__ai__Telemetry.yaml | 0 .../annotations/effect__ai__Tool.yaml | 0 .../annotations/effect__ai__Toolkit.yaml | 0 .../annotations/effect__ai__index.yaml | 0 .../annotations/effect__cli.yaml | 0 .../annotations/effect__cli__Args.yaml | 0 .../annotations/effect__cli__AutoCorrect.yaml | 0 .../effect__cli__BuiltInOptions.yaml | 0 .../annotations/effect__cli__CliApp.yaml | 0 .../annotations/effect__cli__CliConfig.yaml | 0 .../annotations/effect__cli__Command.yaml | 0 .../effect__cli__CommandDescriptor.yaml | 0 .../effect__cli__CommandDirective.yaml | 0 .../annotations/effect__cli__ConfigFile.yaml | 0 .../annotations/effect__cli__HelpDoc.yaml | 0 .../effect__cli__HelpDoc__Span.yaml | 0 .../annotations/effect__cli__Options.yaml | 0 .../annotations/effect__cli__Primitive.yaml | 0 .../annotations/effect__cli__Prompt.yaml | 0 .../annotations/effect__cli__Usage.yaml | 0 .../effect__cli__ValidationError.yaml | 0 .../annotations/effect__cli__index.yaml | 0 .../annotations/effect__cluster.yaml | 0 .../effect__cluster__ClusterCron.yaml | 0 .../effect__cluster__ClusterError.yaml | 0 .../effect__cluster__ClusterSchema.yaml | 0 ...ffect__cluster__ClusterWorkflowEngine.yaml | 0 .../effect__cluster__DeliverAt.yaml | 0 .../annotations/effect__cluster__Entity.yaml | 0 .../effect__cluster__EntityAddress.yaml | 0 .../effect__cluster__EntityId.yaml | 0 .../effect__cluster__EntityProxy.yaml | 0 .../effect__cluster__EntityProxyServer.yaml | 0 .../effect__cluster__EntityResource.yaml | 0 .../effect__cluster__Envelope.yaml | 0 .../effect__cluster__HttpRunner.yaml | 0 .../effect__cluster__K8sHttpClient.yaml | 0 .../effect__cluster__MachineId.yaml | 0 .../annotations/effect__cluster__Message.yaml | 0 .../effect__cluster__MessageStorage.yaml | 0 .../annotations/effect__cluster__Reply.yaml | 0 .../annotations/effect__cluster__Runner.yaml | 0 .../effect__cluster__RunnerAddress.yaml | 0 .../effect__cluster__RunnerHealth.yaml | 0 .../effect__cluster__RunnerServer.yaml | 0 .../effect__cluster__RunnerStorage.yaml | 0 .../annotations/effect__cluster__Runners.yaml | 0 .../annotations/effect__cluster__ShardId.yaml | 0 .../effect__cluster__Sharding.yaml | 0 .../effect__cluster__ShardingConfig.yaml | 0 ...t__cluster__ShardingRegistrationEvent.yaml | 0 .../effect__cluster__SingleRunner.yaml | 0 .../effect__cluster__Singleton.yaml | 0 .../effect__cluster__SingletonAddress.yaml | 0 .../effect__cluster__Snowflake.yaml | 0 .../effect__cluster__SocketRunner.yaml | 0 .../effect__cluster__SqlMessageStorage.yaml | 0 .../effect__cluster__SqlRunnerStorage.yaml | 0 .../effect__cluster__TestRunner.yaml | 0 .../annotations/effect__cluster__index.yaml | 0 .../annotations/effect__experimental.yaml | 0 .../effect__experimental__DevTools.yaml | 0 ...ffect__experimental__DevTools__Client.yaml | 0 ...ffect__experimental__DevTools__Domain.yaml | 0 ...ffect__experimental__DevTools__Server.yaml | 0 .../effect__experimental__Event.yaml | 0 .../effect__experimental__EventGroup.yaml | 0 .../effect__experimental__EventJournal.yaml | 7 +- .../effect__experimental__EventLog.yaml | 11 +- ...ect__experimental__EventLogEncryption.yaml | 12 + .../effect__experimental__EventLogRemote.yaml | 15 +- .../effect__experimental__EventLogServer.yaml | 6 +- ...erimental__EventLogServer__Cloudflare.yaml | 0 .../effect__experimental__Machine.yaml | 0 ...ect__experimental__Machine__Procedure.yaml | 0 ..._experimental__Machine__ProcedureList.yaml | 0 ...l__Machine__SerializableProcedureList.yaml | 0 .../effect__experimental__PersistedCache.yaml | 0 .../effect__experimental__PersistedQueue.yaml | 0 ...__experimental__PersistedQueue__Redis.yaml | 0 .../effect__experimental__Persistence.yaml | 0 ...fect__experimental__Persistence__Lmdb.yaml | 0 ...ect__experimental__Persistence__Redis.yaml | 0 .../effect__experimental__RateLimiter.yaml | 0 ...ect__experimental__RateLimiter__Redis.yaml | 0 .../effect__experimental__Reactivity.yaml | 0 ...effect__experimental__RequestResolver.yaml | 0 .../effect__experimental__Sse.yaml | 0 .../effect__experimental__VariantSchema.yaml | 0 .../effect__experimental__index.yaml | 0 .../annotations/effect__index.yaml | 0 .../effect__opentelemetry__Logger.yaml | 0 .../effect__opentelemetry__Metrics.yaml | 0 .../effect__opentelemetry__NodeSdk.yaml | 0 .../effect__opentelemetry__Otlp.yaml | 0 .../effect__opentelemetry__OtlpLogger.yaml | 0 .../effect__opentelemetry__OtlpMetrics.yaml | 0 .../effect__opentelemetry__OtlpResource.yaml | 0 .../effect__opentelemetry__OtlpTracer.yaml | 0 .../effect__opentelemetry__Resource.yaml | 0 .../effect__opentelemetry__Tracer.yaml | 0 .../effect__opentelemetry__WebSdk.yaml | 0 .../effect__opentelemetry__index.yaml | 0 ...__platform-browser__BrowserHttpClient.yaml | 0 ...fect__platform-browser__BrowserWorker.yaml | 0 ...platform-browser__BrowserWorkerRunner.yaml | 0 .../effect__platform-browser__Clipboard.yaml | 0 ...effect__platform-browser__Geolocation.yaml | 0 ...effect__platform-browser__Permissions.yaml | 0 .../effect__platform-browser__index.yaml | 0 ...ect__platform-bun__BunCommandExecutor.yaml | 0 .../effect__platform-bun__BunContext.yaml | 0 ...orm-bun__BunFileSystem__ParcelWatcher.yaml | 0 ...effect__platform-bun__BunHttpPlatform.yaml | 0 .../effect__platform-bun__BunHttpServer.yaml | 0 ...t__platform-bun__BunHttpServerRequest.yaml | 0 ...ffect__platform-bun__BunKeyValueStore.yaml | 0 .../effect__platform-bun__BunSink.yaml | 0 .../effect__platform-bun__BunSocket.yaml | 0 ...effect__platform-bun__BunSocketServer.yaml | 0 .../effect__platform-bun__BunStream.yaml | 0 .../effect__platform-bun__BunTerminal.yaml | 0 .../effect__platform-bun__BunWorker.yaml | 0 .../effect__platform-bun__index.yaml | 0 ...form-node-shared__NodeCommandExecutor.yaml | 0 ...shared__NodeFileSystem__ParcelWatcher.yaml | 0 ...atform-node-shared__NodeKeyValueStore.yaml | 0 ...__platform-node-shared__NodeMultipart.yaml | 0 ...ffect__platform-node-shared__NodeSink.yaml | 0 ...ect__platform-node-shared__NodeSocket.yaml | 0 ...ect__platform-node-shared__NodeStream.yaml | 0 ...t__platform-node-shared__NodeTerminal.yaml | 0 ...t__platform-node__NodeCommandExecutor.yaml | 0 .../effect__platform-node__NodeContext.yaml | 0 ...m-node__NodeFileSystem__ParcelWatcher.yaml | 0 ...effect__platform-node__NodeHttpClient.yaml | 0 ...effect__platform-node__NodeHttpServer.yaml | 0 ...ect__platform-node__NodeKeyValueStore.yaml | 0 .../effect__platform-node__NodeTerminal.yaml | 0 .../effect__platform-node__NodeWorker.yaml | 0 .../effect__platform-node__Undici.yaml | 0 .../effect__platform-node__index.yaml | 0 .../annotations/effect__platform.yaml | 0 .../effect__platform__ChannelSchema.yaml | 0 .../effect__platform__Command.yaml | 0 .../effect__platform__CommandExecutor.yaml | 0 .../effect__platform__Cookies.yaml | 0 .../effect__platform__Effectify.yaml | 0 .../annotations/effect__platform__Error.yaml | 0 .../annotations/effect__platform__Etag.yaml | 0 .../effect__platform__FetchHttpClient.yaml | 0 .../effect__platform__FileSystem.yaml | 3 + .../effect__platform__Headers.yaml | 0 .../effect__platform__HttpApi.yaml | 0 .../effect__platform__HttpApiBuilder.yaml | 0 .../effect__platform__HttpApiClient.yaml | 0 .../effect__platform__HttpApiEndpoint.yaml | 0 .../effect__platform__HttpApiError.yaml | 0 .../effect__platform__HttpApiGroup.yaml | 0 .../effect__platform__HttpApiMiddleware.yaml | 0 .../effect__platform__HttpApiScalar.yaml | 0 .../effect__platform__HttpApiSchema.yaml | 0 .../effect__platform__HttpApiSecurity.yaml | 0 .../effect__platform__HttpApiSwagger.yaml | 0 .../effect__platform__HttpApp.yaml | 0 .../effect__platform__HttpBody.yaml | 0 .../effect__platform__HttpClient.yaml | 0 .../effect__platform__HttpClientError.yaml | 0 .../effect__platform__HttpClientRequest.yaml | 0 .../effect__platform__HttpClientResponse.yaml | 0 ...effect__platform__HttpIncomingMessage.yaml | 0 .../effect__platform__HttpLayerRouter.yaml | 0 .../effect__platform__HttpMethod.yaml | 0 .../effect__platform__HttpMiddleware.yaml | 3 + .../effect__platform__HttpMultiplex.yaml | 0 .../effect__platform__HttpPlatform.yaml | 0 .../effect__platform__HttpRouter.yaml | 0 .../effect__platform__HttpServer.yaml | 0 .../effect__platform__HttpServerError.yaml | 0 .../effect__platform__HttpServerRequest.yaml | 0 ...fect__platform__HttpServerRespondable.yaml | 0 .../effect__platform__HttpServerResponse.yaml | 0 .../effect__platform__KeyValueStore.yaml | 0 .../effect__platform__MsgPack.yaml | 0 .../effect__platform__Multipart.yaml | 0 .../annotations/effect__platform__Ndjson.yaml | 0 .../effect__platform__OpenApi.yaml | 0 .../effect__platform__OpenApiJsonSchema.yaml | 0 .../annotations/effect__platform__Path.yaml | 6 + ...ect__platform__PlatformConfigProvider.yaml | 0 .../effect__platform__PlatformLogger.yaml | 0 .../effect__platform__Runtime.yaml | 0 .../annotations/effect__platform__Socket.yaml | 0 .../effect__platform__SocketServer.yaml | 0 .../effect__platform__Template.yaml | 0 .../effect__platform__Terminal.yaml | 0 .../effect__platform__Transferable.yaml | 0 .../annotations/effect__platform__Url.yaml | 0 .../effect__platform__UrlParams.yaml | 2 +- .../annotations/effect__platform__Worker.yaml | 0 .../effect__platform__WorkerError.yaml | 0 .../effect__platform__WorkerRunner.yaml | 0 .../annotations/effect__platform__index.yaml | 0 .../annotations/effect__printer-ansi.yaml | 0 .../effect__printer-ansi__Ansi.yaml | 0 .../effect__printer-ansi__AnsiDoc.yaml | 0 .../effect__printer-ansi__Color.yaml | 0 .../effect__printer-ansi__index.yaml | 0 .../annotations/effect__printer.yaml | 0 .../annotations/effect__printer__Doc.yaml | 0 .../effect__printer__DocStream.yaml | 0 .../annotations/effect__printer__DocTree.yaml | 0 .../annotations/effect__printer__Flatten.yaml | 0 .../annotations/effect__printer__Layout.yaml | 0 .../effect__printer__Optimize.yaml | 0 .../effect__printer__PageWidth.yaml | 0 .../annotations/effect__printer__index.yaml | 0 .../annotations/effect__rpc.yaml | 0 .../annotations/effect__rpc__Rpc.yaml | 0 .../annotations/effect__rpc__RpcClient.yaml | 0 .../effect__rpc__RpcClientError.yaml | 0 .../annotations/effect__rpc__RpcGroup.yaml | 0 .../annotations/effect__rpc__RpcMessage.yaml | 0 .../effect__rpc__RpcMiddleware.yaml | 0 .../annotations/effect__rpc__RpcSchema.yaml | 0 .../effect__rpc__RpcSerialization.yaml | 0 .../annotations/effect__rpc__RpcServer.yaml | 0 .../annotations/effect__rpc__RpcTest.yaml | 0 .../annotations/effect__rpc__index.yaml | 0 ...ect__sql-clickhouse__ClickhouseClient.yaml | 0 ...t__sql-clickhouse__ClickhouseMigrator.yaml | 0 .../effect__sql-clickhouse__index.yaml | 0 .../annotations/effect__sql-d1__D1Client.yaml | 0 .../annotations/effect__sql-d1__index.yaml | 0 .../effect__sql-drizzle__Mysql.yaml | 0 .../annotations/effect__sql-drizzle__Pg.yaml | 0 .../effect__sql-drizzle__Sqlite.yaml | 0 .../effect__sql-kysely__Kysely.yaml | 0 .../effect__sql-kysely__Mssql.yaml | 0 .../effect__sql-kysely__Mysql.yaml | 0 .../annotations/effect__sql-kysely__Pg.yaml | 0 .../effect__sql-kysely__Sqlite.yaml | 0 .../effect__sql-kysely__patch.types.yaml | 0 .../effect__sql-libsql__index.yaml | 0 .../effect__sql-mssql__MssqlClient.yaml | 0 .../effect__sql-mssql__Parameter.yaml | 0 .../effect__sql-mssql__Procedure.yaml | 0 .../annotations/effect__sql-mssql__index.yaml | 0 .../effect__sql-mysql2__MysqlClient.yaml | 0 .../effect__sql-mysql2__index.yaml | 0 .../annotations/effect__sql-pg__PgClient.yaml | 0 .../annotations/effect__sql-pg__index.yaml | 0 .../effect__sql-sqlite-bun__SqliteClient.yaml | 0 .../effect__sql-sqlite-bun__index.yaml | 0 .../effect__sql-sqlite-do__SqliteClient.yaml | 0 .../effect__sql-sqlite-do__index.yaml | 0 ...effect__sql-sqlite-node__SqliteClient.yaml | 0 .../effect__sql-sqlite-node__index.yaml | 0 ...sql-sqlite-react-native__SqliteClient.yaml | 0 ...ffect__sql-sqlite-react-native__index.yaml | 0 ...effect__sql-sqlite-wasm__SqliteClient.yaml | 0 .../effect__sql-sqlite-wasm__index.yaml | 0 .../annotations/effect__sql.yaml | 0 .../annotations/effect__sql__Migrator.yaml | 0 .../effect__sql__Migrator__FileSystem.yaml | 0 .../annotations/effect__sql__Model.yaml | 0 .../annotations/effect__sql__SqlClient.yaml | 23 + .../effect__sql__SqlConnection.yaml | 0 .../annotations/effect__sql__SqlError.yaml | 0 .../effect__sql__SqlEventJournal.yaml | 0 .../effect__sql__SqlEventLogServer.yaml | 12 + .../effect__sql__SqlPersistedQueue.yaml | 0 .../annotations/effect__sql__SqlResolver.yaml | 0 .../annotations/effect__sql__SqlSchema.yaml | 0 .../annotations/effect__sql__Statement.yaml | 0 .../annotations/effect__sql__index.yaml | 0 .../annotations/effect__typeclass.yaml | 0 .../effect__typeclass__Alternative.yaml | 0 .../effect__typeclass__Applicative.yaml | 0 .../effect__typeclass__Bicovariant.yaml | 0 .../effect__typeclass__Bounded.yaml | 0 .../effect__typeclass__Chainable.yaml | 0 .../effect__typeclass__Contravariant.yaml | 0 .../effect__typeclass__Coproduct.yaml | 0 .../effect__typeclass__Covariant.yaml | 0 .../effect__typeclass__Filterable.yaml | 0 .../effect__typeclass__FlatMap.yaml | 0 .../effect__typeclass__Foldable.yaml | 0 .../effect__typeclass__Invariant.yaml | 0 .../annotations/effect__typeclass__Monad.yaml | 0 .../effect__typeclass__Monoid.yaml | 0 .../annotations/effect__typeclass__Of.yaml | 0 .../effect__typeclass__Pointed.yaml | 0 .../effect__typeclass__Product.yaml | 0 .../effect__typeclass__SemiAlternative.yaml | 0 .../effect__typeclass__SemiApplicative.yaml | 0 .../effect__typeclass__SemiCoproduct.yaml | 0 .../effect__typeclass__SemiProduct.yaml | 0 .../effect__typeclass__Semigroup.yaml | 0 .../effect__typeclass__Traversable.yaml | 0 ...ect__typeclass__TraversableFilterable.yaml | 0 .../effect__typeclass__data__Array.yaml | 0 .../effect__typeclass__data__BigInt.yaml | 0 .../effect__typeclass__data__Boolean.yaml | 0 .../effect__typeclass__data__Duration.yaml | 0 .../effect__typeclass__data__Effect.yaml | 0 .../effect__typeclass__data__Either.yaml | 0 .../effect__typeclass__data__Identity.yaml | 0 .../effect__typeclass__data__Micro.yaml | 0 .../effect__typeclass__data__Number.yaml | 0 .../effect__typeclass__data__Option.yaml | 0 .../effect__typeclass__data__Ordering.yaml | 0 .../effect__typeclass__data__Predicate.yaml | 0 .../effect__typeclass__data__Record.yaml | 0 .../effect__typeclass__data__String.yaml | 0 .../effect__typeclass__data__Tuple.yaml | 0 .../annotations/effect__typeclass__index.yaml | 0 .../annotations/effect__vitest__index.yaml | 7 +- .../annotations/effect__vitest__utils.yaml | 0 .../annotations/effect__workflow.yaml | 0 .../effect__workflow__Activity.yaml | 0 .../effect__workflow__DurableClock.yaml | 0 .../effect__workflow__DurableDeferred.yaml | 0 .../effect__workflow__DurableQueue.yaml | 0 .../effect__workflow__DurableRateLimiter.yaml | 0 .../effect__workflow__Workflow.yaml | 0 .../effect__workflow__WorkflowEngine.yaml | 0 .../effect__workflow__WorkflowProxy.yaml | 0 ...effect__workflow__WorkflowProxyServer.yaml | 0 .../effect/migration => migration}/cause.md | 0 .../migration => migration}/equality.md | 0 migration/error-handling.md | 173 + .../fiber-keep-alive.md | 0 .../migration => migration}/fiberref.md | 0 .../effect/migration => migration}/forking.md | 0 .../migration => migration}/generators.md | 0 .../layer-memoization.md | 45 +- .../effect/migration => migration}/runtime.md | 0 .../effect/migration => migration}/schema.md | 15 +- .../effect/migration => migration}/scope.md | 0 .../migration => migration}/services.md | 0 .../migration => migration}/v3-to-v4.md | 366 +- .../migration => migration}/yieldable.md | 0 old.patch | 0 package.json | 144 +- .../ai/anthropic/CHANGELOG.md | 20 + .../ai/anthropic/README.md | 0 .../ai/anthropic/codegen.yml | 0 .../ai/anthropic/package.json | 2 +- .../ai/anthropic/src/AnthropicClient.ts | 2 +- .../ai/anthropic/src/AnthropicConfig.ts | 0 .../ai/anthropic/src/AnthropicError.ts | 0 .../anthropic/src/AnthropicLanguageModel.ts | 39 +- .../ai/anthropic/src/AnthropicTelemetry.ts | 0 .../ai/anthropic/src/AnthropicTool.ts | 0 .../ai/anthropic/src/Generated.ts | 0 .../ai/anthropic/src/index.ts | 0 .../ai/anthropic/src/internal/errors.ts | 78 +- .../ai/anthropic/src/internal/utilities.ts | 0 .../ai/anthropic/test/AnthropicClient.test.ts | 29 + .../test/AnthropicLanguageModel.test.ts | 337 +- .../ai/anthropic/tsconfig.json | 0 .../typetest/AnthropicLanguageModel.tst.ts | 0 packages/ai/cloudflare/CHANGELOG.md | 61 + .../ai/cloudflare/README.md | 4 + .../ai/cloudflare/package.json | 2 +- .../ai/cloudflare/src/CloudflareClient.ts | 16 +- .../cloudflare/src/CloudflareDecisionModel.ts | 0 .../ai/cloudflare/src/CloudflareSchema.ts | 0 .../ai/cloudflare/src/index.ts | 0 .../ai/cloudflare/src/internal/errors.ts | 19 +- .../cloudflare/test/CloudflareClient.test.ts | 24 +- .../test/CloudflareDecisionModel.test.ts | 0 .../ai/cloudflare/tsconfig.json | 0 .../typetest/CloudflareDecisionModel.tst.ts | 0 .../ai/openai-compat/CHANGELOG.md | 22 + .../ai/openai-compat/README.md | 0 .../ai/openai-compat/package.json | 2 +- .../ai/openai-compat/src/OpenAiClient.ts | 0 .../ai/openai-compat/src/OpenAiConfig.ts | 0 .../openai-compat/src/OpenAiEmbeddingModel.ts | 6 + .../ai/openai-compat/src/OpenAiError.ts | 0 .../openai-compat/src/OpenAiLanguageModel.ts | 0 .../ai/openai-compat/src/OpenAiTelemetry.ts | 0 .../ai/openai-compat/src/index.ts | 0 .../ai/openai-compat/src/internal/errors.ts | 41 +- .../openai-compat/src/internal/utilities.ts | 0 .../openai-compat/test/OpenAiClient.test.ts | 18 +- .../test/OpenAiEmbeddingModel.test.ts | 17 +- .../test/OpenAiLanguageModel.test.ts | 17 +- .../test/OpenAiTelemetry.test.ts | 0 .../ai/openai-compat/tsconfig.json | 0 .../typetest/OpenAiConfig.tst.ts | 0 .../ai/openai/CHANGELOG.md | 32 + .../packages => packages}/ai/openai/README.md | 0 .../ai/openai/codegen.yaml | 0 .../ai/openai/package.json | 2 +- .../ai/openai/src/Generated.ts | 0 .../ai/openai/src/OpenAiClient.ts | 0 .../ai/openai/src/OpenAiClientGenerated.ts | 0 .../ai/openai/src/OpenAiConfig.ts | 0 .../ai/openai/src/OpenAiDecisionModel.ts | 23 +- .../ai/openai/src/OpenAiEmbeddingModel.ts | 6 + .../ai/openai/src/OpenAiError.ts | 21 +- .../ai/openai/src/OpenAiLanguageModel.ts | 25 +- .../ai/openai/src/OpenAiSchema.ts | 11 +- .../ai/openai/src/OpenAiTelemetry.ts | 0 .../ai/openai/src/OpenAiTool.ts | 0 .../ai/openai/src/index.ts | 0 .../ai/openai/src/internal/errors.ts | 41 +- .../ai/openai/src/internal/utilities.ts | 0 .../ai/openai/test/OpenAiClient.test.ts | 77 +- .../openai/test/OpenAiDecisionModel.test.ts | 29 +- .../openai/test/OpenAiEmbeddingModel.test.ts | 58 +- .../openai/test/OpenAiLanguageModel.test.ts | 65 +- .../ai/openai/test/OpenAiSchema.test.ts | 12 + .../ai/openai/test/OpenAiTelemetry.test.ts | 0 .../ai/openai/tsconfig.json | 0 .../typetest/OpenAiDecisionModel.tst.ts | 0 .../ai/openai/typetest/OpenAiTool.tst.ts | 0 .../ai/openrouter/CHANGELOG.md | 32 + .../ai/openrouter/README.md | 0 .../ai/openrouter/codegen.yml | 0 .../ai/openrouter/package.json | 2 +- .../ai/openrouter/src/Generated.ts | 0 .../ai/openrouter/src/OpenRouterClient.ts | 28 +- .../ai/openrouter/src/OpenRouterConfig.ts | 0 .../openrouter/src/OpenRouterDecisionModel.ts | 57 +- .../ai/openrouter/src/OpenRouterError.ts | 0 .../openrouter/src/OpenRouterLanguageModel.ts | 59 +- .../ai/openrouter/src/OpenRouterSchema.ts | 0 .../ai/openrouter/src/index.ts | 0 .../ai/openrouter/src/internal/errors.ts | 80 +- .../ai/openrouter/src/internal/utilities.ts | 0 .../ai/openrouter/test/Generated.test.ts | 0 .../openrouter/test/OpenRouterClient.test.ts | 1 + .../test/OpenRouterDecisionModel.test.ts | 56 +- .../test/OpenRouterLanguageModel.test.ts | 161 +- .../openrouter/test/OpenRouterSchema.test.ts | 0 .../ai/openrouter/tsconfig.json | 0 .../typetest/OpenRouterDecisionModel.tst.ts | 0 .../ai/typesafe/CHANGELOG.md | 18 + .../ai/typesafe/README.md | 0 .../ai/typesafe/package.json | 2 +- .../ai/typesafe/src/TypeSafeClient.ts | 0 .../ai/typesafe/src/TypeSafeConfig.ts | 0 .../ai/typesafe/src/TypeSafeDecisionModel.ts | 0 .../ai/typesafe/src/TypeSafeSchema.ts | 0 .../ai/typesafe/src/index.ts | 0 .../ai/typesafe/src/internal/errors.ts | 19 +- .../ai/typesafe/test/TypeSafeClient.test.ts | 43 +- .../test/TypeSafeDecisionModel.test.ts | 34 +- .../ai/typesafe/test/TypeSafeSchema.test.ts | 0 .../ai/typesafe/tsconfig.json | 0 .../typetest/TypeSafeDecisionModel.tst.ts | 0 .../atom/react/CHANGELOG.md | 8 + .../packages => packages}/atom/react/LICENSE | 0 .../atom/react/README.md | 0 .../atom/react/package.json | 6 +- .../atom/react/src/Hooks.ts | 14 + .../atom/react/src/ReactHydration.ts | 0 .../atom/react/src/RegistryContext.ts | 2 +- .../atom/react/src/ScopedAtom.ts | 0 .../atom/react/src/index.ts | 0 .../atom/react/test/index.test.tsx | 44 +- .../atom/react/tsconfig.json | 0 .../atom/react/vitest.setup.ts | 0 .../atom/solid/CHANGELOG.md | 8 + .../packages => packages}/atom/solid/LICENSE | 0 .../atom/solid/README.md | 0 .../atom/solid/package.json | 2 +- .../atom/solid/src/Hooks.ts | 14 + .../atom/solid/src/RegistryContext.ts | 2 +- .../atom/solid/src/index.ts | 0 .../atom/solid/test/index.test.tsx | 0 .../atom/solid/tsconfig.json | 0 .../atom/solid/vitest.setup.ts | 0 .../atom/vue/CHANGELOG.md | 7 + .../packages => packages}/atom/vue/LICENSE | 0 .../packages => packages}/atom/vue/README.md | 0 .../atom/vue/package.json | 4 +- .../atom/vue/src/index.ts | 14 + .../atom/vue/test/index.test.ts | 0 .../atom/vue/tsconfig.json | 0 packages/cli/.gitignore | 0 packages/cli/.madgerc | 7 - packages/cli/.npmignore | 14 - packages/cli/CHANGELOG.md | 1793 -- packages/cli/bin.js | 2 - packages/cli/package.json | 158 - packages/cli/src/argv-patch.ts | 16 - packages/cli/src/extract.ts | 75 - packages/cli/src/gist.ts | 748 - packages/cli/src/index.ts | 904 - packages/cli/src/os-command.ts | 54 - packages/cli/src/shared.ts | 3 - packages/cli/src/sync-effect-app-subtree.ts | 307 - packages/cli/src/sync-effect-subtree.ts | 140 - packages/cli/src/sync-shared.ts | 349 - packages/cli/src/update-packages.ts | 4 - packages/cli/test.gists.yaml | 27 - packages/cli/test/argv-patch.test.ts | 198 - packages/cli/test/gist-cache.test.ts | 92 - packages/cli/test/update-packages.test.ts | 13 - packages/cli/tsconfig.json | 17 - packages/cli/vitest.config.ts | 5 - packages/e2e/CHANGELOG.md | 891 - packages/e2e/package.json | 34 - .../e2e/test/repoInvalidation.e2e.test.ts | 270 - packages/e2e/tsconfig.json | 28 - packages/e2e/vitest.config.ts | 13 - packages/effect-app/.madgerc | 7 - packages/effect-app/.npmignore | 14 - packages/effect-app/CHANGELOG.md | 9773 ------- packages/effect-app/package.json | 97 - packages/effect-app/src/Array.ts | 78 - packages/effect-app/src/Chunk.ts | 32 - packages/effect-app/src/Config.ts | 7 - packages/effect-app/src/Config/SecretURL.ts | 80 - .../src/Config/internal/configSecretURL.ts | 68 - packages/effect-app/src/ConfigProvider.ts | 5 - packages/effect-app/src/Context.ts | 181 - packages/effect-app/src/DataDependencies.ts | 171 - packages/effect-app/src/DateTime.ts | 473 - packages/effect-app/src/Effect.ts | 183 - packages/effect-app/src/Emailer.ts | 51 - packages/effect-app/src/Function.ts | 168 - packages/effect-app/src/Inputify.type.ts | 12 - packages/effect-app/src/Layer.ts | 69 - packages/effect-app/src/Model.ts | 5 - packages/effect-app/src/Model/Repository.ts | 7 - .../src/Model/Repository/Registry.ts | 35 - .../src/Model/Repository/dependency.ts | 32 - .../effect-app/src/Model/Repository/ext.ts | 374 - .../src/Model/Repository/internal/internal.ts | 987 - .../effect-app/src/Model/Repository/legacy.ts | 29 - .../src/Model/Repository/makeRepo.ts | 168 - .../src/Model/Repository/service.ts | 676 - .../src/Model/Repository/validation.ts | 38 - packages/effect-app/src/Model/dsl.ts | 130 - .../effect-app/src/Model/filter/filterApi.ts | 78 - packages/effect-app/src/Model/filter/types.ts | 6 - .../src/Model/filter/types/errors.ts | 47 - .../src/Model/filter/types/fields.ts | 50 - .../src/Model/filter/types/path/common.ts | 404 - .../src/Model/filter/types/path/eager.ts | 329 - .../src/Model/filter/types/path/index.ts | 4 - .../src/Model/filter/types/utils.ts | 128 - .../src/Model/filter/types/validator.ts | 46 - packages/effect-app/src/Model/query.ts | 13 - packages/effect-app/src/Model/query/dsl.ts | 3092 --- .../src/Model/query/new-kid-interpreter.ts | 531 - packages/effect-app/src/NonEmptySet.ts | 92 - packages/effect-app/src/Option.ts | 63 - packages/effect-app/src/Pure.ts | 450 - packages/effect-app/src/QueueMaker.ts | 11 - packages/effect-app/src/RequestContext.ts | 48 - .../src/RequestScopedDependencies.ts | 45 - packages/effect-app/src/Schema.ts | 523 - packages/effect-app/src/Schema/Class.ts | 683 - packages/effect-app/src/Schema/FastCheck.ts | 23 - packages/effect-app/src/Schema/Methods.ts | 8 - .../effect-app/src/Schema/SchemaParser.ts | 12 - .../src/Schema/SpecialJsonSchema.ts | 137 - .../effect-app/src/Schema/SpecialOpenApi.ts | 130 - packages/effect-app/src/Schema/brand.ts | 71 - packages/effect-app/src/Schema/email.ts | 32 - packages/effect-app/src/Schema/ext.ts | 681 - packages/effect-app/src/Schema/moreStrings.ts | 390 - packages/effect-app/src/Schema/numbers.ts | 162 - packages/effect-app/src/Schema/phoneNumber.ts | 30 - packages/effect-app/src/Schema/schema.ts | 5 - packages/effect-app/src/Schema/strings.ts | 69 - packages/effect-app/src/SchemaAST.ts | 1 - packages/effect-app/src/Set.ts | 666 - packages/effect-app/src/Store.ts | 325 - packages/effect-app/src/TypeTest.ts | 7 - packages/effect-app/src/Types.ts | 16 - packages/effect-app/src/Widen.type.ts | 26 - packages/effect-app/src/_ext/Array.ts | 36 - packages/effect-app/src/_ext/ord.ext.ts | 40 - packages/effect-app/src/builtin-json.d.ts | 24 - packages/effect-app/src/builtin.ts | 53 - packages/effect-app/src/client.ts | 8 - .../effect-app/src/client/InvalidationKeys.ts | 55 - .../effect-app/src/client/apiClientFactory.ts | 465 - packages/effect-app/src/client/clientFor.ts | 186 - packages/effect-app/src/client/errors.ts | 310 - packages/effect-app/src/client/makeClient.ts | 629 - packages/effect-app/src/faker.ts | 32 - packages/effect-app/src/http.ts | 1 - packages/effect-app/src/http/Request.ts | 42 - packages/effect-app/src/http/internal/lib.ts | 13 - packages/effect-app/src/ids.ts | 66 - packages/effect-app/src/index.ts | 27 - packages/effect-app/src/logger.ts | 3 - packages/effect-app/src/middleware.ts | 28 - .../effect-app/src/migration/nativeEffect.ts | 645 - packages/effect-app/src/rpc.ts | 4 - packages/effect-app/src/rpc/Invalidation.ts | 278 - .../effect-app/src/rpc/MiddlewareMaker.ts | 492 - packages/effect-app/src/rpc/README.md | 41 - packages/effect-app/src/rpc/RpcContextMap.ts | 156 - packages/effect-app/src/rpc/RpcMiddleware.ts | 244 - packages/effect-app/src/runtime.ts | 57 - packages/effect-app/src/setupRequest.ts | 138 - packages/effect-app/src/toast.ts | 54 - packages/effect-app/src/transform.ts | 80 - packages/effect-app/src/utils.ts | 884 - packages/effect-app/src/utils/effectify.ts | 260 - packages/effect-app/src/utils/extend.ts | 28 - packages/effect-app/src/utils/gen.ts | 24 - packages/effect-app/src/utils/logLevel.ts | 17 - packages/effect-app/src/utils/logger.ts | 36 - packages/effect-app/src/validation.ts | 18 - .../effect-app/src/validation/validators.ts | 31 - packages/effect-app/src/waitingGate.ts | 82 - packages/effect-app/src/withToast.ts | 179 - .../effect-app/test/dateTimeLocal.test.ts | 55 - packages/effect-app/test/moreStrings.test.ts | 62 - .../test/nativeEffectMigration.test.ts | 77 - packages/effect-app/test/rpc.test.ts | 62 - .../test/rpcMiddlewareContext.test.ts | 63 - packages/effect-app/test/schema.test.ts | 681 - packages/effect-app/test/secretURL.test.ts | 160 - packages/effect-app/test/special.test.ts | 1263 - packages/effect-app/test/utils.test.ts | 209 - packages/effect-app/test/waitingGate.test.ts | 64 - packages/effect-app/tsconfig.base.json | 61 - packages/effect-app/tsconfig.json | 12 - packages/effect-app/tsconfig.src.json | 31 - packages/effect-app/tsconfig.test.json | 33 - packages/effect-app/vitest.config.ts | 5 - .../packages => packages}/effect/ARBITRARY.md | 0 .../packages => packages}/effect/CHANGELOG.md | 594 + .../packages => packages}/effect/CONFIG.md | 0 .../packages => packages}/effect/HTTPAPI.md | 458 +- .../packages => packages}/effect/LICENSE | 0 .../packages => packages}/effect/MCP.md | 209 +- .../packages => packages}/effect/OPTIC.md | 0 .../packages => packages}/effect/README.md | 0 .../packages => packages}/effect/SCHEMA.md | 73 +- .../effect/benchmark/Pool.ts | 0 packages/effect/benchmark/encoding/Xml.ts | 115 + .../effect/benchmark/eventlog/Bandwidth.md | 18 + .../effect/benchmark/eventlog/Bandwidth.ts | 223 + .../effect/benchmark/http/multipart.ts | 0 .../benchmark/http/serverAllocations.ts | 0 .../effect/benchmark/http/tracer.ts | 0 .../benchmark/httpapi/sseClientDecoder.ts | 0 .../effect/benchmark/net/NetAddress.ts | 0 .../effect/benchmark/rpc/RpcSerialization.ts | 0 .../effect/benchmark/schema/Optic.ts | 0 .../effect/benchmark/schema/SchemaBinary.md | 0 .../effect/benchmark/schema/SchemaBinary.ts | 0 .../effect/benchmark/schema/SchemaError.ts | 0 .../effect/benchmark/stream/splitLines.ts | 0 .../packages => packages}/effect/package.json | 25 +- .../effect/runtimeperf/README.md | 0 .../effect/runtimeperf/compare.mts | 0 .../effect/runtimeperf/config.json | 0 .../effect/runtimeperf/materialize.mts | 0 .../effect/runtimeperf/run.mts | 0 .../effect/runtimeperf/stats.mts | 0 .../arbitrary/fixtures/fast-check-v4.ts | 0 .../suites/arbitrary/fixtures/native.ts | 0 .../suites/arbitrary/fixtures/schema.ts | 0 .../suites/compiler-rebuild/README.md | 0 .../suites/compiler-rebuild/costs.mts | 0 .../suites/compiler-rebuild/fixtures/aot.ts | 0 .../suites/compiler-rebuild/fixtures/cases.ts | 0 .../compiler-rebuild/fixtures/generate.mts | 0 .../compiler-rebuild/fixtures/interpreted.ts | 0 .../suites/compiler-rebuild/fixtures/jit.ts | 0 .../compiler-rebuild/fixtures/valibot.ts | 0 .../compiler-rebuild/fixtures/zod-cases.ts | 0 .../compiler-rebuild/fixtures/zod-compiled.ts | 0 .../compiler-rebuild/fixtures/zod-jitless.ts | 0 .../compiler-rebuild/report-resources.mts | 0 .../suites/compiler-rebuild/resources.mts | 0 .../suites/compiler-rebuild/run-resources.mts | 0 .../runtimeperf/suites/moltar/fixtures/aot.ts | 0 .../suites/moltar/fixtures/cases.ts | 0 .../suites/moltar/fixtures/data.ts | 0 .../suites/moltar/fixtures/generate.mts | 0 .../suites/moltar/fixtures/interpreted.ts | 0 .../runtimeperf/suites/moltar/fixtures/jit.ts | 0 .../suites/moltar/fixtures/valibot.ts | 0 .../runtimeperf/suites/moltar/fixtures/zod.ts | 0 .../suites/schema-benchmarks/fixtures/data.ts | 0 .../schema-benchmarks/fixtures/effect-beta.ts | 0 .../schema-benchmarks/fixtures/valibot.ts | 0 .../suites/schema-benchmarks/fixtures/zod.ts | 0 .../suites/schema-checked-sources/README.md | 0 .../schema-checked-sources/fixtures/cases.ts | 0 .../suites/schema-construction/AST-LAYOUT.md | 0 .../suites/schema-construction/README.md | 0 .../suites/schema-construction/RESULTS.md | 0 .../suites/schema-construction/TO-TYPE.md | 0 .../suites/schema-construction/fixtures.mts | 0 .../suites/schema-construction/profile.mts | 0 .../suites/schema-construction/run.mts | 0 .../suites/schema-construction/worker.mts | 0 .../fixtures/cases.ts | 0 .../suites/schema/fixtures/adapters.ts | 0 .../suites/schema/fixtures/behavior.ts | 0 .../suites/schema/fixtures/cold.ts | 0 .../suites/schema/fixtures/comparison.ts | 0 .../runtimeperf/test/materialize.test.mts | 0 .../effect/runtimeperf/test/registry.test.mts | 0 .../test/schema-construction.test.mts | 0 .../effect/runtimeperf/test/stats.test.mts | 0 .../effect/runtimeperf/test/worker.test.mts | 0 .../runtimeperf/test/worktree-diff.test.mts | 0 .../effect/runtimeperf/utils.mts | 0 .../effect/runtimeperf/worker.mts | 0 .../effect/runtimeperf/worktree-diff.mts | 0 .../effect/src/Arbitrary.ts | 0 .../packages => packages}/effect/src/Array.ts | 63 +- .../effect/src/BigDecimal.ts | 0 .../effect/src/BigInt.ts | 0 .../effect/src/Boolean.ts | 0 .../packages => packages}/effect/src/Brand.ts | 0 .../effect/src/ByteSize.ts | 0 packages/effect/src/Bytes.ts | 111 + .../packages => packages}/effect/src/Cache.ts | 0 .../packages => packages}/effect/src/Cause.ts | 0 .../effect/src/Channel.ts | 201 +- .../effect/src/ChannelSchema.ts | 0 .../packages => packages}/effect/src/Chunk.ts | 47 +- .../packages => packages}/effect/src/Clock.ts | 0 .../effect/src/Combiner.ts | 0 .../effect/src/Config.ts | 11 +- .../effect/src/ConfigProvider.ts | 3 +- .../effect/src/Console.ts | 0 .../effect/src/Context.ts | 0 .../packages => packages}/effect/src/Cron.ts | 9 +- .../effect/src/Crypto.ts | 0 .../packages => packages}/effect/src/Data.ts | 0 .../effect/src/DateTime.ts | 0 .../effect/src/Deferred.ts | 0 .../effect/src/Differ.ts | 0 .../effect/src/Duration.ts | 82 +- .../effect/src/Effect.ts | 245 +- .../effect/src/Effectable.ts | 0 .../packages => packages}/effect/src/Equal.ts | 0 .../effect/src/Equivalence.ts | 11 +- .../effect/src/ErrorReporter.ts | 0 .../effect/src/ExecutionPlan.ts | 0 .../packages => packages}/effect/src/Exit.ts | 11 +- .../packages => packages}/effect/src/Fiber.ts | 0 .../effect/src/FiberHandle.ts | 19 +- .../effect/src/FiberMap.ts | 8 +- .../effect/src/FiberSet.ts | 0 .../effect/src/FileSystem.ts | 10 + .../effect/src/Filter.ts | 0 .../effect/src/Formatter.ts | 0 .../effect/src/Function.ts | 0 .../packages => packages}/effect/src/Graph.ts | 0 .../packages => packages}/effect/src/HKT.ts | 0 .../packages => packages}/effect/src/Hash.ts | 0 .../effect/src/HashMap.ts | 0 .../effect/src/HashRing.ts | 0 .../effect/src/HashSet.ts | 0 .../effect/src/Inspectable.ts | 0 .../effect/src/Iterable.ts | 29 +- .../effect/src/JsonPatch.ts | 0 .../effect/src/JsonPointer.ts | 0 .../effect/src/JsonSchema.ts | 31 +- .../packages => packages}/effect/src/Latch.ts | 0 .../packages => packages}/effect/src/Layer.ts | 4 +- .../effect/src/LayerMap.ts | 0 .../effect/src/LayerRef.ts | 0 .../effect/src/LogLevel.ts | 0 .../effect/src/Logger.ts | 0 .../effect/src/ManagedRuntime.ts | 78 +- .../packages => packages}/effect/src/Match.ts | 0 .../effect/src/Metric.ts | 266 +- .../effect/src/MutableHashMap.ts | 0 .../effect/src/MutableHashSet.ts | 0 .../effect/src/MutableList.ts | 0 .../effect/src/MutableRef.ts | 0 .../effect/src/Newtype.ts | 0 .../effect/src/NonEmptyIterable.ts | 0 .../effect/src/Number.ts | 4 +- .../packages => packages}/effect/src/Optic.ts | 0 .../effect/src/Option.ts | 0 .../packages => packages}/effect/src/Order.ts | 0 .../effect/src/Ordering.ts | 0 .../effect/src/PartitionedSemaphore.ts | 0 .../packages => packages}/effect/src/Path.ts | 10 +- .../effect/src/Pipeable.ts | 0 .../effect/src/PlatformError.ts | 0 .../packages => packages}/effect/src/Pool.ts | 0 .../effect/src/Predicate.ts | 0 .../effect/src/PrimaryKey.ts | 0 .../effect/src/PubSub.ts | 19 +- .../packages => packages}/effect/src/Pull.ts | 0 .../packages => packages}/effect/src/Queue.ts | 5 +- .../effect/src/Random.ts | 2 +- .../packages => packages}/effect/src/RcMap.ts | 16 +- .../packages => packages}/effect/src/RcRef.ts | 0 .../effect/src/Record.ts | 17 + .../effect/src/Redactable.ts | 0 .../effect/src/Redacted.ts | 0 .../effect/src/Reducer.ts | 0 .../packages => packages}/effect/src/Ref.ts | 0 .../effect/src/References.ts | 9 +- .../effect/src/RegExp.ts | 0 .../effect/src/Request.ts | 0 .../effect/src/RequestResolver.ts | 47 +- .../effect/src/Resource.ts | 2 +- .../effect/src/Result.ts | 0 .../effect/src/Runtime.ts | 37 +- .../effect/src/Schedule.ts | 0 .../effect/src/Scheduler.ts | 0 .../effect/src/Schema.ts | 548 +- .../effect/src/SchemaAST.ts | 29 +- .../effect/src/SchemaGetter.ts | 27 +- .../effect/src/SchemaIssue.ts | 97 +- .../effect/src/SchemaParser.ts | 0 .../effect/src/SchemaRepresentation.ts | 65 +- .../effect/src/SchemaTransformation.ts | 2 +- .../packages => packages}/effect/src/Scope.ts | 14 +- .../effect/src/ScopedCache.ts | 23 +- .../effect/src/ScopedRef.ts | 0 packages/effect/src/SemVer.ts | 1042 + packages/effect/src/SemVerRange.ts | 703 + .../effect/src/Semaphore.ts | 0 .../packages => packages}/effect/src/Sink.ts | 8 +- .../effect/src/StandardSchema.ts | 0 .../packages => packages}/effect/src/Stdio.ts | 0 .../effect/src/Stream.ts | 246 +- .../effect/src/String.ts | 16 +- .../effect/src/Struct.ts | 0 .../effect/src/SubscriptionRef.ts | 0 .../effect/src/Symbol.ts | 0 .../effect/src/SynchronizedRef.ts | 0 .../packages => packages}/effect/src/Take.ts | 0 .../effect/src/Terminal.ts | 0 packages/effect/src/TextDiff.ts | 766 + .../effect/src/Tracer.ts | 0 .../packages => packages}/effect/src/Trie.ts | 0 .../packages => packages}/effect/src/Tuple.ts | 0 .../effect/src/TxChunk.ts | 0 .../effect/src/TxDeferred.ts | 0 .../effect/src/TxHashMap.ts | 0 .../effect/src/TxHashSet.ts | 0 .../effect/src/TxPriorityQueue.ts | 0 .../effect/src/TxPubSub.ts | 0 .../effect/src/TxQueue.ts | 0 .../effect/src/TxReentrantLock.ts | 0 .../packages => packages}/effect/src/TxRef.ts | 0 .../effect/src/TxSemaphore.ts | 0 .../effect/src/TxSubscriptionRef.ts | 0 .../packages => packages}/effect/src/Types.ts | 0 .../effect/src/UndefinedOr.ts | 0 .../packages => packages}/effect/src/Unify.ts | 0 .../packages => packages}/effect/src/Utils.ts | 0 .../effect/src/Version.ts | 0 .../effect/src/ai/AiError.ts | 70 +- .../src/ai/AnthropicStructuredOutput.ts | 0 .../effect/src/ai/Chat.ts | 15 +- .../effect/src/ai/Decision.ts | 50 +- packages/effect/src/ai/DecisionModel.ts | 617 + .../effect/src/ai/EmbeddingModel.ts | 38 +- .../effect/src/ai/IdGenerator.ts | 0 .../effect/src/ai/LanguageModel.ts | 0 packages/effect/src/ai/McpClient.ts | 1211 + packages/effect/src/ai/McpClientError.ts | 329 + .../effect/src/ai/McpProtocol.ts | 4 +- .../effect/src/ai/McpSchema.ts | 188 +- .../effect/src/ai/McpServer.ts | 437 +- .../effect/src/ai/Model.ts | 0 .../effect/src/ai/OpenAiStructuredOutput.ts | 0 .../effect/src/ai/Prompt.ts | 76 +- .../effect/src/ai/Response.ts | 142 +- .../effect/src/ai/ResponseIdTracker.ts | 0 .../effect/src/ai/Telemetry.ts | 0 .../effect/src/ai/Tokenizer.ts | 0 .../effect/src/ai/Tool.ts | 16 +- .../effect/src/ai/Toolkit.ts | 88 +- .../effect/src/ai/index.ts | 12 + .../src/ai/internal/codec-transformer.ts | 0 .../effect/src/ai/internal/mcpCore.ts | 27 +- .../effect/src/ai/internal/mcpProtocol.ts | 24 +- .../ai/internal/mcpProtocol/v2024_11_05.ts | 2 +- .../ai/internal/mcpProtocol/v2025_03_26.ts | 2 +- .../ai/internal/mcpProtocol/v2025_06_18.ts | 6 +- .../ai/internal/mcpProtocol/v2025_11_25.ts | 6 +- .../ai/internal/mcpProtocol/v2026_07_28.ts | 26 +- .../src/ai/internal/mcpProtocolRegistry.ts | 0 .../effect/src/ai/internal/mcpRuntime.ts | 7 +- .../src/ai/internal/mcpSchema/v2024_11_05.ts | 33 +- .../src/ai/internal/mcpSchema/v2025_03_26.ts | 6 +- .../src/ai/internal/mcpSchema/v2025_06_18.ts | 14 +- .../src/ai/internal/mcpSchema/v2025_11_25.ts | 16 +- .../src/ai/internal/mcpSchema/v2026_07_28.ts | 86 +- .../src/ai/internal/mcpStatefulRuntime.ts | 0 .../src/ai/internal/structured-output.ts | 0 .../effect/src/cli/Argument.ts | 0 .../effect/src/cli/CliConfig.ts | 0 .../effect/src/cli/CliError.ts | 53 +- .../effect/src/cli/CliOutput.ts | 0 .../effect/src/cli/Command.ts | 143 +- .../effect/src/cli/Completions.ts | 5 + .../effect/src/cli/Flag.ts | 61 + .../effect/src/cli/GlobalFlag.ts | 6 +- .../effect/src/cli/HelpDoc.ts | 0 .../effect/src/cli/Param.ts | 74 + .../effect/src/cli/Primitive.ts | 0 .../effect/src/cli/Prompt.ts | 0 .../effect/src/cli/TerminalCapabilities.ts | 156 + .../effect/src/cli/index.ts | 6 + .../effect/src/cli/internal/ansi.ts | 0 .../effect/src/cli/internal/auto-suggest.ts | 0 .../effect/src/cli/internal/command.ts | 11 +- .../src/cli/internal/completions/bash.ts | 7 +- .../cli/internal/completions/descriptor.ts | 12 +- .../src/cli/internal/completions/fish.ts | 18 +- .../cli/internal/completions/powershell.ts | 37 +- .../src/cli/internal/completions/zsh.ts | 13 +- .../effect/src/cli/internal/config.ts | 0 packages/effect/src/cli/internal/formatter.ts | 18 + .../effect/src/cli/internal/help.ts | 0 .../effect/src/cli/internal/lexer.ts | 17 +- .../effect/src/cli/internal/parser.ts | 42 +- .../effect/src/cli/internal/wizard.ts | 0 packages/effect/src/cluster/ClusterCron.ts | 225 + .../effect/src/cluster/ClusterError.ts | 0 packages/effect/src/cluster/ClusterMetrics.ts | 260 + .../effect/src/cluster/ClusterSchema.ts | 0 .../src/cluster/ClusterWorkflowEngine.ts | 125 +- .../effect/src/cluster/DeliverAt.ts | 0 .../effect/src/cluster/Entity.ts | 4 + .../effect/src/cluster/EntityAddress.ts | 0 .../effect/src/cluster/EntityId.ts | 0 .../effect/src/cluster/EntityProxy.ts | 0 .../effect/src/cluster/EntityProxyServer.ts | 0 .../effect/src/cluster/EntityResource.ts | 0 .../effect/src/cluster/EntityType.ts | 0 .../effect/src/cluster/Envelope.ts | 0 .../effect/src/cluster/HttpRunner.ts | 0 .../effect/src/cluster/K8sHttpClient.ts | 0 .../effect/src/cluster/K8sTypes.ts | 0 .../effect/src/cluster/MachineId.ts | 0 .../effect/src/cluster/Message.ts | 0 .../effect/src/cluster/MessageStorage.ts | 7 + .../effect/src/cluster/Reply.ts | 0 .../effect/src/cluster/Runner.ts | 0 .../effect/src/cluster/RunnerAddress.ts | 0 .../effect/src/cluster/RunnerHealth.ts | 0 .../effect/src/cluster/RunnerServer.ts | 0 .../effect/src/cluster/RunnerStorage.ts | 0 .../effect/src/cluster/Runners.ts | 101 +- .../effect/src/cluster/ShardId.ts | 0 .../effect/src/cluster/Sharding.ts | 156 +- .../effect/src/cluster/ShardingConfig.ts | 15 + .../src/cluster/ShardingRegistrationEvent.ts | 0 .../effect/src/cluster/SingleRunner.ts | 0 .../effect/src/cluster/Singleton.ts | 0 .../effect/src/cluster/SingletonAddress.ts | 0 .../effect/src/cluster/Snowflake.ts | 0 .../effect/src/cluster/SocketRunner.ts | 0 .../effect/src/cluster/SqlMessageStorage.ts | 113 +- .../effect/src/cluster/SqlRunnerStorage.ts | 14 +- .../effect/src/cluster/TestRunner.ts | 0 .../effect/src/cluster/index.ts | 0 .../src/cluster/internal/clusterAbandon.ts | 0 .../src/cluster/internal/entityActivation.ts | 0 .../src/cluster/internal/entityManager.ts | 90 +- .../src/cluster/internal/entityReaper.ts | 0 .../effect/src/cluster/internal/hash.ts | 0 .../src/cluster/internal/interruptors.ts | 0 .../src/cluster/internal/resourceMap.ts | 0 .../src/cluster/internal/resourceRef.ts | 0 .../effect/src/cluster/internal/shardLock.ts | 0 .../effect/src/devtools/DevTools.ts | 5 +- .../effect/src/devtools/DevToolsClient.ts | 37 +- .../effect/src/devtools/DevToolsSchema.ts | 0 .../effect/src/devtools/DevToolsServer.ts | 0 .../effect/src/devtools/index.ts | 0 .../effect/src/encoding/Base64.ts | 0 .../effect/src/encoding/Base64Url.ts | 31 + .../effect/src/encoding/EncodingError.ts | 0 .../effect/src/encoding/Hex.ts | 0 .../effect/src/encoding/Ini.ts | 3 + packages/effect/src/encoding/JsonArray.ts | 849 + .../effect/src/encoding/Ndjson.ts | 231 +- .../effect/src/encoding/SchemaBinary.ts | 34 +- .../effect/src/encoding/Sse.ts | 0 packages/effect/src/encoding/Tar.ts | 1417 + .../effect/src/encoding/Toml.ts | 6 +- packages/effect/src/encoding/Xml.ts | 2037 ++ .../effect/src/encoding/Yaml.ts | 144 +- packages/effect/src/encoding/Zip.ts | 1558 ++ .../effect/src/encoding/index.ts | 24 + .../effect/src/eventlog/Event.ts | 0 .../effect/src/eventlog/EventGroup.ts | 0 packages/effect/src/eventlog/EventJournal.ts | 1223 + .../effect/src/eventlog/EventLog.ts | 692 +- .../effect/src/eventlog/EventLogEncryption.ts | 260 + .../effect/src/eventlog/EventLogMessage.ts | 73 +- .../effect/src/eventlog/EventLogRemote.ts | 98 +- .../effect/src/eventlog/EventLogServer.ts | 182 +- .../src/eventlog/EventLogServerEncrypted.ts | 201 +- .../src/eventlog/EventLogServerUnencrypted.ts | 257 +- .../src/eventlog/EventLogSessionAuth.ts | 0 packages/effect/src/eventlog/EventRow.ts | 706 + .../effect/src/eventlog/SqlEventJournal.ts | 677 + .../eventlog/SqlEventLogServerEncrypted.ts | 476 + .../eventlog/SqlEventLogServerUnencrypted.ts | 203 +- .../effect/src/eventlog/index.ts | 6 + .../effect/src/eventlog/internal/conflicts.ts | 123 + .../internal/identityRootSecretDerivation.ts | 0 .../effect/src/graphql/GraphQL.ts | 0 .../effect/src/graphql/GraphQLClient.ts | 0 .../effect/src/graphql/GraphQLClientError.ts | 17 +- .../effect/src/graphql/GraphQLGroup.ts | 0 .../effect/src/graphql/GraphQLMiddleware.ts | 0 .../effect/src/graphql/GraphQLProtocol.ts | 7 +- .../effect/src/graphql/GraphQLResolver.ts | 121 +- .../effect/src/graphql/index.ts | 0 .../effect/src/http-api/HttpApi.ts | 2 +- .../effect/src/http-api/HttpApiBuilder.ts | 174 +- .../effect/src/http-api/HttpApiClient.ts | 188 +- .../effect/src/http-api/HttpApiEndpoint.ts | 177 +- .../effect/src/http-api/HttpApiError.ts | 55 + .../effect/src/http-api/HttpApiGroup.ts | 0 .../effect/src/http-api/HttpApiMiddleware.ts | 0 .../effect/src/http-api/HttpApiScalar.ts | 7 +- .../effect/src/http-api/HttpApiSchema.ts | 280 +- .../effect/src/http-api/HttpApiSecurity.ts | 0 .../effect/src/http-api/HttpApiSwagger.ts | 15 +- .../effect/src/http-api/HttpApiTest.ts | 42 +- .../effect/src/http-api/OpenApi.ts | 126 +- .../effect/src/http-api/index.ts | 0 .../effect/src/http-api/internal/html.ts | 0 .../src/http-api/internal/httpApiScalar.ts | 0 .../src/http-api/internal/httpApiSwagger.ts | 0 .../effect/src/http-api/internal/mediaType.ts | 0 packages/effect/src/http-api/internal/path.ts | 89 + .../effect/src/http/Cookies.ts | 0 .../effect/src/http/Etag.ts | 0 .../effect/src/http/FetchHttpClient.ts | 62 +- .../effect/src/http/FindMyWay.ts | 0 .../http/FindMyWay/internal/queryString.ts | 0 .../src/http/FindMyWay/internal/router.ts | 11 + .../effect/src/http/Headers.ts | 337 + .../effect/src/http/HttpBody.ts | 0 .../effect/src/http/HttpClient.ts | 77 +- .../effect/src/http/HttpClientError.ts | 0 .../effect/src/http/HttpClientRequest.ts | 62 +- .../effect/src/http/HttpClientResponse.ts | 71 + .../effect/src/http/HttpEffect.ts | 40 +- .../effect/src/http/HttpIncomingMessage.ts | 26 +- .../effect/src/http/HttpMethod.ts | 0 .../effect/src/http/HttpMiddleware.ts | 203 +- .../effect/src/http/HttpPlatform.ts | 0 .../effect/src/http/HttpRouter.ts | 10 +- .../effect/src/http/HttpServer.ts | 0 .../effect/src/http/HttpServerError.ts | 9 +- .../effect/src/http/HttpServerRequest.ts | 83 +- .../effect/src/http/HttpServerRespondable.ts | 0 .../effect/src/http/HttpServerResponse.ts | 72 +- .../effect/src/http/HttpStaticServer.ts | 30 +- .../effect/src/http/HttpStatus.ts | 0 .../effect/src/http/HttpTraceContext.ts | 0 .../effect/src/http/Mime.ts | 0 .../effect/src/http/Multipart.ts | 0 .../effect/src/http/MultipartParser.ts | 0 .../src/http/MultipartParser/HeadersParser.ts | 0 .../effect/src/http/MultipartParser/Search.ts | 0 .../MultipartParser/internal/contentType.ts | 204 + .../http/MultipartParser/internal/headers.ts | 20 +- .../MultipartParser/internal/multipart.ts | 42 +- .../http/MultipartParser/internal/search.ts | 0 .../effect/src/http/Template.ts | 0 .../effect/src/http/Url.ts | 7 +- .../effect/src/http/UrlParams.ts | 0 .../effect/src/http/index.ts | 0 .../effect/src/http/internal/compression.ts | 0 .../src/http/internal/compressionBody.ts | 0 .../effect/src/http/internal/decompression.ts | 43 +- .../effect/src/http/internal/headers.ts | 0 .../effect/src/http/internal/httpBody.ts | 0 .../effect/src/http/internal/lazyStream.ts | 25 + .../effect/src/http/internal/mimeTypes.ts | 0 .../effect/src/http/internal/requestState.ts | 33 + .../effect/src/http/internal/tracing.ts | 0 .../effect/src/http/internal/urlParams.ts | 0 .../effect/src/http/internal/webResponse.ts | 20 + .../packages => packages}/effect/src/index.ts | 24 + .../effect/src/internal/arbitrary/array.ts | 0 .../src/internal/arbitrary/bigDecimal.ts | 0 .../effect/src/internal/arbitrary/bigInt.ts | 0 .../effect/src/internal/arbitrary/model.ts | 0 .../effect/src/internal/arbitrary/regexp.ts | 0 .../effect/src/internal/arbitrary/runner.ts | 0 .../effect/src/internal/arbitrary/schema.ts | 0 .../effect/src/internal/array.ts | 0 .../effect/src/internal/core.ts | 2 +- .../effect/src/internal/count.ts | 0 .../effect/src/internal/dateTime.ts | 27 +- .../effect/src/internal/doNotation.ts | 0 .../effect/src/internal/effect.ts | 204 +- .../effect/src/internal/equal.ts | 0 .../effect/src/internal/errors.ts | 0 .../effect/src/internal/executionPlan.ts | 0 .../effect/src/internal/graph.ts | 0 .../effect/src/internal/graphCsr.ts | 0 .../effect/src/internal/graphql.ts | 0 .../effect/src/internal/graphqlHeaders.ts | 0 .../effect/src/internal/hash.ts | 0 .../effect/src/internal/hashMap.ts | 4 +- .../effect/src/internal/hashSet.ts | 0 packages/effect/src/internal/hydration.ts | 15 + packages/effect/src/internal/inflate.ts | 710 + .../effect/src/internal/layer.ts | 0 .../effect/src/internal/matcher.ts | 0 .../effect/src/internal/metric.ts | 0 .../effect/src/internal/option.ts | 0 .../effect/src/internal/persistence.ts | 0 .../effect/src/internal/random.ts | 0 .../effect/src/internal/rcRef.ts | 0 .../effect/src/internal/record.ts | 6 + .../effect/src/internal/redacted.ts | 0 .../effect/src/internal/references.ts | 0 .../effect/src/internal/request.ts | 27 +- .../effect/src/internal/result.ts | 0 .../effect/src/internal/schedule.ts | 59 +- .../effect/src/internal/schema/annotations.ts | 0 .../effect/src/internal/schema/cause.ts | 0 .../effect/src/internal/schema/codegen.ts | 0 .../src/internal/schema/compilerRegistry.ts | 0 .../internal/schema/fromJsonSchemaDocument.ts | 455 +- .../src/internal/schema/fromRepresentation.ts | 0 .../effect/src/internal/schema/interpreter.ts | 103 +- .../effect/src/internal/schema/make.ts | 0 .../effect/src/internal/schema/parser.ts | 0 .../src/internal/schema/standardSchema.ts | 62 +- .../src/internal/schema/toCodeDocument.ts | 11 +- .../effect/src/internal/schema/toCodec.ts | 117 +- .../src/internal/schema/toDifferJsonPatch.ts | 0 .../src/internal/schema/toEncoderXml.ts | 0 .../src/internal/schema/toEquivalence.ts | 0 .../effect/src/internal/schema/toFormatter.ts | 12 +- .../effect/src/internal/schema/toIso.ts | 0 .../internal/schema/toJsonSchemaDocument.ts | 108 +- .../src/internal/schema/toRepresentation.ts | 0 .../effect/src/internal/schemaError.ts | 0 .../effect/src/internal/stackTraceLimit.ts | 0 .../effect/src/internal/stream.ts | 0 .../effect/src/internal/tracer.ts | 0 .../effect/src/internal/trie.ts | 4 +- .../effect/src/internal/ulid.ts | 0 .../effect/src/internal/uuid.ts | 25 +- packages/effect/src/internal/version.ts | 2 + .../effect/src/net/AddressResolver.ts | 0 .../effect/src/net/Dns.ts | 0 .../effect/src/net/Host.ts | 0 .../effect/src/net/IpInterface.ts | 0 .../effect/src/net/IpNetwork.ts | 0 .../effect/src/net/NetAddress.ts | 11 +- .../effect/src/net/index.ts | 0 .../effect/src/observability/Otlp.ts | 0 .../effect/src/observability/OtlpExporter.ts | 17 +- .../effect/src/observability/OtlpLogger.ts | 0 .../effect/src/observability/OtlpMetrics.ts | 8 +- .../effect/src/observability/OtlpResource.ts | 22 +- .../src/observability/OtlpSerialization.ts | 20 +- .../effect/src/observability/OtlpTracer.ts | 0 .../src/observability/PrometheusMetrics.ts | 0 .../effect/src/observability/index.ts | 0 .../src/observability/internal/otlpEnv.ts | 0 .../observability/internal/otlpProtobuf.ts | 0 .../src/observability/internal/protobuf.ts | 12 +- .../effect/src/persistence/KeyValueStore.ts | 0 .../effect/src/persistence/Persistable.ts | 0 .../effect/src/persistence/PersistedCache.ts | 4 +- .../effect/src/persistence/PersistedQueue.ts | 374 +- .../effect/src/persistence/Persistence.ts | 94 +- .../effect/src/persistence/RateLimiter.ts | 39 +- .../effect/src/persistence/Redis.ts | 0 .../effect/src/persistence/UserDirectories.ts | 273 + .../effect/src/persistence/index.ts | 6 + .../effect/src/process/ChildProcess.ts | 3 +- .../effect/src/process/ChildProcessSpawner.ts | 0 .../effect/src/process/index.ts | 0 .../effect/src/reactivity/AsyncResult.ts | 4 +- .../effect/src/reactivity/Atom.ts | 172 +- .../effect/src/reactivity/AtomHttpApi.ts | 26 +- .../effect/src/reactivity/AtomRef.ts | 0 .../effect/src/reactivity/AtomRegistry.ts | 61 +- .../effect/src/reactivity/AtomRpc.ts | 28 +- .../effect/src/reactivity/Hydration.ts | 65 +- .../effect/src/reactivity/Reactivity.ts | 121 +- .../effect/src/reactivity/index.ts | 0 .../effect/src/rpc/Rpc.ts | 21 + .../effect/src/rpc/RpcClient.ts | 382 +- .../effect/src/rpc/RpcClientError.ts | 0 .../effect/src/rpc/RpcGroup.ts | 0 .../effect/src/rpc/RpcMessage.ts | 0 .../effect/src/rpc/RpcMiddleware.ts | 0 .../effect/src/rpc/RpcSchema.ts | 0 .../effect/src/rpc/RpcSerialization.ts | 26 +- .../effect/src/rpc/RpcServer.ts | 31 +- .../effect/src/rpc/RpcTest.ts | 0 .../effect/src/rpc/RpcWorker.ts | 0 .../effect/src/rpc/Utils.ts | 0 .../effect/src/rpc/index.ts | 0 .../effect/src/rpc/internal/tracing.ts | 0 .../effect/src/schema/Model.ts | 0 .../effect/src/schema/SchemaAOTCompiler.ts | 0 .../src/schema/SchemaAOTCompiler/Build.ts | 0 .../effect/src/schema/SchemaCompiler.ts | 0 .../src/schema/SchemaCompiler/runtime.ts | 0 .../effect/src/schema/SchemaJITCompiler.ts | 0 .../src/schema/SchemaJITCompiler/enable.ts | 0 .../effect/src/schema/VariantSchema.ts | 108 +- .../effect/src/schema/index.ts | 0 .../effect/src/socket/DatagramSocket.ts | 0 .../effect/src/socket/Socket.ts | 39 +- .../effect/src/socket/SocketServer.ts | 0 .../effect/src/socket/index.ts | 0 .../effect/src/sql/Migrator.ts | 28 +- .../effect/src/sql/SqlClient.ts | 295 +- .../effect/src/sql/SqlConnection.ts | 0 .../effect/src/sql/SqlError.ts | 0 .../effect/src/sql/SqlModel.ts | 0 .../effect/src/sql/SqlResolver.ts | 9 + .../effect/src/sql/SqlSchema.ts | 0 .../effect/src/sql/SqlStream.ts | 0 .../effect/src/sql/Statement.ts | 43 +- .../effect/src/sql/index.ts | 0 .../effect/src/testing/TestClock.ts | 5 +- .../effect/src/testing/TestConsole.ts | 0 .../effect/src/testing/TestCrypto.ts | 0 .../effect/src/testing/TestSchema.ts | 12 +- .../effect/src/testing/index.ts | 0 .../effect/src/workers/Transferable.ts | 0 .../effect/src/workers/Worker.ts | 0 .../effect/src/workers/WorkerError.ts | 0 .../effect/src/workers/WorkerRunner.ts | 0 .../effect/src/workers/index.ts | 0 .../effect/src/workflow/Activity.ts | 0 .../effect/src/workflow/DurableClock.ts | 0 .../effect/src/workflow/DurableDeferred.ts | 0 .../effect/src/workflow/DurableQueue.ts | 0 .../effect/src/workflow/Workflow.ts | 0 .../effect/src/workflow/WorkflowEngine.ts | 44 +- .../effect/src/workflow/WorkflowProxy.ts | 0 .../src/workflow/WorkflowProxyServer.ts | 0 .../effect/src/workflow/index.ts | 0 .../effect/src/workflow/internal/crypto.ts | 0 .../effect/test/Array.test.ts | 27 + .../effect/test/ArrayNaNIndex.test.ts | 0 .../effect/test/AtomRef.test.ts | 0 .../effect/test/BigDecimal.test.ts | 0 .../effect/test/BigInt.test.ts | 36 + .../effect/test/Boolean.test.ts | 0 .../effect/test/Brand.test.ts | 0 .../effect/test/ByteSize.test.ts | 0 packages/effect/test/Bytes.test.ts | 31 + .../effect/test/Cache.test.ts | 69 +- .../effect/test/Cause.test.ts | 36 + .../effect/test/CauseMapAnnotations.test.ts | 0 .../effect/test/Channel.test.ts | 167 +- .../effect/test/Chunk.test.ts | 91 + .../effect/test/Clock.test.ts | 0 .../effect/test/Combiner.test.ts | 0 .../effect/test/Config.test.ts | 23 + .../effect/test/ConfigProvider.test.ts | 34 +- .../effect/test/Context.test.ts | 0 .../effect/test/Cron.test.ts | 8 + .../effect/test/Crypto.test.ts | 0 .../effect/test/Data.test.ts | 17 + .../effect/test/DateTime.test.ts | 70 + .../effect/test/Deferred.test.ts | 0 .../effect/test/Duration.test.ts | 30 + .../effect/test/Effect.test.ts | 414 +- .../effect/test/EffectEager.test.ts | 0 .../effect/test/EffectKeepAlive.test.ts | 0 .../effect/test/Effectable.test.ts | 0 .../effect/test/Equal.test.ts | 0 .../effect/test/Equivalence.test.ts | 6 + .../effect/test/ExecutionPlan.test.ts | 0 packages/effect/test/Exit.test.ts | 42 + .../effect/test/Fiber.test.ts | 8 + .../effect/test/FiberHandle.test.ts | 30 + .../effect/test/FiberMap.test.ts | 31 + .../effect/test/FiberSet.test.ts | 0 .../test/FiberSetRuntimePropagation.test.ts | 0 .../effect/test/FileSystem.test-utils.ts | 365 + .../effect/test/Formatter.test.ts | 0 .../effect/test/Function.test.ts | 0 .../effect/test/Graph.test.ts | 0 .../effect/test/Hash.test.ts | 0 .../effect/test/HashMap.test.ts | 46 + .../effect/test/HashRing.test.ts | 0 .../effect/test/HashSet.test.ts | 0 .../effect/test/HttpClient.test.ts | 14 + .../effect/test/Iterable.test.ts | 7 + .../effect/test/JsonPatch.test.ts | 0 .../effect/test/JsonPointer.test.ts | 0 .../effect/test/JsonSchema.test.ts | 21 + .../effect/test/Latch.test.ts | 0 .../effect/test/Layer.test.ts | 29 + .../effect/test/LayerMap.test.ts | 0 .../effect/test/LayerRef.test.ts | 0 .../effect/test/LogLevel.test.ts | 0 .../effect/test/Logger.test.ts | 17 +- .../effect/test/ManagedRuntime.test.ts | 128 + .../effect/test/Match.test.ts | 0 .../effect/test/Metric.test.ts | 54 +- .../effect/test/Migrator.test.ts | 0 .../effect/test/MutableHashMap.test.ts | 0 .../effect/test/MutableHashSet.test.ts | 0 .../effect/test/MutableList.test.ts | 0 .../effect/test/Newtype.test.ts | 0 .../effect/test/Number.test.ts | 3 + .../effect/test/Optic.test.ts | 0 .../effect/test/Option.test.ts | 38 + .../effect/test/Order.test.ts | 0 .../effect/test/Ordering.test.ts | 0 .../effect/test/PartitionedSemaphore.test.ts | 0 .../effect/test/Path.test.ts | 15 +- .../effect/test/Pathfinding.test.ts | 0 .../effect/test/Pool.test.ts | 9 +- .../effect/test/Predicate.test.ts | 0 .../effect/test/PubSub.test.ts | 52 +- .../effect/test/Pull.test.ts | 0 .../effect/test/Queue.test.ts | 13 +- .../effect/test/Random.test.ts | 15 +- .../effect/test/RcMap.test.ts | 67 +- .../effect/test/RcRef.test.ts | 0 .../effect/test/Record.test.ts | 0 .../effect/test/Redacted.test.ts | 0 .../effect/test/Reducer.test.ts | 0 .../effect/test/Ref.test.ts | 0 .../effect/test/Request.test.ts | 109 + packages/effect/test/RequestResolver.test.ts | 134 + .../effect/test/Resource.test.ts | 44 + .../effect/test/Result.test.ts | 0 .../effect/test/Schedule.test.ts | 29 +- .../effect/test/Scheduler.test.ts | 0 .../effect/test/Scope.test.ts | 2 +- .../effect/test/ScopedCache.test.ts | 530 +- .../effect/test/ScopedRef.test.ts | 0 packages/effect/test/SemVer.test.ts | 191 + packages/effect/test/SemVerRange.test.ts | 145 + .../effect/test/Semaphore.test.ts | 0 .../effect/test/Sink.test.ts | 16 + .../effect/test/StackCapture.test.ts | 0 .../effect/test/StackTraceLimit.test.ts | 0 .../effect/test/Stdio.test.ts | 0 .../effect/test/Stream.test.ts | 478 +- .../effect/test/String.test.ts | 23 + .../effect/test/Struct.test.ts | 0 .../effect/test/SubscriptionRef.test.ts | 0 .../effect/test/Symbol.test.ts | 0 .../SynchronizedRef.modifySomeEffect.test.ts | 0 .../effect/test/SynchronizedRef.test.ts | 0 .../effect/test/TestClock.test.ts | 44 + packages/effect/test/TextDiff.test.ts | 223 + .../effect/test/Tracer.test.ts | 53 +- .../effect/test/Trie.test.ts | 19 + .../effect/test/Tuple.test.ts | 0 .../effect/test/TxChunk.test.ts | 0 .../effect/test/TxDeferred.test.ts | 0 .../effect/test/TxHashMap.test.ts | 0 .../effect/test/TxHashSet.test.ts | 0 .../effect/test/TxPriorityQueue.test.ts | 0 .../effect/test/TxPubSub.test.ts | 0 .../effect/test/TxQueue.test.ts | 0 .../effect/test/TxReentrantLock.test.ts | 0 .../effect/test/TxSemaphore.test.ts | 0 .../effect/test/TxSubscriptionRef.test.ts | 0 .../effect/test/UndefinedOr.test.ts | 0 .../effect/test/Version.test.ts | 0 .../effect/test/ai/AiError.test.ts | 38 + .../test/ai/AnthropicStructuredOutput.test.ts | 0 ...opicStructuredOutputRepresentation.test.ts | 0 .../effect/test/ai/Chat.test.ts | 71 +- .../effect/test/ai/DecisionModel.test.ts | 113 +- .../effect/test/ai/EmbeddingModel.test.ts | 0 .../effect/test/ai/LanguageModel.test.ts | 71 +- .../ai/LanguageModelRepresentation.test.ts | 0 .../ai/LanguageModelTrackerLifecycle.test.ts | 0 packages/effect/test/ai/McpClient.test.ts | 623 + .../effect/test/ai/McpSchema.test.ts | 14 + .../McpServer/LayerHttpRequestScoped.test.ts | 0 .../McpConformance/BaseProtocolTest.ts | 22 +- .../McpConformance/CompletionTest.ts | 44 +- .../McpConformance/ElicitationTest.ts | 0 .../McpServer/McpConformance/LifecycleTest.ts | 0 .../McpServer/McpConformance/LoggingTest.ts | 45 +- .../McpConformance/McpConformance.ts | 0 .../McpConformance/McpConformanceFixtures.ts | 0 .../McpServer/McpConformance/McpTestPeer.ts | 0 .../McpConformance/MultiRoundTripTest.ts | 0 .../McpServer/McpConformance/PromptsTest.ts | 34 +- .../McpServer/McpConformance/ResourcesTest.ts | 0 .../ai/McpServer/McpConformance/RootsTest.ts | 0 .../McpServer/McpConformance/SamplingTest.ts | 0 .../McpConformance/SubscriptionsTest.ts | 15 +- .../ai/McpServer/McpConformance/ToolsTest.ts | 52 +- .../McpConformance/TransportsTest.ts | 0 .../McpServer/McpConformance/UtilitiesTest.ts | 0 .../test/ai/McpServer/McpProtocol.test.ts | 0 .../test/ai/McpServer/McpServer.test.ts | 264 +- .../ai/McpServer/ProtocolAdapters.test.ts | 88 +- .../ai/McpServer/TestUtils/McpHttpHarness.ts | 0 .../ai/McpServer/TestUtils/McpHttpResponse.ts | 0 .../ai/McpServer/TestUtils/McpServerLayer.ts | 0 .../ai/McpServer/TestUtils/McpStdioHarness.ts | 0 .../McpServer/TestUtils/McpWorkerdHarness.ts | 0 .../effect/test/ai/McpServer/Workerd.test.ts | 0 .../test/ai/McpServer/fixtures/workerd.ts | 0 .../test/ai/McpServer/v2024_11_05.test.ts | 0 .../test/ai/McpServer/v2025_03_26.test.ts | 0 .../test/ai/McpServer/v2025_06_18.test.ts | 0 .../test/ai/McpServer/v2025_11_25.test.ts | 0 .../test/ai/McpServer/v2026_07_28.test.ts | 0 .../effect/test/ai/Model.test.ts | 0 .../test/ai/OpenAiStructuredOutput.test.ts | 0 ...enAiStructuredOutputRepresentation.test.ts | 0 .../effect/test/ai/Prompt.test.ts | 0 .../effect/test/ai/Response.test.ts | 39 + .../effect/test/ai/ResponseIdTracker.test.ts | 0 .../effect/test/ai/Tokenizer.test.ts | 0 .../effect/test/ai/Tool.test.ts | 134 +- .../effect/test/ai/ToolRepresentation.test.ts | 0 .../effect/test/ai/utils.ts | 0 .../test/arbitrary/Arbitrary.array.test.ts | 0 .../test/arbitrary/Arbitrary.causes.test.ts | 0 .../arbitrary/Arbitrary.composition.test.ts | 0 .../arbitrary/Arbitrary.configuration.test.ts | 0 .../effect/test/arbitrary/Arbitrary.test.ts | 0 .../effect/test/cli/Ansi.test.ts | 0 .../effect/test/cli/Arguments.test.ts | 0 .../effect/test/cli/Command.test.ts | 235 +- .../effect/test/cli/Errors.test.ts | 14 + .../effect/test/cli/Help.test.ts | 0 .../effect/test/cli/Lexer.test.ts | 18 + .../effect/test/cli/LogLevel.test.ts | 0 .../effect/test/cli/Param.test.ts | 0 .../effect/test/cli/Primitive.test.ts | 30 +- .../effect/test/cli/Prompt.test.ts | 17 +- .../test/cli/TerminalCapabilities.test.ts | 68 + .../test/cli/completions/completions.test.ts | 63 + .../test/cli/completions/descriptor.test.ts | 0 .../test/cli/fixtures/ComprehensiveCli.ts | 0 .../effect/test/cli/services/MockTerminal.ts | 0 .../effect/test/cli/services/TestActions.ts | 0 .../effect/test/cluster/ClusterCron.test.ts | 269 + .../cluster/ClusterWorkflowEngine.test.ts | 385 +- .../effect/test/cluster/Entity.test.ts | 20 +- .../effect/test/cluster/EntityProxy.test.ts | 0 .../effect/test/cluster/Envelope.test.ts | 0 .../effect/test/cluster/K8sHttpClient.test.ts | 0 .../test/cluster/MessageStorage.test.ts | 0 .../effect/test/cluster/ResourceMap.test.ts | 0 .../effect/test/cluster/ResourceRef.test.ts | 36 +- .../effect/test/cluster/RunnerServer.test.ts | 39 +- .../effect/test/cluster/RunnerStorage.test.ts | 0 .../effect/test/cluster/Runners.test.ts | 70 +- .../effect/test/cluster/Sharding.test.ts | 844 +- .../test/cluster/ShardingConfig.test.ts | 0 .../effect/test/cluster/TestEntity.ts | 0 .../effect/test/cluster/interruptors.test.ts | 0 .../test/devtools/DevToolsClient.test.ts | 208 + .../effect/test/encoding/Base64.test.ts | 0 .../effect/test/encoding/Base64Url.test.ts | 5 + .../test/encoding/EncodingError.test.ts | 0 .../effect/test/encoding/Hex.test.ts | 0 .../effect/test/encoding/Ini.test.ts | 7 + .../effect/test/encoding/JsonArray.test.ts | 252 + .../effect/test/encoding/Ndjson.test.ts | 39 + .../effect/test/encoding/SchemaBinary.test.ts | 0 .../effect/test/encoding/Sse.test.ts | 0 packages/effect/test/encoding/Tar.test.ts | 213 + .../effect/test/encoding/Toml.test.ts | 16 + packages/effect/test/encoding/Xml.test.ts | 505 + .../effect/test/encoding/Yaml.test.ts | 80 +- packages/effect/test/encoding/Zip.test.ts | 666 + .../effect/test/eventlog/EventJournal.test.ts | 601 + .../eventlog/EventJournalConflictsTest.ts | 88 + .../eventlog/EventJournalIndexedDb.test.ts | 769 + .../effect/test/eventlog/EventLog.test.ts | 1397 + .../EventLogIdentityDerivation.test.ts | 0 .../test/eventlog/EventLogMessage.test.ts | 115 + .../test/eventlog/EventLogRemote.test.ts | 475 + .../eventlog/EventLogServerEncrypted.test.ts | 4 + .../eventlog/EventLogServerEncryptedTest.ts | 233 + .../EventLogServerUnencrypted.test.ts | 160 +- ...EventLogServerUnencryptedRegressionTest.ts | 265 + .../test/eventlog/EventLogSessionAuth.test.ts | 0 .../effect/test/eventlog/EventRow.test.ts | 637 + .../eventlog/SqlEventJournalSharedTest.ts | 81 + .../SqlEventLogServerEncryptedStorageTest.ts | 140 + ...SqlEventLogServerUnencryptedStorageTest.ts | 115 + .../test/fixtures/migrator/0001_first.js | 0 packages/effect/test/fixtures/tar/README.md | 11 + packages/effect/test/fixtures/tar/gnu.tar.gz | Bin 0 -> 548 bytes packages/effect/test/fixtures/tar/pax.tar.gz | Bin 0 -> 877 bytes .../test/fixtures/tar/sparse-gnu.tar.gz | Bin 0 -> 161 bytes .../test/fixtures/tar/sparse-pax.tar.gz | Bin 0 -> 330 bytes .../effect/test/fixtures/tar/ustar.tar.gz | Bin 0 -> 239 bytes packages/effect/test/fixtures/tar/v7.tar.gz | Bin 0 -> 154 bytes .../effect/test/fixtures/text.txt | 0 packages/effect/test/fixtures/zip/7zip.zip | Bin 0 -> 914 bytes packages/effect/test/fixtures/zip/README.md | 17 + .../test/fixtures/zip/infozip-stream.zip | Bin 0 -> 893 bytes packages/effect/test/fixtures/zip/infozip.zip | Bin 0 -> 997 bytes .../effect/test/fixtures/zip/python-zip64.zip | Bin 0 -> 339 bytes .../effect/test/graphql/GraphQL.test.ts | 0 .../graphql/GraphQLClient.makeTest.test.ts | 0 .../test/graphql/GraphQLClient.pages.test.ts | 21 +- .../GraphQLClient.subscriptions.test.ts | 23 +- .../effect/test/graphql/GraphQLClient.test.ts | 0 .../test/graphql/GraphQLClientError.test.ts | 0 .../test/graphql/GraphQLProtocol.sse.test.ts | 0 .../test/graphql/GraphQLProtocol.test.ts | 0 .../graphql/GraphQLProtocol.webSocket.test.ts | 15 +- .../test/graphql/GraphQLResolver.test.ts | 185 +- .../effect/test/graphql/fixtures.ts | 21 +- .../effect/test/http/Cookies.test.ts | 0 .../effect/test/http/FetchHttpClient.test.ts | 62 + .../http/FindMyWay/case-insensitive.test.ts | 0 .../http/FindMyWay/matching-order.test.ts | 0 .../test/http/FindMyWay/methods.test.ts | 0 .../http/FindMyWay/optional-params.test.ts | 0 .../http/FindMyWay/params-collisions.test.ts | 0 .../http/FindMyWay/path-params-match.test.ts | 0 .../test/http/FindMyWay/querystring.test.ts | 0 .../effect/test/http/FindMyWay/regex.test.ts | 0 packages/effect/test/http/Headers.test.ts | 256 + .../effect/test/http/HttpBody.test.ts | 0 .../effect/test/http/HttpClient.test.ts | 117 +- .../test/http/HttpClientRequest.test.ts | 0 .../effect/test/http/HttpCompression.test.ts | 0 .../test/http/HttpDecompression.test.ts | 29 +- .../effect/test/http/HttpEffect.test.ts | 37 +- .../effect/test/http/HttpMiddleware.test.ts | 77 + .../effect/test/http/HttpPlatform.test.ts | 0 .../effect/test/http/HttpRouter.test.ts | 63 +- .../effect/test/http/HttpServer.test.ts | 0 .../effect/test/http/HttpServerError.test.ts | 0 .../test/http/HttpServerRequest.body.test.ts | 25 + .../test/http/HttpServerRequest.test.ts | 13 + .../test/http/HttpServerRespondable.test.ts | 0 .../test/http/HttpServerResponse.test.ts | 24 + .../effect/test/http/HttpStaticServer.test.ts | 54 +- .../effect/test/http/HttpStatus.test.ts | 0 .../effect/test/http/HttpTraceContext.test.ts | 0 .../effect/test/http/Mime.test.ts | 0 .../effect/test/http/Multipart.test.ts | 61 + .../test/http/SchemaRepresentation.test.ts | 0 .../effect/test/http/Template.test.ts | 0 .../effect/test/http/Url.test.ts | 13 + .../effect/test/http/UrlParams.test.ts | 0 .../effect/test/httpapi/HttpApi.test.ts | 0 .../test/httpapi/HttpApiBuilder.test.ts | 281 +- .../effect/test/httpapi/HttpApiClient.test.ts | 53 + .../test/httpapi/HttpApiDocumentation.test.ts | 0 .../test/httpapi/HttpApiEndpoint.test.ts | 73 + .../effect/test/httpapi/HttpApiError.test.ts | 0 .../effect/test/httpapi/HttpApiSchema.test.ts | 22 + .../test/httpapi/HttpApiSecurity.test.ts | 0 .../effect/test/httpapi/HttpApiTest.test.ts | 99 + .../effect/test/httpapi/OpenApi.test.ts | 72 +- .../httpapi/OpenApiRepresentation.test.ts | 57 + .../effect/test/internal/Uuid.test.ts | 0 .../effect/test/net/AddressResolver.test.ts | 0 .../effect/test/net/Dns.test.ts | 0 .../effect/test/net/Host.test.ts | 0 .../effect/test/net/IpInterface.test.ts | 0 .../effect/test/net/IpNetwork.test.ts | 0 .../effect/test/net/NetAddress.test.ts | 0 .../observability/OtlpCompression.test.ts | 0 .../test/observability/OtlpConfig.test.ts | 0 .../test/observability/OtlpEnvHeaders.test.ts | 0 .../test/observability/OtlpExporter.test.ts | 19 + .../test/observability/OtlpLogger.test.ts | 0 .../test/observability/OtlpMetrics.test.ts | 22 + .../test/observability/OtlpResource.test.ts | 15 + .../observability/OtlpSerialization.test.ts | 160 + .../test/observability/OtlpTracer.test.ts | 0 .../observability/PrometheusMetrics.test.ts | 0 .../test/persistence/KeyValueStore.test.ts | 0 .../test/persistence/KeyValueStoreTest.ts | 0 .../test/persistence/PersistedCache.test.ts | 0 .../test/persistence/PersistedCacheTest.ts | 39 + .../test/persistence/PersistedQueue.test.ts | 0 .../test/persistence/PersistedQueueSqlTest.ts | 155 + .../test/persistence/PersistedQueueTest.ts | 88 +- .../test/persistence/RateLimiter.test.ts | 55 + .../test/persistence/RateLimiterTest.ts | 49 + .../effect/test/persistence/Redis.test.ts | 0 .../effect/test/persistence/SqlCleanupTest.ts | 0 .../test/persistence/UserDirectories.test.ts | 150 + .../effect/test/process/ChildProcess.test.ts | 0 .../test/process/ChildProcessSpawnerTest.ts | 17 +- .../process/fixtures/bash/no-permissions.sh | 0 .../fixtures/bash/parent-exits-early.sh | 0 .../process/fixtures/bash/spawn-children.sh | 0 .../test/process/fixtures/config/SHOUTING | 0 .../test/process/fixtures/config/integer | 0 .../process/fixtures/config/nested/config | 0 .../test/process/fixtures/config/secret | 0 .../test/reactivity/AsyncResult.test.ts | 14 +- .../effect/test/reactivity/Atom.test.ts | 372 +- .../test/reactivity/AtomHttpApi.test.ts | 54 + .../effect/test/reactivity/AtomRpc.test.ts | 58 +- .../effect/test/reactivity/Hydration.test.ts | 232 + .../effect/test/rpc/Rpc.test.ts | 0 .../effect/test/rpc/RpcClient.test.ts | 552 +- .../effect/test/rpc/RpcSerialization.test.ts | 27 +- .../effect/test/rpc/RpcServer.test.ts | 95 + .../effect/test/schema/CodePoints.test.ts | 0 .../effect/test/schema/Graph.test.ts | 0 .../effect/test/schema/HMR.test.ts | 0 .../effect/test/schema/Schema.test.ts | 133 +- .../test/schema/SchemaAOTCompiler.test.ts | 0 .../schema/SchemaAOTCompilerBuild.test.ts | 0 .../effect/test/schema/SchemaAST.test.ts | 0 .../test/schema/SchemaCompilerApi.test.ts | 0 .../test/schema/SchemaCompilerArray.test.ts | 0 .../schema/SchemaCompilerConcurrency.test.ts | 0 .../schema/SchemaCompilerConstruction.test.ts | 0 .../schema/SchemaCompilerRegression.test.ts | 0 .../test/schema/SchemaCompilerStartup.test.ts | 0 .../effect/test/schema/SchemaGetter.test.ts | 18 + .../effect/test/schema/SchemaIssue.test.ts | 0 .../test/schema/SchemaJITCompiler.test.ts | 0 .../SchemaJITCompilerCheckedSources.test.ts | 0 .../SchemaJITCompilerExcessProperties.test.ts | 0 .../schema/SchemaJITCompilerFallback.test.ts | 0 .../effect/test/schema/SchemaParser.test.ts | 151 +- .../test/schema/SchemaTransformation.test.ts | 15 +- .../test/schema/TemplateLiteral.test.ts | 0 .../effect/test/schema/VariantSchema.test.ts | 36 + .../effect/test/schema/fixtures/HMR-Class.ts | 0 .../test/schema/fixtures/HMR-sanity-check.ts | 0 .../effect/test/schema/fixtures/aot-build.ts | 0 .../test/schema/fixtures/aot-import-guard.ts | 0 .../effect/test/schema/fixtures/aot-runner.ts | 0 .../effect/test/schema/fixtures/aot.ts | 0 .../test/schema/fixtures/construction.ts | 0 .../test/schema/jsonSchemaRoundTrip.test.ts | 0 .../representation/builtInRevivers.test.ts | 18 + .../schema/representation/fromJson.test.ts | 0 .../fromJsonMultiDocument.test.ts | 0 .../fromJsonSchemaAnnotations.test.ts | 360 + .../fromJsonSchemaDocument.test.ts | 51 +- .../fromJsonSchemaMultiDocument.test.ts | 15 + .../representation/fromRepresentation.test.ts | 0 .../fromRepresentations.test.ts | 0 .../schema/representation/makeCode.test.ts | 0 .../schemaToJsonSchemaDocument.test.ts | 0 .../stringTreeRoundtrip.test.ts | 0 .../toCodeDocument.annotations.test.ts | 0 .../representation/toCodeDocument.test.ts | 8 +- .../test/schema/representation/toJson.test.ts | 0 .../toJsonMultiDocument.test.ts | 0 .../toJsonSchemaDocument.test.ts | 64 +- .../toJsonSchemaMultiDocument.test.ts | 0 .../representation/toMultiDocument.test.ts | 0 .../representation/toRepresentation.test.ts | 0 .../representation/toRepresentations.test.ts | 0 .../schema/standardSchemaAdapters.test.ts | 93 + .../effect/test/schema/toCodec.test.ts | 107 + .../test/schema/toDifferJsonPatch.test.ts | 0 .../effect/test/schema/toEquivalence.test.ts | 0 .../effect/test/schema/toFormatter.test.ts | 45 +- .../effect/test/schema/toIso.test.ts | 0 .../test/schema/toJsonSchemaDocument.test.ts | 187 +- .../schema/toStandardJSONSchemaV1.test.ts | 7 +- .../test/schema/toStandardSchemaV1.test.ts | 124 +- .../effect/test/schema/v3-v4.test.ts | 0 .../effect/test/socket/DatagramSocket.test.ts | 0 .../effect/test/socket/Socket.test.ts | 32 + packages/effect/test/sql/SqlClient.test.ts | 620 + .../effect/test/sql/SqlError.test.ts | 0 .../effect/test/sql/SqlResolver.test.ts | 0 .../effect/test/sql/SqlSchema.test.ts | 0 .../effect/test/sql/Statement.test.ts | 44 +- .../effect/test/testing/TestCrypto.test.ts | 28 + .../effect/test/testing/TestSchema.test.ts | 6 + .../effect/test/utils/assert.ts | 0 .../effect/test/utils/chunkCoordination.ts | 0 .../effect/test/utils/counter.ts | 0 .../effect/test/utils/gc.ts | 0 .../effect/test/workers/Worker.test.ts | 0 .../effect/test/workers/WorkerError.test.ts | 0 .../effect/test/workflow/DurableQueue.test.ts | 21 +- .../effect/test/workflow/Workflow.test.ts | 0 .../test/workflow/WorkflowEngine.test.ts | 207 + .../effect/tsconfig.json | 0 .../effect/typeperf/README.md | 0 .../effect/typeperf/compare.mjs | 0 .../effect/typeperf/compare/httpapi.mjs | 0 .../effect/typeperf/config.json | 0 .../effect/typeperf/run.mjs | 0 .../suites/graphql-selections/baseline.ts | 0 .../fixtures/opaque-class-count-5.ts | 0 .../fixtures/struct-interface-count-5.ts | 0 .../suites/graphql-selections/thresholds.json | 0 .../typeperf/suites/httpapi/baseline.ts | 0 .../fixtures/_endpoint-selection-types.ts | 0 .../httpapi/fixtures/_grouped-api-500.ts | 0 .../httpapi/fixtures/_grouped-api-types.ts | 0 .../fixtures/builder-endpoint-count-500.ts | 0 .../fixtures/builder-handle-all-count-500.ts | 0 ...uilder-handle-all-two-batches-count-500.ts | 0 .../fixtures/builder-handlers-count-100.ts | 0 .../builder-raw-handlers-count-100.ts | 0 .../fixtures/client-endpoint-count-500.ts | 0 .../fixtures/client-group-count-500.ts | 0 .../fixtures/client-groups-100x5-count-500.ts | 0 .../fixtures/client-methods-count-500.ts | 0 .../fixtures/client-top-level-count-500.ts | 0 .../fixtures/endpoint-class-count-500.ts | 0 .../httpapi/fixtures/endpoint-count-500.ts | 0 .../fixtures/group-endpoints-count-500.ts | 0 .../httpapi/fixtures/url-builder-count-500.ts | 0 .../url-builder-top-level-count-500.ts | 0 .../typeperf/suites/httpapi/thresholds.json | 0 .../effect/typeperf/suites/schema/baseline.ts | 0 .../typeperf/suites/schema/fixtures/array.ts | 0 .../typeperf/suites/schema/fixtures/brand.ts | 0 .../schema/fixtures/decode-unknown-effect.ts | 0 .../schema/fixtures/encode-unknown-effect.ts | 0 .../typeperf/suites/schema/fixtures/exit.ts | 0 .../suites/schema/fixtures/non-empty-array.ts | 0 .../typeperf/suites/schema/fixtures/record.ts | 0 .../suites/schema/fixtures/struct-mixed.ts | 0 .../schema/fixtures/struct-mutable-only.ts | 0 .../schema/fixtures/struct-optional-only.ts | 0 .../suites/schema/fixtures/struct-required.ts | 0 .../schema/fixtures/struct-with-rest.ts | 0 .../suites/schema/fixtures/tagged-union.ts | 0 .../fixtures/to-codec-array-from-single.ts | 0 .../suites/schema/fixtures/to-codec-json.ts | 0 .../schema/fixtures/to-codec-string-tree.ts | 0 .../suites/schema/fixtures/to-encoded.ts | 0 .../fixtures/to-standard-json-schema.ts | 0 .../suites/schema/fixtures/to-type.ts | 0 .../suites/schema/fixtures/tuple-with-rest.ts | 0 .../typeperf/suites/schema/fixtures/tuple.ts | 0 .../typeperf/suites/schema/fixtures/union.ts | 0 .../typeperf/suites/schema/thresholds.json | 0 .../effect/typetest/AiError.tst.ts | 0 .../effect/typetest/Array.tst.ts | 0 .../effect/typetest/Brand.tst.ts | 0 .../effect/typetest/ByteSize.tst.ts | 0 .../effect/typetest/Channel.tst.ts | 29 + .../effect/typetest/ChannelSchema.tst.ts | 0 .../effect/typetest/Clock.tst.ts | 0 .../effect/typetest/Config.tst.ts | 0 .../effect/typetest/ConfigProvider.tst.ts | 0 .../effect/typetest/Context.tst.ts | 0 .../effect/typetest/Crypto.tst.ts | 0 .../effect/typetest/Data.tst.ts | 0 .../effect/typetest/Effect.tst.ts | 21 + .../effect/typetest/Effectable.tst.ts | 0 .../effect/typetest/Equivalence.tst.ts | 0 .../effect/typetest/Fiber.tst.ts | 0 .../effect/typetest/FileSystem.tst.ts | 0 .../effect/typetest/Function.tst.ts | 0 .../effect/typetest/Graph.tst.ts | 0 .../effect/typetest/Iterable.tst.ts | 0 .../effect/typetest/Layer.tst.ts | 0 .../effect/typetest/Match.tst.ts | 0 .../effect/typetest/MigratedSubpaths.tst.ts | 0 .../effect/typetest/Newtype.tst.ts | 0 .../effect/typetest/Optic.tst.ts | 0 .../effect/typetest/Option.tst.ts | 0 .../effect/typetest/Order.tst.ts | 0 .../effect/typetest/Predicate.tst.ts | 0 .../effect/typetest/Queue.tst.ts | 0 .../effect/typetest/Random.tst.ts | 0 .../effect/typetest/Record.tst.ts | 0 .../effect/typetest/Result.tst.ts | 0 .../effect/typetest/Schedule.tst.ts | 4 +- .../effect/typetest/Scope.tst.ts | 0 packages/effect/typetest/SemVer.tst.ts | 23 + .../effect/typetest/Sink.tst.ts | 0 .../effect/typetest/Stream.tst.ts | 21 + packages/effect/typetest/String.tst.ts | 15 + .../effect/typetest/Struct.tst.ts | 0 .../effect/typetest/SynchronizedRef.tst.ts | 0 .../effect/typetest/TestSchema.tst.ts | 0 .../effect/typetest/Tuple.tst.ts | 0 .../effect/typetest/Types.tst.ts | 0 .../effect/typetest/VariantSchema.tst.ts | 0 .../effect/typetest/ai/DecisionModel.tst.ts | 37 +- .../effect/typetest/ai/LanguageModel.tst.ts | 0 packages/effect/typetest/ai/McpClient.tst.ts | 73 + packages/effect/typetest/ai/McpSchema.tst.ts | 12 + .../effect/typetest/ai/McpServer.tst.ts | 4 +- .../effect/typetest/ai/Response.tst.ts | 0 .../effect/typetest/ai/Tool.tst.ts | 0 .../effect/typetest/ai/Toolkit.tst.ts | 0 .../typetest/arbitrary/Arbitrary.tst.ts | 0 .../effect/typetest/cli/Command.tst.ts | 24 + .../effect/typetest/cli/Param.tst.ts | 0 .../effect/typetest/cli/Prompt.tst.ts | 0 .../typetest/cluster/EntityProxyServer.tst.ts | 0 .../typetest/cluster/K8sHttpClient.tst.ts | 0 .../effect/typetest/cluster/Reply.tst.ts | 0 .../effect/typetest/encoding/Base64Url.tst.ts | 8 + .../effect/typetest/encoding/JsonArray.tst.ts | 31 + .../effect/typetest/encoding/Ndjson.tst.ts | 0 packages/effect/typetest/encoding/Tar.tst.ts | 25 + packages/effect/typetest/encoding/Xml.tst.ts | 38 + packages/effect/typetest/encoding/Zip.tst.ts | 35 + .../effect/typetest/eventlog/EventLog.tst.ts | 42 + .../effect/typetest/eventlog/EventRow.tst.ts | 153 + .../effect/typetest/graphql/GraphQL.tst.ts | 0 .../typetest/graphql/GraphQLClient.tst.ts | 0 .../typetest/graphql/GraphQLMiddleware.tst.ts | 0 .../typetest/graphql/GraphQLProtocol.tst.ts | 0 .../typetest/graphql/GraphQLResolver.tst.ts | 0 .../effect/typetest/http/HttpClient.tst.ts | 0 .../typetest/http/HttpMiddleware.tst.ts | 41 + .../effect/typetest/http/HttpPlatform.tst.ts | 0 .../effect/typetest/http/HttpRouter.tst.ts | 0 .../effect/typetest/http/HttpServer.tst.ts | 0 .../effect/typetest/httpapi/HttpApi.tst.ts | 0 .../typetest/httpapi/HttpApiBuilder.tst.ts | 39 +- .../typetest/httpapi/HttpApiClient.tst.ts | 80 + .../typetest/httpapi/HttpApiEndpoint.tst.ts | 27 + .../typetest/httpapi/HttpApiGroup.tst.ts | 0 .../typetest/httpapi/HttpApiMiddleware.tst.ts | 0 .../typetest/httpapi/HttpApiSchema.tst.ts | 0 .../httpapi/OpenApiRepresentation.tst.ts | 0 .../typetest/net/AddressResolver.tst.ts | 0 .../effect/typetest/net/Dns.tst.ts | 0 .../effect/typetest/net/IpInterface.tst.ts | 0 .../effect/typetest/net/IpNetwork.tst.ts | 0 .../effect/typetest/net/NetAddress.tst.ts | 0 .../typetest/reactivity/AsyncResult.tst.ts | 0 .../effect/typetest/reactivity/Atom.tst.ts | 29 +- .../typetest/reactivity/AtomHttpApi.tst.ts | 31 + .../effect/typetest/reactivity/AtomRpc.tst.ts | 15 +- .../typetest/reactivity/Reactivity.tst.ts | 16 + .../typetest/reactivity/ReactivityKeys.tst.ts | 103 + .../effect/typetest/rpc/Rpc.tst.ts | 0 .../effect/typetest/rpc/RpcClient.tst.ts | 0 .../effect/typetest/rpc/RpcGroup.tst.ts | 0 .../effect/typetest/rpc/RpcMessage.tst.ts | 0 .../effect/typetest/rpc/RpcMiddleware.tst.ts | 0 .../effect/typetest/rpc/RpcServer.tst.ts | 0 .../effect/typetest/schema/Array.tst.ts | 0 packages/effect/typetest/schema/Enum.tst.ts | 30 + .../typetest/schema/FromJsonSchema.tst.ts | 0 .../effect/typetest/schema/Graph.tst.ts | 0 .../effect/typetest/schema/Literals.tst.ts | 0 .../effect/typetest/schema/Record.tst.ts | 0 .../effect/typetest/schema/Schema.tst.ts | 16 + .../typetest/schema/SchemaAOTCompiler.tst.ts | 0 .../schema/SchemaAOTCompilerBuild.tst.ts | 0 .../schema/SchemaBuiltInRevivers.tst.ts | 0 .../typetest/schema/SchemaCompiler.tst.ts | 0 .../typetest/schema/SchemaGetter.tst.ts | 0 .../schema/SchemaJsonSchemaConsumer.tst.ts | 0 .../schema/SchemaRepresentation.tst.ts | 0 .../SchemaRepresentationCompilers.tst.ts | 0 .../schema/SchemaRepresentationReviver.tst.ts | 0 .../effect/typetest/schema/Struct.tst.ts | 0 .../effect/typetest/schema/Tuple.tst.ts | 0 .../effect/typetest/schema/Union.tst.ts | 42 +- .../effect/typetest/schema/api.tst.ts | 0 .../effect/typetest/schema/options.tst.ts | 0 .../effect/typetest/schema/toIso.tst.ts | 0 .../effect/typetest/socket/Socket.tst.ts | 0 packages/effect/typetest/sql/SqlClient.tst.ts | 14 + .../effect/typetest/sql/SqlSchema.tst.ts | 0 .../effect/typetest/workflow/Workflow.tst.ts | 0 packages/eslint-codegen-model/CHANGELOG.md | 1434 - .../examples/model-facades.ts | 318 - .../examples/workflow-facades.ts | 184 - packages/eslint-codegen-model/index.js | 3 - packages/eslint-codegen-model/package.json | 75 - packages/eslint-codegen-model/src/cli.ts | 638 - packages/eslint-codegen-model/src/index.ts | 4 - .../eslint-codegen-model/src/js-yaml.d.ts | 3 - .../eslint-codegen-model/src/normalise.ts | 38 - .../eslint-codegen-model/src/oxlint/index.ts | 8 - .../src/oxlint/rules/codegen.ts | 142 - .../src/presets/barrel.ts | 219 - .../eslint-codegen-model/src/presets/index.ts | 6 - .../eslint-codegen-model/src/presets/meta.ts | 48 - .../src/presets/model-facade.ts | 48 - .../eslint-codegen-model/src/presets/model.ts | 194 - .../src/shared/codegen-block.ts | 101 - .../src/shared/native-type-resolver.ts | 110 - .../src/shared/type-resolver.ts | 631 - packages/eslint-codegen-model/tsconfig.json | 14 - packages/eslint-shared-config/CHANGELOG.md | 610 - packages/eslint-shared-config/package.json | 59 - .../eslint-shared-config/src/dprint.jsonc | 36 - .../src/eslint.base.config.mjs | 114 - .../src/eslint.vue.config.mjs | 79 - .../eslint-shared-config/src/oxlintrc.json | 137 - .../src/plugin-effect-app.mjs | 91 - .../test/no-await-effect.test.mts | 204 - .../eslint-shared-config/vitest.config.ts | 9 - packages/infra/.madgerc | 7 - packages/infra/.npmignore | 14 - packages/infra/CHANGELOG.md | 16223 ------------ packages/infra/_check.sh | 3 - packages/infra/docs/cluster-storage.md | 26 - packages/infra/docs/workflow-engine.md | 262 - packages/infra/examples/query.ts | 138 - packages/infra/package.json | 111 - packages/infra/src/CUPS.ts | 196 - packages/infra/src/ClusterCosmos.ts | 1170 - packages/infra/src/ClusterServiceBus.ts | 244 - packages/infra/src/ContextProvider.ts | 165 - packages/infra/src/Emailer/Sendgrid.ts | 161 - packages/infra/src/Emailer/fake.ts | 18 - packages/infra/src/MainFiberSet.ts | 95 - packages/infra/src/QueueMaker/errors.ts | 32 - packages/infra/src/SQL.ts | 1 - packages/infra/src/SQL/Model.ts | 932 - packages/infra/src/Store/Cosmos.ts | 656 - packages/infra/src/Store/Cosmos/query.ts | 531 - packages/infra/src/Store/Disk.ts | 270 - packages/infra/src/Store/Memory.ts | 609 - packages/infra/src/Store/SQL.ts | 805 - packages/infra/src/Store/SQL/Pg.ts | 411 - packages/infra/src/Store/SQL/query.ts | 748 - packages/infra/src/Store/codeFilter.ts | 244 - packages/infra/src/Store/index.test.ts.bak | 112 - packages/infra/src/Store/index.ts | 48 - packages/infra/src/Store/jsonDocument.ts | 143 - packages/infra/src/Store/utils.ts | 345 - packages/infra/src/WorkflowEngineCosmos.ts | 749 - packages/infra/src/WorkflowEngineSqlite.ts | 813 - packages/infra/src/arbs.ts | 40 - packages/infra/src/codec.ts | 9 - packages/infra/src/cosmos-client.ts | 72 - packages/infra/src/errorReporter.ts | 102 - packages/infra/src/errors.ts | 1 - packages/infra/src/fileUtil.ts | 130 - packages/infra/src/index.ts | 2 - .../src/internal/RequestContextMiddleware.ts | 59 - packages/infra/src/internal/auth.ts | 272 - packages/infra/src/internal/events.ts | 77 - packages/infra/src/internal/health.ts | 5 - packages/infra/src/layerUtils.ts | 68 - packages/infra/src/logger.ts | 3 - packages/infra/src/logger/jsonLogger.ts | 35 - packages/infra/src/logger/logFmtLogger.ts | 18 - packages/infra/src/logger/shared.ts | 19 - packages/infra/src/middlewares.ts | 10 - packages/infra/src/mongo-client.ts | 25 - packages/infra/src/otel.ts | 177 - packages/infra/src/rateLimit.ts | 126 - packages/infra/src/redis-client.ts | 133 - packages/infra/src/reportError.ts | 30 - packages/infra/src/routing.ts | 827 - packages/infra/src/routing/middleware.ts | 4 - .../routing/middleware/RouterMiddleware.ts | 27 - .../src/routing/middleware/middleware.ts | 196 - packages/infra/src/routing/schema/jwt.ts | 21 - packages/infra/src/routing/tsort.ts | 56 - packages/infra/src/routing/utils.ts | 48 - packages/infra/src/test.ts | 30 - packages/infra/src/util.ts | 14 - packages/infra/src/vitest.ts | 2 - packages/infra/test/arbs-magnitude.test.ts | 33 - packages/infra/test/arbs.test.ts | 41 - packages/infra/test/auth.test.ts | 101 - packages/infra/test/cluster-cosmos.test.ts | 634 - .../infra/test/cluster-servicebus.test.ts | 180 - packages/infra/test/cluster-sqlite.test.ts | 207 - packages/infra/test/contextProvider.test.ts | 161 - packages/infra/test/controller.test.ts | 425 - .../test/cosmos-container-throughput.test.ts | 80 - packages/infra/test/cosmos-query.test.ts | 368 - packages/infra/test/fixtures.ts | 139 - packages/infra/test/json-lower.test.ts | 129 - packages/infra/test/layerUtils.test.ts | 21 - packages/infra/test/query.test.ts | 2436 -- packages/infra/test/rawQuery.test.ts | 851 - packages/infra/test/repository-ext.test.ts | 456 - .../test/repository-legacy-document.test.ts | 394 - packages/infra/test/requires.test.ts | 182 - packages/infra/test/router-generator.test.ts | 187 - .../test/routing-interruptibility.test.ts | 66 - .../test/rpc-context-map-streaming.test.ts | 265 - .../infra/test/rpc-e2e-invalidation.test.ts | 421 - .../infra/test/rpc-multi-middleware.test.ts | 213 - .../infra/test/rpc-stream-fullstack.test.ts | 307 - packages/infra/test/sql-store.test.ts | 1931 -- packages/infra/test/validateSample.test.ts | 265 - .../infra/test/workflow-engine-cosmos.test.ts | 345 - .../infra/test/workflow-engine-sqlite.test.ts | 297 - packages/infra/tsconfig.examples.json | 15 - packages/infra/tsconfig.json | 12 - packages/infra/tsconfig.src.json | 38 - packages/infra/tsconfig.test.json | 33 - packages/infra/types/modules.d.ts | 0 packages/infra/vitest.config.ts | 5 - .../opentelemetry/CHANGELOG.md | 12 + .../opentelemetry/LICENSE | 0 .../opentelemetry/README.md | 0 .../opentelemetry/package.json | 2 +- .../opentelemetry/src/NodeSdk.ts | 0 .../opentelemetry/src/OtelLogger.ts | 0 .../opentelemetry/src/OtelMetrics.ts | 36 +- .../opentelemetry/src/OtelTracer.ts | 0 .../opentelemetry/src/Resource.ts | 11 +- .../opentelemetry/src/WebSdk.ts | 0 .../opentelemetry/src/index.ts | 0 .../opentelemetry/src/internal/attributes.ts | 0 .../opentelemetry/src/internal/metrics.ts | 18 +- .../opentelemetry/src/internal/utilities.ts | 0 .../opentelemetry/test/OtelLogger.test.ts | 0 .../opentelemetry/test/OtelMetrics.test.ts | 79 +- .../opentelemetry/test/OtelTracer.test.ts | 0 .../opentelemetry/test/Resource.test.ts | 20 + .../opentelemetry/tsconfig.json | 0 .../platform/browser/CHANGELOG.md | 16 + .../platform/browser/LICENSE | 0 .../platform/browser/README.md | 0 .../platform/browser/package.json | 2 +- .../platform/browser/src/BrowserCrypto.ts | 0 .../platform/browser/src/BrowserHttpClient.ts | 43 +- .../browser/src/BrowserKeyValueStore.ts | 0 .../browser/src/BrowserPersistence.ts | 0 .../platform/browser/src/BrowserRuntime.ts | 0 .../platform/browser/src/BrowserSocket.ts | 0 .../platform/browser/src/BrowserStream.ts | 0 .../platform/browser/src/BrowserWorker.ts | 0 .../browser/src/BrowserWorkerRunner.ts | 4 +- .../platform/browser/src/Clipboard.ts | 0 .../platform/browser/src/Geolocation.ts | 0 .../platform/browser/src/IndexedDb.ts | 2 +- .../platform/browser/src/IndexedDbDatabase.ts | 0 .../browser/src/IndexedDbQueryBuilder.ts | 0 .../platform/browser/src/IndexedDbTable.ts | 0 .../platform/browser/src/IndexedDbVersion.ts | 0 .../platform/browser/src/Permissions.ts | 0 .../platform/browser/src/index.ts | 0 .../browser/test/BrowserCrypto.test.ts | 0 .../browser/test/BrowserHttpClient.test.ts | 30 + .../browser/test/BrowserKeyValueStore.test.ts | 0 .../browser/test/BrowserPersistence.test.ts | 0 .../BrowserPersistencePersistedCache.test.ts | 0 .../browser/test/BrowserWorkerRunner.test.ts | 16 +- .../browser/test/IndexedDbDatabase.test.ts | 0 .../test/IndexedDbQueryBuilder.test.ts | 0 .../browser/test/IndexedDbTable.test.ts | 0 .../browser/test/IndexedDbVersion.test.ts | 0 .../platform/browser/test/Permissions.test.ts | 0 .../platform/browser/test/RpcWorker.test.ts | 0 .../platform/browser/test/fixtures/rpc-e2e.ts | 0 .../browser/test/fixtures/rpc-schemas.ts | 0 .../browser/test/fixtures/rpc-worker.ts | 0 .../platform/browser/tsconfig.json | 0 .../platform/browser/vitest.setup.ts | 0 .../platform/bun/CHANGELOG.md | 46 + .../platform/bun/LICENSE | 0 .../platform/bun/README.md | 0 .../platform/bun/benchmark/DatagramSocket.ts | 0 .../platform/bun/package.json | 6 +- .../platform/bun/src/BunAddressResolver.ts | 0 .../bun/src/BunChildProcessSpawner.ts | 0 .../platform/bun/src/BunClusterHttp.ts | 0 .../platform/bun/src/BunClusterSocket.ts | 12 +- .../platform/bun/src/BunCrypto.ts | 0 .../platform/bun/src/BunDatagramSocket.ts | 0 .../platform/bun/src/BunDns.ts | 0 .../platform/bun/src/BunFileSystem.ts | 0 packages/platform/bun/src/BunHttpClient.ts | 105 + .../platform/bun/src/BunHttpPlatform.ts | 0 .../platform/bun/src/BunHttpServer.ts | 63 +- .../platform/bun/src/BunHttpServerRequest.ts | 0 .../platform/bun/src/BunMultipart.ts | 0 .../platform/bun/src/BunPath.ts | 0 .../platform/bun/src/BunRedis.ts | 61 +- .../platform/bun/src/BunRuntime.ts | 0 .../platform/bun/src/BunServices.ts | 0 .../platform/bun/src/BunSink.ts | 0 .../platform/bun/src/BunSocket.ts | 0 .../platform/bun/src/BunSocketServer.ts | 0 .../platform/bun/src/BunStdio.ts | 0 .../platform/bun/src/BunStream.ts | 0 .../platform/bun/src/BunTerminal.ts | 0 .../platform/bun/src/BunWorker.ts | 0 .../platform/bun/src/BunWorkerRunner.ts | 0 .../platform/bun/src/index.ts | 0 .../bun/test/BunAddressResolver.test.ts | 0 .../platform/bun/test/BunClusterHttp.test.ts | 0 .../bun/test/BunDatagramSocket.test.ts | 13 +- .../bun/test/BunDns.integration.test.ts | 0 .../platform/bun/test/BunHttpClient.test.ts | 38 + .../bun/test/BunHttpCompression.test.ts | 0 .../platform/bun/test/BunHttpPlatform.test.ts | 0 .../platform/bun/test/BunHttpServer.test.ts | 196 + .../platform/bun/test/BunMultipart.test.ts | 0 .../platform/bun/test/BunSocket.test.ts | 0 .../platform/bun/test/BunStream.test.ts | 0 .../platform/bun/tsconfig.json | 0 .../platform/deno/CHANGELOG.md | 58 + .../platform/deno/LICENSE | 0 .../platform/deno/README.md | 0 .../platform/deno/benchmark/DatagramSocket.ts | 0 .../platform/deno/package.json | 8 +- .../platform/deno/src/DenoAddressResolver.ts | 0 .../deno/src/DenoChildProcessSpawner.ts | 0 .../platform/deno/src/DenoClusterHttp.ts | 0 .../platform/deno/src/DenoClusterSocket.ts | 31 +- .../platform/deno/src/DenoCrypto.ts | 0 .../platform/deno/src/DenoDatagramSocket.ts | 0 .../platform/deno/src/DenoDns.ts | 0 .../platform/deno/src/DenoFileSystem.ts | 7 + packages/platform/deno/src/DenoHttpClient.ts | 138 + .../platform/deno/src/DenoHttpPlatform.ts | 0 .../platform/deno/src/DenoHttpServer.ts | 108 +- .../deno/src/DenoHttpServerRequest.ts | 0 .../platform/deno/src/DenoKeyValueStore.ts | 0 .../platform/deno/src/DenoMultipart.ts | 0 .../platform/deno/src/DenoPath.ts | 0 .../platform/deno/src/DenoRedis.ts | 0 .../platform/deno/src/DenoRuntime.ts | 9 +- .../platform/deno/src/DenoServices.ts | 0 .../platform/deno/src/DenoSocket.ts | 0 .../platform/deno/src/DenoSocketServer.ts | 0 .../platform/deno/src/DenoStdio.ts | 0 .../platform/deno/src/DenoTerminal.ts | 0 .../platform/deno/src/DenoWorker.ts | 0 .../platform/deno/src/DenoWorkerRunner.ts | 4 +- .../platform/deno/src/index.ts | 0 .../platform/deno/src/internal/error.ts | 1 + .../deno/test/DenoAddressResolver.test.ts | 0 .../deno/test/DenoChildProcessSpawner.test.ts | 0 .../platform/deno/test/DenoCrypto.test.ts | 0 .../deno/test/DenoDatagramSocket.test.ts | 0 .../DenoDatagramSocketUnsupported.test.ts | 0 .../deno/test/DenoDns.integration.test.ts | 0 .../platform/deno/test/DenoFileSystem.test.ts | 0 .../platform/deno/test/DenoHttpClient.test.ts | 56 + .../deno/test/DenoHttpCompression.test.ts | 0 .../deno/test/DenoHttpPlatform.test.ts | 0 .../platform/deno/test/DenoHttpServer.test.ts | 355 +- .../deno/test/DenoKeyValueStore.test.ts | 0 .../platform/deno/test/DenoPath.test.ts | 0 .../deno/test/DenoRedis.integration.test.ts | 0 .../platform/deno/test/DenoSocket.test.ts | 0 .../deno/test/DenoSocketServer.test.ts | 0 .../platform/deno/test/DenoStdio.test.ts | 0 .../platform/deno/test/DenoTerminal.test.ts | 0 .../deno/test/DenoWorkerRunner.test.ts | 0 .../platform/deno/test/RpcWorker.test.ts | 0 .../deno/test/cluster/SocketRunner.test.ts | 59 +- .../deno/test/fixtures/deno-terminal.ts | 0 .../platform/deno/test/fixtures/rpc-e2e.ts | 0 .../deno/test/fixtures/rpc-schemas.ts | 0 .../platform/deno/test/fixtures/rpc-worker.ts | 0 .../platform/deno/test/fixtures/text.txt | 0 .../platform/deno/test/fixtures/tls/ca.pem | 0 .../platform/deno/test/fixtures/tls/cert.pem | 0 .../platform/deno/test/fixtures/tls/key.pem | 0 .../platform/deno/test/internal/error.test.ts | 1 + .../platform/deno/tsconfig.json | 0 .../platform/node-shared/CHANGELOG.md | 26 + .../platform/node-shared/LICENSE | 0 .../platform/node-shared/README.md | 0 .../node-shared/benchmark/DatagramSocket.ts | 0 .../platform/node-shared/benchmark/Socket.ts | 0 .../platform/node-shared/package.json | 6 +- .../node-shared/src/NodeAddressResolver.ts | 0 .../src/NodeChildProcessSpawner.ts | 5 +- .../node-shared/src/NodeClusterSocket.ts | 0 .../platform/node-shared/src/NodeCrypto.ts | 0 .../node-shared/src/NodeDatagramSocket.ts | 0 .../platform/node-shared/src/NodeDns.ts | 0 .../node-shared/src/NodeFileSystem.ts | 24 +- .../node-shared/src/NodeHttpCompression.ts | 0 .../platform/node-shared/src/NodePath.ts | 0 .../platform/node-shared/src/NodeRuntime.ts | 0 packages/platform/node-shared/src/NodeSink.ts | 206 + .../platform/node-shared/src/NodeSocket.ts | 0 .../node-shared/src/NodeSocketServer.ts | 0 .../platform/node-shared/src/NodeStdio.ts | 0 .../platform/node-shared/src/NodeStream.ts | 125 +- .../platform/node-shared/src/NodeTerminal.ts | 24 +- .../platform/node-shared/src/index.ts | 0 .../src/internal/nodeChildProcessSpawner.ts | 0 .../node-shared/src/internal/stream.ts | 42 + .../node-shared/src/internal/utils.ts | 1 + .../test/AddressResolver.test-utils.ts | 0 .../node-shared/test/Dns.test-utils.ts | 0 .../test/HttpPlatform.test-utils.ts | 0 .../test/HttpServerHttp2.test-utils.ts | 144 + .../test/NodeAddressResolver.test.ts | 0 .../test/NodeChildProcessSpawner.test.ts | 92 +- .../test/NodeDatagramSocket.test.ts | 24 +- .../test/NodeDns.integration.test.ts | 0 .../node-shared/test/NodeFileSystem.test.ts | 73 +- .../node-shared/test/NodePath.test.ts | 0 .../node-shared/test/NodeSink.test.ts | 538 + .../node-shared/test/NodeStdio.test.ts | 0 .../node-shared/test/NodeStream.test.ts | 93 + .../node-shared/test/NodeTerminal.test.ts | 8 + .../test/fixtures/buffered-input.ts | 0 .../test/fixtures/helloworld.tar.gz | Bin .../test/fixtures/node-terminal.ts | 30 + .../test/fixtures/process-group.ts | 13 +- .../platform/node-shared/tsconfig.json | 0 .../platform/node/CHANGELOG.md | 74 + .../platform/node/LICENSE | 0 .../platform/node/README.md | 0 .../platform/node/package.json | 13 +- .../platform/node/src/NodeAddressResolver.ts | 0 .../node/src/NodeChildProcessSpawner.ts | 0 .../platform/node/src/NodeClusterHttp.ts | 4 +- .../platform/node/src/NodeClusterSocket.ts | 0 .../platform/node/src/NodeCrypto.ts | 0 .../platform/node/src/NodeDatagramSocket.ts | 0 .../platform/node/src/NodeDns.ts | 0 .../platform/node/src/NodeFileSystem.ts | 0 .../platform/node/src/NodeHttpClient.ts | 316 +- .../node/src/NodeHttpIncomingMessage.ts | 6 +- .../platform/node/src/NodeHttpPlatform.ts | 0 .../platform/node/src/NodeHttpServer.ts | 533 +- .../node/src/NodeHttpServerRequest.ts | 19 +- .../platform/node/src/NodeMultipart.ts | 0 .../platform/node/src/NodeMultipartParser.ts | 9 +- .../platform/node/src/NodePath.ts | 0 .../platform/node/src/NodeRedis.ts | 93 +- .../platform/node/src/NodeRuntime.ts | 0 .../platform/node/src/NodeServices.ts | 0 .../platform/node/src/NodeSink.ts | 0 .../platform/node/src/NodeSocket.ts | 0 .../platform/node/src/NodeSocketServer.ts | 0 .../platform/node/src/NodeStdio.ts | 0 .../platform/node/src/NodeStream.ts | 0 .../platform/node/src/NodeTerminal.ts | 0 .../platform/node/src/NodeWorker.ts | 2 +- .../platform/node/src/NodeWorkerRunner.ts | 0 .../platform/node/src/Undici.ts | 0 .../platform/node/src/index.ts | 0 packages/platform/node/src/internal/http.ts | 11 + .../platform/node/src/internal/upgrade.ts | 42 + .../node/test/GraphQLProtocol.test.ts | 0 .../platform/node/test/HttpApi.test.ts | 338 + .../node/test/HttpStaticServer.test.ts | 25 +- .../test/HttpStaticServerConditional.test.ts | 0 .../platform/node/test/KeyValueStore.test.ts | 0 .../node/test/MultipartParser.test.ts | 7 +- .../platform/node/test/NodeCrypto.test.ts | 0 .../platform/node/test/NodeHttpClient.test.ts | 658 + .../node/test/NodeHttpCompression.test.ts | 0 .../node/test/NodeHttpPlatform.test.ts | 0 .../platform/node/test/NodeHttpServer.test.ts | 820 +- .../platform/node/test/NodeMultipart.test.ts | 21 + .../node/test/NodeRedis.integration.test.ts | 0 .../platform/node/test/NodeSocket.test.ts | 98 +- .../node/test/NodeWorkerRunner.test.ts | 0 .../platform/node/test/OpenApi.test.ts | 21 + .../platform/node/test/RpcServer.test.ts | 64 +- .../test/__snapshots__/HttpApi.test.ts.snap | 6 +- .../cluster-integration/ClusterCron.test.ts | 11 +- .../test/cluster-integration/Entity.test.ts | 0 .../test/cluster-integration/Locks.test.ts | 0 .../cluster-integration/Persistence.test.ts | 0 .../node/test/cluster-integration/README.md | 0 .../cluster-integration/Residency.test.ts | 0 .../test/cluster-integration/Smoke.test.ts | 0 .../StreamDisconnect.test.ts | 0 .../cluster-integration/Transport.test.ts | 0 .../test/cluster-integration/Workflow.test.ts | 0 .../test/cluster-integration/globalSetup.ts | 0 .../node/test/cluster-integration/harness.ts | 0 .../node/test/cluster/MessageStorageTest.ts | 0 .../node/test/cluster/SocketRunner.test.ts | 25 +- .../SqlMessageStorage.integration.test.ts | 97 +- .../SqlRunnerStorage.integration.test.ts | 0 .../fixtures/http-static-server-outside.txt | 0 .../http-static-server/conditional.txt | 0 .../http-static-server/custom/home.html | 0 .../fixtures/http-static-server/file.binx | 0 .../http-static-server/guide/index.html | 0 .../fixtures/http-static-server/hello.txt | 0 .../fixtures/http-static-server/index.html | 0 .../fixtures/http-static-server/range.txt | 0 .../node/test/fixtures/mssql-utils.ts | 0 .../node/test/fixtures/mysql2-utils.ts | 0 .../platform/node/test/fixtures/pg-utils.ts | 23 +- .../platform/node/test/fixtures/rpc-e2e.ts | 0 .../node/test/fixtures/rpc-schemas.ts | 0 .../platform/node/test/fixtures/text.txt | 0 .../platform/node/test/fixtures/tls/cert.pem | 0 .../platform/node/test/fixtures/tls/key.pem | 0 .../node/test/fixtures/worker-runner.ts | 0 .../platform/node/tsconfig.json | 0 .../sql/clickhouse/CHANGELOG.md | 30 + .../sql/clickhouse/LICENSE | 0 .../sql/clickhouse/README.md | 0 .../sql/clickhouse/package.json | 2 +- .../sql/clickhouse/src/ClickhouseClient.ts | 71 +- .../sql/clickhouse/src/ClickhouseMigrator.ts | 0 .../sql/clickhouse/src/index.ts | 0 .../sql/clickhouse/test/Client.test.ts | 101 +- .../test/SqlErrorClassification.test.ts | 24 +- .../sql/clickhouse/tsconfig.json | 0 .../packages => packages}/sql/d1/CHANGELOG.md | 25 + .../packages => packages}/sql/d1/LICENSE | 0 .../packages => packages}/sql/d1/README.md | 0 .../packages => packages}/sql/d1/package.json | 4 +- .../sql/d1/src/D1Client.ts | 2 + .../packages => packages}/sql/d1/src/index.ts | 0 .../sql/d1/test/Client.test.ts | 0 .../sql/d1/test/Resolver.test.ts | 0 .../sql/d1/test/utils.ts | 0 .../sql/d1/tsconfig.json | 0 .../sql/libsql/CHANGELOG.md | 27 + .../packages => packages}/sql/libsql/LICENSE | 0 .../sql/libsql/README.md | 0 .../sql/libsql/package.json | 4 +- .../sql/libsql/src/LibsqlClient.ts | 16 +- .../sql/libsql/src/LibsqlMigrator.ts | 0 .../sql/libsql/src/index.ts | 0 .../libsql/test/Client.integration.test.ts | 60 +- .../libsql/test/Resolver.integration.test.ts | 0 .../sql/libsql/test/util.ts | 0 .../sql/libsql/tsconfig.json | 0 .../sql/mssql/CHANGELOG.md | 31 + .../packages => packages}/sql/mssql/LICENSE | 0 .../packages => packages}/sql/mssql/README.md | 0 .../sql/mssql/package.json | 6 +- .../sql/mssql/src/MssqlClient.ts | 24 +- .../sql/mssql/src/MssqlMigrator.ts | 0 .../sql/mssql/src/Parameter.ts | 0 .../sql/mssql/src/Procedure.ts | 0 .../sql/mssql/src/index.ts | 0 packages/sql/mssql/test/Binary.test.ts | 118 + .../sql/mssql/test/Client.test.ts | 22 + .../test/Persistence.integration.test.ts | 431 + .../mssql/test/SqlErrorClassification.test.ts | 12 +- ...ventLogServerEncrypted.integration.test.ts | 16 + packages/sql/mssql/test/utils.ts | 39 + .../sql/mssql/tsconfig.json | 0 .../sql/mysql2/CHANGELOG.md | 25 + .../packages => packages}/sql/mysql2/LICENSE | 0 .../sql/mysql2/README.md | 0 .../sql/mysql2/package.json | 6 +- .../sql/mysql2/src/MysqlClient.ts | 1 + .../sql/mysql2/src/MysqlMigrator.ts | 0 .../sql/mysql2/src/index.ts | 0 .../mysql2/test/Client.integration.test.ts | 0 .../test/KeyValueStore.integration.test.ts | 0 .../sql/mysql2/test/Model.integration.test.ts | 0 .../test/MysqlClient.integration.test.ts | 0 .../test/Persistence.integration.test.ts | 0 .../test/SqlErrorClassification.test.ts | 12 +- ...ventLogServerEncrypted.integration.test.ts | 16 + ...ntLogServerUnencrypted.integration.test.ts | 0 .../sql/mysql2/test/utils.ts | 10 + .../sql/mysql2/tsconfig.json | 0 .../packages => packages}/sql/pg/CHANGELOG.md | 35 + .../packages => packages}/sql/pg/LICENSE | 0 .../packages => packages}/sql/pg/README.md | 0 .../sql/pg/benchmark/PgClient.ts | 0 .../sql/pg/benchmark/PgCodec.ts | 0 .../sql/pg/benchmark/README.md | 0 .../packages => packages}/sql/pg/package.json | 8 +- .../sql/pg/src/PgAuth.ts | 0 .../sql/pg/src/PgClient.ts | 104 +- .../sql/pg/src/PgConnection.ts | 257 +- packages/sql/pg/src/PgMigrator.ts | 270 + .../sql/pg/src/PgPool.ts | 153 +- .../sql/pg/src/PgProtocol.ts | 0 .../sql/pg/src/PgTypes.ts | 0 .../packages => packages}/sql/pg/src/index.ts | 0 packages/sql/pg/src/internal/config.ts | 234 + .../sql/pg/src/internal/connection.ts | 0 .../sql/pg/src/internal/password.ts | 0 .../sql/pg/src/internal/sqlError.ts | 0 .../sql/pg/test/Client.integration.test.ts | 85 +- .../sql/pg/test/ContainerTeardown.test.ts | 151 + .../pg/test/KeyValueStore.integration.test.ts | 0 .../pg/test/Persistence.integration.test.ts | 13 +- .../sql/pg/test/PgAuth.test.ts | 0 .../pg/test/PgConnection.in-process.test.ts | 0 .../pg/test/PgConnection.integration.test.ts | 28 +- .../sql/pg/test/PgConnection.test.ts | 0 .../pg/test/PgMigrator.integration.test.ts | 0 packages/sql/pg/test/PgMigrator.test.ts | 185 + .../sql/pg/test/PgPool.in-process.test.ts | 303 + .../sql/pg/test/PgPool.integration.test.ts | 21 +- .../sql/pg/test/PgProtocol.test.ts | 0 .../sql/pg/test/PgTypes.test.ts | 0 .../pg/test/SqlErrorClassification.test.ts | 0 .../test/SqlEventJournal.integration.test.ts | 334 + ...ventLogServerEncrypted.integration.test.ts | 16 + ...ntLogServerUnencrypted.integration.test.ts | 10 + .../StartupParameters.integration.test.ts | 0 .../sql/pg/test/TransactionAcquire.test.ts | 0 .../sql/pg/test/fixtures/goldens.ts | 0 .../sql/pg/test/fixtures/regenerate.ts | 0 .../sql/pg/test/utils.ts | 60 +- .../sql/pg/tsconfig.json | 0 packages/sql/pg/typetest/PgClient.tst.ts | 23 + .../sql/pglite/CHANGELOG.md | 25 + .../packages => packages}/sql/pglite/LICENSE | 0 .../sql/pglite/README.md | 0 .../sql/pglite/package.json | 4 +- .../sql/pglite/src/PgliteClient.ts | 1 + .../sql/pglite/src/PgliteMigrator.ts | 0 .../sql/pglite/src/index.ts | 0 .../sql/pglite/test/Client.test.ts | 12 +- .../sql/pglite/test/Json.test.ts | 0 .../sql/pglite/test/Migrator.test.ts | 0 .../sql/pglite/test/PersistedQueue.test.ts | 0 .../test/SqlErrorClassification.test.ts | 12 +- .../sql/pglite/test/SqlRunnerStorage.test.ts | 35 + .../sql/pglite/test/Transaction.test.ts | 27 +- .../sql/pglite/tsconfig.json | 0 .../sql/sqlite-bun/CHANGELOG.md | 25 + .../sql/sqlite-bun/LICENSE | 0 .../sql/sqlite-bun/README.md | 0 .../sql/sqlite-bun/package.json | 4 +- .../sql/sqlite-bun/src/SqliteClient.ts | 4 +- .../sql/sqlite-bun/src/SqliteMigrator.ts | 0 .../sql/sqlite-bun/src/index.ts | 0 .../sql/sqlite-bun/test/Client.test.ts | 0 .../sql/sqlite-bun/tsconfig.json | 0 .../sql/sqlite-do/CHANGELOG.md | 23 + .../sql/sqlite-do/LICENSE | 0 .../sql/sqlite-do/README.md | 0 .../sql/sqlite-do/package.json | 4 +- .../sql/sqlite-do/src/SqliteClient.ts | 1 + .../sql/sqlite-do/src/SqliteMigrator.ts | 0 .../sql/sqlite-do/src/index.ts | 0 .../sql/sqlite-do/test/Client.test.ts | 0 .../sql/sqlite-do/test/Miniflare.test.ts | 0 .../sqlite-do/test/SqlEventJournal.test.ts | 66 + .../sqlite-do/test/fixtures/eventJournal.ts | 48 + .../sqlite-do/test/fixtures/transaction.ts | 0 .../sql/sqlite-do/tsconfig.json | 0 .../sql/sqlite-node/CHANGELOG.md | 25 + .../sql/sqlite-node/LICENSE | 0 .../sql/sqlite-node/README.md | 0 .../sql/sqlite-node/package.json | 2 +- .../sql/sqlite-node/src/SqliteClient.ts | 4 +- .../sql/sqlite-node/src/SqliteMigrator.ts | 0 .../sql/sqlite-node/src/index.ts | 0 .../sql/sqlite-node/test/Client.test.ts | 81 + .../sqlite-node/test/KeyValueStore.test.ts | 0 .../sql/sqlite-node/test/Persistence.test.ts | 24 + .../sql/sqlite-node/test/Resolver.test.ts | 0 .../sqlite-node/test/SqlEventJournal.test.ts | 896 + .../test/SqlEventLogServerEncrypted.test.ts | 121 +- .../test/SqlEventLogServerUnencrypted.test.ts | 44 + .../sqlite-node/test/SqlModelServices.test.ts | 0 .../sqlite-node/test/SqliteMigrator.test.ts | 21 + .../sql/sqlite-node/tsconfig.json | 0 .../sql/sqlite-react-native/CHANGELOG.md | 23 + .../sql/sqlite-react-native/LICENSE | 0 .../sql/sqlite-react-native/README.md | 0 .../sql/sqlite-react-native/package.json | 2 +- .../sqlite-react-native/src/SqliteClient.ts | 1 + .../sqlite-react-native/src/SqliteMigrator.ts | 0 .../sql/sqlite-react-native/src/index.ts | 0 .../sqlite-react-native/test/Client.test.ts | 0 .../sql/sqlite-react-native/tsconfig.json | 0 .../sql/sqlite-wasm/CHANGELOG.md | 25 + .../sql/sqlite-wasm/LICENSE | 0 .../sql/sqlite-wasm/README.md | 0 .../sql/sqlite-wasm/package.json | 2 +- .../sql/sqlite-wasm/src/OpfsWorker.ts | 0 .../sql/sqlite-wasm/src/SqliteClient.ts | 5 +- .../sql/sqlite-wasm/src/SqliteMigrator.ts | 0 .../sql/sqlite-wasm/src/index.ts | 0 .../sqlite-wasm/src/internal/opfsWorker.ts | 0 .../sql/sqlite-wasm/src/sqlite-wasm.d.ts | 0 .../sql/sqlite-wasm/test/Client.test.ts | 11 +- .../sql/sqlite-wasm/test/Memory.test.ts | 0 .../sql/sqlite-wasm/tsconfig.json | 0 .../tools/ai-codegen/codegen.schema.json | 0 .../tools/ai-codegen/package.json | 0 .../tools/ai-codegen/src/Config.ts | 0 .../tools/ai-codegen/src/Discovery.ts | 0 .../tools/ai-codegen/src/Generator.ts | 0 .../tools/ai-codegen/src/Glob.ts | 0 .../tools/ai-codegen/src/PostProcess.ts | 0 .../tools/ai-codegen/src/SpecFetcher.ts | 0 .../tools/ai-codegen/src/bin.ts | 0 .../tools/ai-codegen/src/main.ts | 0 .../tools/ai-codegen/test/Generator.test.ts | 17 +- .../ai-codegen/test/OpenAiGeneration.test.ts | 0 .../test/OpenRouterTranscriptions.test.ts | 0 .../tools/ai-codegen/tsconfig.json | 0 .../tools/ai-docgen/package.json | 0 .../tools/ai-docgen/src/Glob.ts | 0 .../tools/ai-docgen/src/main.ts | 0 .../tools/ai-docgen/tsconfig.json | 0 .../tools/api-diff/README.md | 0 .../tools/api-diff/package.json | 2 +- .../tools/api-diff/src/Annotations.ts | 0 .../tools/api-diff/src/ApiDiff.ts | 0 .../tools/api-diff/src/Cli.ts | 0 .../tools/api-diff/src/Diff.ts | 0 .../tools/api-diff/src/Discovery.ts | 0 .../tools/api-diff/src/Error.ts | 0 .../tools/api-diff/src/Json.ts | 0 .../tools/api-diff/src/MigrationDoc.ts | 0 .../tools/api-diff/src/Model.ts | 0 .../tools/api-diff/src/Report.ts | 0 .../tools/api-diff/src/Snapshot.ts | 0 .../tools/api-diff/src/Worktrees.ts | 0 .../tools/api-diff/src/bin.ts | 0 .../tools/api-diff/test/Annotations.test.ts | 0 .../tools/api-diff/test/Cli.test.ts | 0 .../tools/api-diff/test/Diff.test.ts | 0 .../tools/api-diff/test/Discovery.test.ts | 0 .../tools/api-diff/test/Error.test.ts | 0 .../tools/api-diff/test/MigrationDoc.test.ts | 0 .../tools/api-diff/test/Report.test.ts | 0 .../tools/api-diff/test/Snapshot.test.ts | 0 .../tools/api-diff/test/Worktrees.test.ts | 0 .../tools/api-diff/test/utils.ts | 0 .../tools/api-diff/tsconfig.json | 0 .../tools/bundle/README.md | 0 .../bundle/fixtures/arbitrary-combinators.ts | 0 .../tools/bundle/fixtures/basic.ts | 0 .../tools/bundle/fixtures/batching.ts | 0 .../tools/bundle/fixtures/brand.ts | 0 .../tools/bundle/fixtures/cache.ts | 0 .../tools/bundle/fixtures/config.ts | 0 .../tools/bundle/fixtures/differ.ts | 0 .../tools/bundle/fixtures/http-client.ts | 0 .../tools/bundle/fixtures/http-router.ts | 0 .../tools/bundle/fixtures/logger.ts | 0 .../tools/bundle/fixtures/metric.ts | 0 .../tools/bundle/fixtures/optic.ts | 0 .../tools/bundle/fixtures/pubsub.ts | 0 .../tools/bundle/fixtures/queue.ts | 0 .../tools/bundle/fixtures/schedule.ts | 0 .../bundle/fixtures/schema-bigdecimal.ts | 0 .../tools/bundle/fixtures/schema-binary.ts | 0 .../tools/bundle/fixtures/schema-class.ts | 0 .../fixtures/schema-fromJsonSchemaDocument.ts | 0 .../schema-representation-roundtrip.ts | 0 .../fixtures/schema-string-transformation.ts | 9 +- .../tools/bundle/fixtures/schema-string.ts | 0 .../fixtures/schema-template-literal.ts | 0 .../bundle/fixtures/schema-toArbitrary.ts | 0 .../bundle/fixtures/schema-toCodeDocument.ts | 0 .../bundle/fixtures/schema-toCodecJson.ts | 0 .../bundle/fixtures/schema-toEquivalence.ts | 0 .../bundle/fixtures/schema-toFormatter.ts | 0 .../fixtures/schema-toJsonSchemaDocument.ts | 0 .../fixtures/schema-toRepresentation.ts | 0 .../tools/bundle/fixtures/schema.ts | 0 .../tools/bundle/fixtures/stm.ts | 0 .../tools/bundle/fixtures/stream.ts | 0 .../tools/bundle/package.json | 2 +- .../tools/bundle/src/Cli.ts | 0 .../tools/bundle/src/Fixtures.ts | 0 .../tools/bundle/src/Plugins.ts | 0 .../tools/bundle/src/Reporter.ts | 0 .../tools/bundle/src/Rollup.ts | 0 .../tools/bundle/src/bin.ts | 0 .../bundle/test/HttpRouterEsbuild.test.ts | 0 .../tools/bundle/test/Plugins.test.ts | 0 .../tools/bundle/tsconfig.fixtures.json | 0 .../tools/bundle/tsconfig.json | 0 .../tools/bundle/tsconfig.src.json | 0 .../tools/docgen/CHANGELOG.md | 9 + .../tools/docgen/LICENSE | 0 .../tools/docgen/README.md | 0 .../tools/docgen/package.json | 4 +- .../tools/docgen/schema.json | 0 .../tools/docgen/src/CLI.ts | 0 .../tools/docgen/src/Checker.ts | 108 +- .../tools/docgen/src/Configuration.ts | 143 +- .../tools/docgen/src/Core.ts | 423 +- .../tools/docgen/src/Domain.ts | 0 .../tools/docgen/src/Parser.ts | 465 +- .../tools/docgen/src/Printer.ts | 96 +- .../tools/docgen/src/bin.ts | 0 .../tools/docgen/src/index.ts | 0 .../docgen/src/internal/markdown-toc.d.ts | 0 .../tools/docgen/test/Checker.test.ts | 24 +- .../tools/docgen/test/Configuration.test.ts | 0 .../tools/docgen/test/Core.test.ts | 8 +- .../tools/docgen/test/Parser.test.ts | 10 +- .../docgen/test/fixtures/invalid-json.txt | 0 .../tools/docgen/tsconfig.json | 0 .../tools/doctest/CHANGELOG.md | 8 + .../tools/doctest/README.md | 0 .../tools/doctest/package.json | 6 +- .../tools/doctest/src/Plugin.ts | 0 .../tools/doctest/src/Protocol.ts | 0 .../tools/doctest/src/Runner.ts | 0 .../tools/doctest/src/Runtime.ts | 0 .../tools/doctest/src/Source.ts | 0 .../tools/doctest/src/Transform.ts | 0 .../tools/doctest/src/index.ts | 0 .../tools/doctest/test/Plugin.test.ts | 0 .../tools/doctest/test/Protocol.test.ts | 0 .../tools/doctest/test/Runtime.test.ts | 0 .../tools/doctest/test/Source.test.ts | 0 .../tools/doctest/test/Transform.test.ts | 0 .../test/fixtures/markdown/docs/helper.ts | 0 .../test/fixtures/markdown/docs/typed.md | 0 .../test/fixtures/markdown/docs/typed.mdx | 0 .../test/fixtures/markdown/src/typed.ts | 0 .../test/fixtures/markdown/vitest.config.ts | 0 .../tools/doctest/tsconfig.json | 0 packages/tools/graphql-generator/CHANGELOG.md | 29 + .../tools/graphql-generator/LICENSE | 0 .../tools/graphql-generator/README.md | 0 .../tools/graphql-generator/package.json | 2 +- .../tools/graphql-generator/src/Config.ts | 0 .../tools/graphql-generator/src/Generator.ts | 0 .../tools/graphql-generator/src/bin.ts | 0 .../graphql-generator/src/internal/Ast.ts | 0 .../graphql-generator/src/internal/Cli.ts | 0 .../src/internal/Diagnostic.ts | 0 .../graphql-generator/src/internal/Emitter.ts | 0 .../src/internal/Generate.ts | 0 .../graphql-generator/src/internal/Glob.ts | 0 .../src/internal/IntrospectionReader.ts | 0 .../graphql-generator/src/internal/Lexer.ts | 0 .../graphql-generator/src/internal/Parser.ts | 0 .../graphql-generator/src/internal/Printer.ts | 0 .../src/internal/SchemaModel.ts | 0 .../src/internal/SdlReader.ts | 0 .../src/internal/Validate.ts | 0 .../graphql-generator/src/internal/Watch.ts | 0 .../tools/graphql-generator/src/main.ts | 0 .../graphql-generator/test/Config.test.ts | 0 .../graphql-generator/test/Diagnostic.test.ts | 0 .../test/Generator.abstract.test.ts | 19 +- .../test/Generator.bindings.test.ts | 0 .../test/Generator.diagnostics.test.ts | 0 .../test/Generator.directives.test.ts | 0 .../test/Generator.mapping.test.ts | 0 .../test/Generator.naming.test.ts | 0 .../test/Generator.output.test.ts | 0 .../test/Generator.runtime.abstract.test.ts | 0 .../test/Generator.runtime.test.ts | 0 .../test/Generator.snapshots.test.ts | 0 .../test/Generator.template.test.ts | 0 .../tools/graphql-generator/test/Glob.test.ts | 0 .../test/GraphQLGeneratorCli.test.ts | 0 .../graphql-generator/test/Lexical.test.ts | 0 .../test/Parser.executable.test.ts | 0 .../test/Parser.typeSystem.test.ts | 0 .../graphql-generator/test/Printer.test.ts | 0 .../test/SchemaReaders.fixtures.test.ts | 0 .../graphql-generator/test/Validate.test.ts | 0 .../graphql-generator/test/Watch.test.ts | 25 +- .../test/fixtures/github/README.md | 0 .../github/documents/fragments.graphql | 0 .../fixtures/github/documents/issues.graphql | 0 .../test/fixtures/github/documents/scalars.ts | 0 .../github/documents/timeline.graphql | 0 .../github/operations/issue-timeline.graphql | 0 .../fixtures/github/operations/search.graphql | 0 .../test/fixtures/github/schema.graphql | 0 .../fixtures/introspection/modern.graphql | 0 .../test/fixtures/introspection/modern.json | 0 .../test/fixtures/sdl/extensions.graphql | 0 .../test/fixtures/sdl/one-of.graphql | 0 .../test/fixtures/sdl/subscriptions.graphql | 0 .../test/fixtures/subscriptions/rooms.graphql | 0 .../generated/subscriptions/rooms.graphql.ts | 0 .../generated/subscriptions/shared.graphql.ts | 0 .../tools/graphql-generator/test/utils/ast.ts | 0 .../graphql-generator/test/utils/generator.ts | 87 +- .../graphql-generator/test/utils/model.ts | 0 .../tools/graphql-generator/tsconfig.json | 0 .../typetest/SelectionShape.tst.ts | 0 .../tools/jsdocs/package.json | 2 +- .../tools/jsdocs/src/Jsdocs.ts | 0 .../tools/jsdocs/src/bin.ts | 0 .../tools/jsdocs/test/jsdocs.test.ts | 0 .../tools/jsdocs/tsconfig.json | 0 .../tools/npm-promote/README.md | 0 .../tools/npm-promote/package.json | 4 +- .../tools/npm-promote/src/Cli.ts | 0 .../tools/npm-promote/src/GitHub.ts | 0 .../tools/npm-promote/src/NpmPromote.ts | 0 .../tools/npm-promote/src/NpmPromoteError.ts | 0 .../tools/npm-promote/src/Process.ts | 0 .../tools/npm-promote/src/Release.ts | 0 .../tools/npm-promote/src/TrackingIssue.ts | 0 .../tools/npm-promote/src/WebsiteDeploy.ts | 0 .../tools/npm-promote/src/Workspace.ts | 0 .../tools/npm-promote/src/bin.ts | 0 .../tools/npm-promote/test/NpmPromote.test.ts | 0 .../tools/npm-promote/test/Release.test.ts | 0 .../tools/npm-promote/test/utils.ts | 0 .../tools/npm-promote/tsconfig.json | 0 .../tools/openapi-generator/CHANGELOG.md | 25 + .../tools/openapi-generator/LICENSE | 0 .../tools/openapi-generator/README.md | 0 .../tools/openapi-generator/package.json | 6 +- .../src/HttpApiTransformer.ts | 7 + .../src/JsonSchemaGenerator.ts | 71 +- .../openapi-generator/src/OpenApiGenerator.ts | 351 +- .../openapi-generator/src/OpenApiPatch.ts | 0 .../src/OpenApiTransformer.ts | 179 +- .../openapi-generator/src/ParsedOperation.ts | 12 + .../tools/openapi-generator/src/Utils.ts | 0 .../tools/openapi-generator/src/bin.ts | 0 .../tools/openapi-generator/src/main.ts | 0 .../test/JsonSchemaGenerator.test.ts | 0 .../JsonSchemaGeneratorRepresentation.test.ts | 0 .../test/OpenApiGenerator.test.ts | 835 +- .../test/OpenApiGeneratorCli.test.ts | 10 +- .../test/OpenApiPatch.test.ts | 0 .../test/OpenApiTransformer.test.ts | 33 + .../openapi-generator/test/Utils.test.ts | 0 .../test/fixtures/cli-basic-spec.json | 0 .../test/fixtures/cli-warning-spec.json | 10 +- .../test/fixtures/patches/invalid-op.json | 0 .../test/fixtures/patches/missing-path.json | 0 .../test/fixtures/patches/missing-value.json | 0 .../test/fixtures/patches/valid-add.json | 0 .../test/fixtures/patches/valid-multiple.json | 0 .../test/fixtures/patches/valid-patch.yaml | 0 .../test/fixtures/patches/valid-remove.json | 0 .../test/fixtures/patches/valid-replace.json | 0 .../tools/openapi-generator/tsconfig.json | 0 .../tools/oxc/oxlintrc.json | 0 .../tools/oxc/package.json | 4 +- .../tools/oxc/src/oxlint/index.ts | 0 .../src/oxlint/rules/no-bigint-literals.ts | 0 .../rules/no-import-from-barrel-package.ts | 0 .../oxlint/rules/no-js-extension-imports.ts | 0 .../oxlint/rules/no-opaque-instance-fields.ts | 0 .../src/oxlint/rules/no-unused-internal.ts | 17 + .../no-import-from-barrel-package.test.ts | 0 .../oxc/test/no-js-extension-imports.test.ts | 0 .../test/no-opaque-instance-fields.test.ts | 0 .../tools/oxc/test/no-unused-internal.test.ts | 23 + .../tools/oxc/test/utils.ts | 0 .../tools/oxc/tsconfig.json | 0 .../tools/utils/package.json | 0 .../tools/utils/src/Codegen.ts | 3 +- .../tools/utils/src/Glob.ts | 0 .../tools/utils/src/bin.ts | 0 .../tools/utils/src/commands/codegen.ts | 0 .../tools/utils/test/Codegen.test.ts | 31 + .../tools/utils/tsconfig.json | 0 .../packages => packages}/vitest/CHANGELOG.md | 10 + .../packages => packages}/vitest/LICENSE | 0 .../packages => packages}/vitest/README.md | 23 +- .../packages => packages}/vitest/package.json | 4 +- .../packages => packages}/vitest/src/index.ts | 36 +- .../vitest/src/internal/internal.ts | 91 +- .../packages => packages}/vitest/src/utils.ts | 0 .../test/arbitrary-configuration.test.ts | 0 .../vitest/test/fixtures.test.ts | 11 +- .../vitest/test/index.test.ts | 86 +- .../vitest/test/isolation.test.ts | 0 .../vitest/test/layer-concurrency.test.ts | 0 .../vitest/test/nested-isolation.test.ts | 0 packages/vitest/test/timeout.test.ts | 275 + .../vitest/tsconfig.json | 0 .../vitest/typetest/index.tst.ts | 5 + packages/vue-components/.prettierrc | 7 - packages/vue-components/.storybook/main.ts | 14 - packages/vue-components/.storybook/preview.ts | 52 - .../vue-components/.storybook/tsconfig.json | 9 - .../vue-components/.vscode/extensions.json | 7 - packages/vue-components/CHANGELOG.md | 6608 ----- packages/vue-components/README.md | 233 - .../__tests__/FixedNuxtErrorBoundary.test.ts | 213 - .../__tests__/IntegerValidation.test.ts | 49 - .../__tests__/OmegaForm.test.ts | 205 - .../OmegaForm/CustomFilterFieldSchema.test.ts | 37 - .../OmegaForm/DateValidation.test.ts | 68 - .../OmegaForm/DecimalSeparatorInput.test.ts | 115 - .../OmegaForm/DecodeTransformFallback.test.ts | 151 - .../OmegaForm/DefaultValuesPriority.test.ts | 23 - .../__tests__/OmegaForm/Defaults.test.ts | 551 - .../OmegaForm/Defaults.values.test.ts | 114 - .../OmegaForm/DialogBlockingI18n.test.ts | 82 - .../OmegaForm/EmailFormatDetection.test.ts | 11 - .../OmegaForm/EmptyStringValidation.test.ts | 77 - .../__tests__/OmegaForm/GeneralErrors.test.ts | 59 - .../OmegaForm/IndexedInputErrors.test.ts | 82 - .../OmegaForm/InputAttributes.test.ts | 221 - .../OmegaForm/InputReactivity.test.ts | 49 - .../__tests__/OmegaForm/InputRegistry.test.ts | 95 - .../OmegaForm/IntersectionError.test.ts | 223 - .../__tests__/OmegaForm/Meta.test.ts | 40 - .../NullableFieldInitialization.test.ts | 184 - .../NullableNestedStructValidation.test.ts | 148 - .../__tests__/OmegaForm/OmegaAutoGen.test.ts | 331 - .../__tests__/OmegaForm/OptionalKey.test.ts | 62 - .../__tests__/OmegaForm/RedactedMeta.test.ts | 181 - .../SelectMultipleLocalization.test.ts | 123 - .../__tests__/OmegaForm/SubmitEffect.test.ts | 142 - .../OmegaForm/SubmitErrorClear.test.ts | 59 - .../OmegaForm/TaggedUnionBehavior.test.ts | 251 - .../TaggedUnionLegacyWarning.test.ts | 31 - .../OmegaForm/TaggedUnionNested.test.ts | 54 - .../OmegaForm/TaggedUnionRequired.test.ts | 81 - .../OmegaForm/TaggedUnionRoot.test.ts | 77 - .../OmegaForm/TransformationSchema.test.ts | 84 - .../OmegaForm/TransformedStruct.test.ts | 174 - .../OmegaForm/UnregisteredFieldError.test.ts | 94 - .../WithDefaultConstructorPersistency.test.ts | 107 - .../createUseFormWithCustomInput.test.ts | 374 - .../__tests__/OmegaFormTest.vue | 62 - .../__tests__/OmegaIntlProvider.vue | 12 - .../migration/schema-v4-migration.test.ts | 703 - packages/vue-components/__tests__/setup.ts | 16 - .../__tests__/test-array-meta.test.ts | 55 - .../__tests__/test-nullor-duplicate.test.ts | 42 - .../__tests__/test-union-meta.test.ts | 17 - .../vue-components/__tests__/tsconfig.json | 9 - .../plans/2026-04-27-omegaform-refactor.md | 2655 -- .../2026-04-27-omegaform-refactor-design.md | 242 - packages/vue-components/env.d.ts | 8 - packages/vue-components/eslint.config.mjs | 20 - packages/vue-components/package.json | 95 - packages/vue-components/src/assets/.keep | 0 .../src/components/CommandButton.vue | 161 - .../vue-components/src/components/Dialog.vue | 58 - .../src/components/FixedNuxtErrorBoundary.vue | 112 - .../src/components/OmegaForm/InputProps.ts | 100 - .../src/components/OmegaForm/OmegaArray.vue | 76 - .../src/components/OmegaForm/OmegaAutoGen.vue | 105 - .../OmegaForm/OmegaErrorsInternal.vue | 238 - .../components/OmegaForm/OmegaFormInput.vue | 41 - .../src/components/OmegaForm/OmegaInput.vue | 95 - .../OmegaForm/OmegaInputVuetify.vue | 370 - .../OmegaForm/OmegaInternalInput.vue | 207 - .../components/OmegaForm/OmegaTaggedUnion.vue | 106 - .../OmegaForm/OmegaTaggedUnionInternal.vue | 41 - .../src/components/OmegaForm/OmegaWrapper.vue | 73 - .../src/components/OmegaForm/blockDialog.ts | 73 - .../OmegaForm/createUseFormWithCustomInput.ts | 70 - .../OmegaForm/decimalSeparatorInput.ts | 38 - .../src/components/OmegaForm/errors.ts | 139 - .../src/components/OmegaForm/getOmegaStore.ts | 31 - .../src/components/OmegaForm/hocs.ts | 19 - .../src/components/OmegaForm/index.ts | 27 - .../src/components/OmegaForm/inputs.ts | 22 - .../src/components/OmegaForm/meta/checks.ts | 107 - .../components/OmegaForm/meta/createMeta.ts | 140 - .../src/components/OmegaForm/meta/defaults.ts | 267 - .../src/components/OmegaForm/meta/redacted.ts | 71 - .../src/components/OmegaForm/meta/types.ts | 78 - .../src/components/OmegaForm/meta/walker.ts | 248 - .../OmegaForm/onMountedWithCleanup.ts | 22 - .../src/components/OmegaForm/persistency.ts | 247 - .../src/components/OmegaForm/submit.ts | 131 - .../src/components/OmegaForm/types.ts | 801 - .../src/components/OmegaForm/useOmegaForm.ts | 200 - .../components/OmegaForm/useRegisterField.ts | 17 - .../OmegaForm/validation/localized.ts | 242 - .../vue-components/src/components/index.ts | 4 - .../vue-components/src/components/style.css | 3 - packages/vue-components/src/index.ts | 20 - packages/vue-components/src/reset.css | 52 - packages/vue-components/src/utils/index.ts | 31 - .../stories/Commands.stories.ts | 46 - .../vue-components/stories/Commands/One.vue | 122 - .../stories/Commands/components.ts | 1 - .../stories/Commands/helpers.ts | 82 - .../stories/Commands/useEffectRouter.ts | 73 - .../vue-components/stories/Dialog.stories.ts | 34 - .../stories/Dialog/AsyncBlocking.vue | 47 - .../stories/Dialog/AsyncBlockingContent.vue | 91 - .../stories/Dialog/SimpleBlocking.vue | 49 - .../stories/Dialog/SimpleBlockingContent.vue | 30 - .../stories/Dialog/UploadBlocking.vue | 47 - .../stories/Dialog/UploadBlockingContent.vue | 105 - .../stories/FixedNuxtErrorBoundary.stories.ts | 18 - .../stories/FixedNuxtErrorBoundary/Demo.vue | 92 - .../stories/OmegaForm.stories.ts | 448 - .../stories/OmegaForm/Array.vue | 118 - .../stories/OmegaForm/AutoGeneration.vue | 74 - .../stories/OmegaForm/Booleans.vue | 42 - .../stories/OmegaForm/Clearable.vue | 25 - .../stories/OmegaForm/ComplexForm.vue | 147 - .../stories/OmegaForm/CustomInput.vue | 62 - .../OmegaForm/CustomInputClassName.vue | 53 - .../stories/OmegaForm/CustomLabelSlot.vue | 146 - .../vue-components/stories/OmegaForm/Date.vue | 35 - .../OmegaForm/DecodeTransformFallback.vue | 48 - .../stories/OmegaForm/Defaults.vue | 208 - .../OmegaForm/DialogBlockingExamples.vue | 76 - .../stories/OmegaForm/EmailForm.vue | 46 - .../stories/OmegaForm/EnterSubmitRepro.vue | 78 - .../stories/OmegaForm/FormTaggedUnion.vue | 116 - .../stories/OmegaForm/IndexedTable.vue | 79 - .../stories/OmegaForm/InputRegistry.vue | 109 - .../OmegaForm/IntegerValidationGerman.vue | 83 - .../stories/OmegaForm/IntersectionExample.vue | 55 - .../vue-components/stories/OmegaForm/Meta.vue | 92 - .../vue-components/stories/OmegaForm/Null.vue | 75 - .../stories/OmegaForm/Nullable.vue | 85 - .../OmegaForm/NullableNestedStruct.vue | 69 - .../stories/OmegaForm/NumberInput.vue | 68 - .../OmegaForm/OmitConstructorDefaults.vue | 131 - .../stories/OmegaForm/OptionalKey.vue | 142 - .../stories/OmegaForm/PersistencyForm.vue | 74 - ...rogrammaticallyHandleSubmitCheckErrors.vue | 39 - .../stories/OmegaForm/RatingInput.vue | 49 - .../stories/OmegaForm/Redacted.vue | 80 - .../OmegaForm/RootLevelTaggedUnion.vue | 127 - .../OmegaForm/RootLevelTaggedUnionLegacy.vue | 118 - .../stories/OmegaForm/SetErrorOnSubmit.vue | 35 - .../stories/OmegaForm/SimpleForm.vue | 43 - .../OmegaForm/SimpleFormVuetifyDefault.vue | 37 - .../stories/OmegaForm/SumExample.vue | 43 - .../stories/OmegaForm/Tanstack.vue | 210 - .../stories/OmegaForm/Union.vue | 77 - .../stories/OmegaForm/UsingOmegaForm.vue | 284 - .../OmegaForm/WindowExitPrevention.vue | 280 - .../OmegaForm/WithDefaultConstructor.vue | 53 - .../createUseFormWIthCustomInput.vue | 45 - .../stories/OmegaForm/form.Input.vue | 129 - packages/vue-components/stories/README.md | 29 - packages/vue-components/stories/tsconfig.json | 9 - packages/vue-components/tsconfig.build.json | 11 - packages/vue-components/tsconfig.json | 35 - .../vue-components/types/vuetify-shims.d.ts | 6 - packages/vue-components/vite.config.mts | 89 - packages/vue-components/vitest.config.mts | 19 - packages/vue/.madgerc | 7 - packages/vue/.npmignore | 14 - packages/vue/.prettierignore | 6 - packages/vue/CHANGELOG.md | 14447 ----------- packages/vue/docs/atom-query-api-redesign.md | 207 - packages/vue/docs/mutation-command-atoms.md | 40 - packages/vue/docs/query-key-invalidation.md | 107 - packages/vue/examples/streamMutation.ts | 72 - packages/vue/package.json | 102 - packages/vue/src/atomQuery.ts | 600 - packages/vue/src/commander.ts | 3435 --- packages/vue/src/confirm.ts | 34 - packages/vue/src/dependencyMetadata.ts | 84 - packages/vue/src/errorReporter.ts | 108 - packages/vue/src/form.ts | 501 - packages/vue/src/index.ts | 8 - packages/vue/src/internal/tanstackQuery.ts | 308 - packages/vue/src/intl.ts | 12 - packages/vue/src/lib.ts | 115 - packages/vue/src/liveQueryInvalidation.ts | 138 - packages/vue/src/makeClient.ts | 1314 - packages/vue/src/makeContext.ts | 34 - packages/vue/src/makeIntl.ts | 77 - packages/vue/src/makeUseCommand.ts | 29 - packages/vue/src/mutate.ts | 516 - packages/vue/src/query.ts | 940 - packages/vue/src/queryLifetime.ts | 74 - packages/vue/src/routeParams.ts | 55 - packages/vue/src/runtime.ts | 1 - packages/vue/src/suspense.ts | 43 - packages/vue/src/toast.ts | 1 - packages/vue/src/withToast.ts | 1 - packages/vue/test/Mutation.test.ts | 917 - .../vue/test/dependencyInvalidation.test.ts | 444 - .../vue/test/form-validation-errors.test.ts | 163 - packages/vue/test/form.test.ts | 280 - .../vue/test/inactiveInvalidation.test.ts | 172 - .../vue/test/interrupt-refetch-repro.test.ts | 625 - packages/vue/test/lib.test.ts | 240 - .../vue/test/liveQueryInvalidation.test.ts | 108 - packages/vue/test/makeClient.test.ts | 492 - packages/vue/test/query-span.test.ts | 92 - packages/vue/test/queryOptions.test.ts | 88 - packages/vue/test/streamFinal.test.ts | 64 - packages/vue/test/streamFn.test.ts | 457 - packages/vue/test/stubs.ts | 310 - packages/vue/test/suspendWaiting.test.ts | 142 - packages/vue/test/suspense-regression.test.ts | 168 - packages/vue/test/suspense.test.ts | 160 - packages/vue/tsconfig.examples.json | 20 - packages/vue/tsconfig.json | 15 - packages/vue/tsconfig.src.json | 36 - packages/vue/tsconfig.test.json | 33 - packages/vue/vitest.config.ts | 18 - patches/.gitkeep | 0 patches/@ben_12__eslint-plugin-dprint.patch | 14 - patches/@changesets__cli@2.31.0.patch | 47 - .../@changesets__get-github-info@1.0.1.patch | 0 patches/@sendgrid__mail.patch | 23 - patches/@tanstack__query-core.patch | 203 - .../ts-plugin-sort-import-suggestions.patch | 45 - patches/typescript.patch | 160 - pnpm-lock.yaml | 21545 +++++++--------- pnpm-workspace.yaml | 64 +- .../.agents/skills/changesets/authoring.md | 46 - repos/effect/.changeset/config.json | 56 - repos/effect/.envrc | 1 - repos/effect/.gitignore | 41 - repos/effect/.oxlintrc.json | 37 - repos/effect/.vscode/extensions.json | 6 - repos/effect/.vscode/settings.json | 51 - repos/effect/README.md | 98 - repos/effect/flake.lock | 27 - repos/effect/flake.nix | 24 - .../annotations/effect__platform__Path.yaml | 3 - .../annotations/effect__sql__SqlClient.yaml | 12 - .../effect__sql__SqlEventLogServer.yaml | 6 - repos/effect/migration/error-handling.md | 106 - repos/effect/package.json | 84 - .../packages/ai/cloudflare/CHANGELOG.md | 41 - .../packages/effect/src/ai/DecisionModel.ts | 461 - .../effect/src/cluster/ClusterCron.ts | 166 - .../effect/src/cluster/ClusterMetrics.ts | 118 - .../effect/src/eventlog/EventJournal.ts | 839 - .../effect/src/eventlog/EventLogEncryption.ts | 175 - .../effect/src/eventlog/SqlEventJournal.ts | 397 - .../eventlog/SqlEventLogServerEncrypted.ts | 347 - .../effect/src/http-api/internal/path.ts | 61 - .../MultipartParser/internal/contentType.ts | 103 - .../src/http/internal/preResponseHandler.ts | 19 - .../packages/effect/src/internal/version.ts | 2 - .../effect/packages/effect/test/Exit.test.ts | 12 - .../effect/test/RequestResolver.test.ts | 63 - .../test/devtools/DevToolsClient.test.ts | 70 - .../effect/test/eventlog/EventJournal.test.ts | 114 - .../effect/test/eventlog/EventLog.test.ts | 184 - .../test/eventlog/EventLogMessage.test.ts | 34 - .../test/eventlog/EventLogRemote.test.ts | 214 - .../effect/test/http/FetchHttpClient.test.ts | 25 - .../packages/effect/test/http/Headers.test.ts | 107 - .../observability/OtlpSerialization.test.ts | 59 - .../test/persistence/PersistedQueueSqlTest.ts | 59 - .../effect/test/sql/SqlClient.test.ts | 311 - .../platform/bun/src/BunHttpClient.ts | 10 - .../platform/deno/src/DenoHttpClient.ts | 10 - .../platform/node-shared/src/NodeSink.ts | 139 - .../node-shared/test/NodeSink.test.ts | 282 - .../platform/node/test/NodeHttpClient.test.ts | 298 - .../packages/sql/mssql/test/Binary.test.ts | 43 - .../test/Persistence.integration.test.ts | 29 - .../effect/packages/sql/pg/src/PgMigrator.ts | 122 - .../packages/sql/pg/test/PgMigrator.test.ts | 86 - .../sql/pg/test/PgPool.in-process.test.ts | 94 - .../test/SqlEventJournal.integration.test.ts | 58 - .../sqlite-node/test/SqlEventJournal.test.ts | 142 - .../tools/graphql-generator/CHANGELOG.md | 21 - .../packages/vitest/test/timeout.test.ts | 116 - repos/effect/pnpm-lock.yaml | 12301 --------- repos/effect/pnpm-workspace.yaml | 46 - repos/effect/scripts/codemods/jsdoc.ts | 79 - repos/effect/tsconfig.base.json | 67 - repos/effect/vitest.config.ts | 224 - repos/effect/vitest.setup.ts | 4 - .../effect/scratchpad => scratchpad}/index.ts | 0 .../scratchpad => scratchpad}/package.json | 0 .../scratchpad => scratchpad}/tsconfig.json | 4 +- scripts/agent-gh.mjs | 60 - scripts/agent-pr-ready.mjs | 65 - scripts/agent-pre-push.mjs | 140 - .../scripts => scripts}/bundle-analyze.sh | 0 .../bundle-compare-selected.sh | 0 .../scripts => scripts}/bundle-compare.sh | 0 .../scripts => scripts}/check-dist-types.mjs | 46 +- .../effect/scripts => scripts}/circular.mjs | 0 scripts/clean-dist.sh | 41 - {repos/effect/scripts => scripts}/clean.mjs | 0 {repos/effect/scripts => scripts}/codemod.mjs | 0 scripts/codemods/jsdoc.test.ts | 51 + scripts/codemods/jsdoc.ts | 65 + scripts/convert_patch.js | 16 - .../scripts => scripts}/copy-ai-docs.mjs | 0 .../effect/scripts => scripts}/effect-bot.mjs | 0 scripts/extract.sh | 26 - scripts/hooks/post-checkout.test.sh | 84 - scripts/install-git-hooks.mjs | 71 - scripts/lib/agent-env.mjs | 26 - scripts/lib/agent-gh-policy.mjs | 155 - scripts/lib/agent-pr-state.mjs | 119 - scripts/lib/agent-ship-gate-cache.mjs | 121 - scripts/mergeTsConfig.mjs | 49 - .../scripts => scripts}/package-scalar.mjs | 0 .../scripts => scripts}/package-swagger.mjs | 0 .../effect/scripts => scripts}/package.json | 0 scripts/rewrite-dist-declaration-imports.mjs | 37 - .../set-strip-internal.mjs | 0 .../scripts => scripts}/setup-agents.mjs | 0 scripts/sort.js | 31 - scripts/stagePublishPackage.mjs | 157 - scripts/sync-effect-subtree.js | 8 - .../effect/scripts => scripts}/tsconfig.json | 4 +- {repos/effect/scripts => scripts}/version.mjs | 0 .../scripts => scripts}/worktree-setup.sh | 0 t3.json | 11 - tsconfig.all.json | 29 - tsconfig.base.json | 126 +- repos/effect/tsconfig.json => tsconfig.json | 0 tsconfig.lib.json | 14 - ...ig.packages.json => tsconfig.packages.json | 0 tsconfig.plugins.json | 27 - ...tsconfig.tests.json => tsconfig.tests.json | 15 +- repos/effect/tstyche.json => tstyche.json | 0 vite.config.base.ts | 40 - vitest.config.ts | 230 +- repos/effect/vitest.docs.ts => vitest.docs.ts | 0 vitest.setup.ts | 1 + wiki/Home.md | 3 - wiki/ResourceClients.md | 41 - wiki/bag-of-tricks.md | 95 - .../effect\342\200\220app\342\200\220cli.md" | 344 - wiki/error-reporter-migration.md | 174 - wiki/repository-derived-query-invalidation.md | 291 - 3505 files changed, 73278 insertions(+), 182721 deletions(-) rename {repos/effect/.agents => .agents}/AGENTS.md (100%) rename {repos/effect/.agents => .agents}/skills/ai-docs/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/bundle-analysis/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/changesets/SKILL.md (91%) create mode 100644 .agents/skills/changesets/authoring.md rename {repos/effect/.agents => .agents}/skills/ci-maintenance/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/ci-maintenance/privileged-workflows.md (100%) rename {repos/effect/.agents => .agents}/skills/dependency-maintenance/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/dependency-maintenance/coordinated-upgrades.md (100%) rename {repos/effect/.agents => .agents}/skills/dependency-maintenance/manifest-roles.md (100%) rename {repos/effect/.agents => .agents}/skills/effect-development/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/jsdocs/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/jsdocs/categories.md (100%) rename {repos/effect/.agents => .agents}/skills/jsdocs/declarations.md (100%) rename {repos/effect/.agents => .agents}/skills/jsdocs/examples.md (100%) rename {repos/effect/.agents => .agents}/skills/migration-guidance/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/migration-guidance/annotations.md (100%) rename {repos/effect/.agents => .agents}/skills/migration-guidance/generation.md (100%) rename {repos/effect/.agents => .agents}/skills/package-development/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/package-development/dependencies.md (100%) rename {repos/effect/.agents => .agents}/skills/package-development/publishing.md (100%) rename {repos/effect/.agents => .agents}/skills/package-development/registration.md (100%) rename {repos/effect/.agents => .agents}/skills/performance-analysis/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/performance-analysis/runtime.md (100%) rename {repos/effect/.agents => .agents}/skills/performance-analysis/types.md (100%) rename {repos/effect/.agents => .agents}/skills/pull-requests/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/test-development/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/test-development/displayed-types.md (100%) rename {repos/effect/.agents => .agents}/skills/test-development/runtime.md (100%) rename {repos/effect/.agents => .agents}/skills/test-development/types.md (100%) rename {repos/effect/.agents => .agents}/skills/vendored-assets/SKILL.md (100%) rename {repos/effect/.agents => .agents}/skills/vendored-assets/browser-assets.md (100%) delete mode 100644 .changeset/README.md delete mode 100644 .changeset/node-24-22-1.md delete mode 100644 .changeset/update-effect-4.0.5.md delete mode 100644 .claude/settings.json delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/humanlog.sh delete mode 100644 .devcontainer/usepnpm.sh delete mode 100644 .githooks/README.md delete mode 100755 .githooks/post-checkout delete mode 100755 .githooks/pre-commit delete mode 100755 .githooks/pre-push delete mode 100644 .github/FUNDING.yml rename {repos/effect/.github => .github}/actions/setup/action.yaml (92%) delete mode 100644 .github/npm-trusted-publishing.md rename {repos/effect/.github => .github}/workflows/ai-codegen.yml (100%) rename {repos/effect/.github => .github}/workflows/bundle-comment.yml (100%) rename {repos/effect/.github => .github}/workflows/check.yml (96%) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/claude.yml rename {repos/effect/.github => .github}/workflows/cluster.yml (100%) rename {repos/effect/.github => .github}/workflows/effect-bot.yml (100%) rename {repos/effect/.github => .github}/workflows/npm-promote.yml (100%) delete mode 100644 .github/workflows/publish-snapshot.yml delete mode 100644 .github/workflows/publish-wiki.yml rename {repos/effect/.github => .github}/workflows/release-queue.yml (95%) delete mode 100644 .github/workflows/release.yaml rename {repos/effect/.github => .github}/workflows/release.yml (100%) rename {repos/effect/.github => .github}/workflows/snapshot.yml (100%) delete mode 100644 .ncurc.json delete mode 100644 .npmrc delete mode 100644 .nvmrc delete mode 100644 .specs/Update-to-latest-Effect.md delete mode 100644 .specs/changeset-publishing.md delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/model.code-snippets delete mode 100644 .vscode/service.code-snippets delete mode 100644 .vscode/tasks.json delete mode 100644 AGENTS.md delete mode 100644 CLAUDE.md rename repos/effect/LICENSE => LICENSE (100%) rename repos/effect/LLMS.md => LLMS.md (100%) rename repos/effect/MIGRATION.md => MIGRATION.md (100%) rename {repos/effect/ai-docs => ai-docs}/README.md (100%) rename {repos/effect/ai-docs => ai-docs}/package.json (97%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/01_basics/01_effect-gen.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/01_basics/02_effect-fn.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/01_basics/10_creating-effects.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/01_basics/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/02_schema/10_schema-basics.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/02_schema/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/03_services/01_service.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/03_services/10_reference.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/03_services/20_layer-composition.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/03_services/20_layer-unwrap.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/03_services/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/04_errors/01_error-handling.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/04_errors/10_catch-tags.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/04_errors/20_reason-errors.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/04_errors/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/05_resources/10_acquire-release.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/05_resources/20_layer-side-effects.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/05_resources/30_layer-map.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/05_resources/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/06_running/10_run-main.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/06_running/20_layer-launch.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/06_running/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/07_pubsub/10_pubsub.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/01_effect/07_pubsub/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/03_stream/10_creating-streams.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/03_stream/20_consuming-streams.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/03_stream/30_encoding.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/03_stream/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/04_integration/10_managed-runtime.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/04_integration/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/05_batching/10_request-resolver.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/05_batching/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/06_schedule/10_schedules.ts (92%) rename {repos/effect/ai-docs => ai-docs}/src/06_schedule/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/07_datetime/10_creating-and-formatting.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/07_datetime/20_time-zones.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/07_datetime/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/08_observability/10_logging.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/08_observability/20_otlp-tracing.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/08_observability/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/09_testing/10_effect-tests.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/09_testing/20_layer-tests.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/09_testing/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/10_predicate/01_basics.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/10_predicate/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/40_sql/10_basics.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/40_sql/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/50_http-client/10_basics.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/50_http-client/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/10_basics.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/20_testing.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/api/Api.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/api/Authorization.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/api/System.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/api/Users.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/domain/User.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/domain/UserErrors.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/server/Authorization.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/server/Users.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/fixtures/server/Users/http.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/51_http-server/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/52_graphql/10_basics.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/52_graphql/20_testing.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/52_graphql/30_batching.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/52_graphql/40_aliased-batching.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/52_graphql/50_errors-and-retries.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/52_graphql/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/60_child-process/10_working-with-child-processes.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/60_child-process/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/70_cli/10_basics.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/70_cli/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/71_ai/10_language-model.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/71_ai/20_tools.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/71_ai/30_chat.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/71_ai/fixtures/domain/LaunchPlan.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/71_ai/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/80_cluster/10_entities.ts (100%) rename {repos/effect/ai-docs => ai-docs}/src/80_cluster/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/src/index.md (100%) rename {repos/effect/ai-docs => ai-docs}/tsconfig.json (83%) delete mode 100644 assets/dev/effect-app-apple-touch-180.png rename repos/effect/deno.json => deno.json (100%) rename repos/effect/dprint.json => dprint.json (100%) delete mode 100644 dprint.jsonc rename repos/effect/jsdocs.config.json => jsdocs.config.json (100%) delete mode 100644 lint-staged.config.js rename {repos/effect/migration => migration}/annotations/README.md (100%) rename {repos/effect/migration => migration}/annotations/effect__Arbitrary.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Array.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__BigDecimal.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__BigInt.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Brand.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Cache.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Cause.yaml (98%) rename {repos/effect/migration => migration}/annotations/effect__Channel.yaml (92%) rename {repos/effect/migration => migration}/annotations/effect__ChildExecutorDecision.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Chunk.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Clock.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Config.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ConfigError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ConfigProvider.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ConfigProviderPathPatch.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Console.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Context.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Cron.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Data.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__DateTime.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__DefaultServices.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Deferred.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Differ.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Duration.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Effect.yaml (89%) rename {repos/effect/migration => migration}/annotations/effect__Effectable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Either.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Encoding.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Equal.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Equivalence.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ExecutionPlan.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ExecutionStrategy.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Exit.yaml (93%) rename {repos/effect/migration => migration}/annotations/effect__FastCheck.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Fiber.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberHandle.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberId.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberRefs.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberRefsPatch.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberSet.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__FiberStatus.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Function.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__GlobalValue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Graph.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__GroupBy.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Hash.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__HashMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__HashSet.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Inspectable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Iterable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__JSONSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__KeyedPool.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Layer.yaml (97%) rename {repos/effect/migration => migration}/annotations/effect__LayerMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__List.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__LogLevel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__LogSpan.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Logger.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Mailbox.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ManagedRuntime.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Match.yaml (67%) rename {repos/effect/migration => migration}/annotations/effect__MergeDecision.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MergeState.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MergeStrategy.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Metric.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricBoundaries.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricHook.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricKey.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricKeyType.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricLabel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricPair.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricPolling.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricRegistry.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MetricState.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Micro.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ModuleVersion.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MutableHashMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MutableHashSet.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MutableList.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MutableQueue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__MutableRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Number.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Option.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Order.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Ordering.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ParseResult.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__PartitionedSemaphore.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Pipeable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Pool.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Predicate.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Pretty.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__PubSub.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Queue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Random.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RateLimiter.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RcMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RcRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Readable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Record.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RedBlackTree.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Redacted.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Ref.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Reloadable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Request.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RequestBlock.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RequestResolver.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Resource.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Runtime.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RuntimeFlags.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__RuntimeFlagsPatch.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__STM.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Schedule.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ScheduleDecision.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ScheduleInterval.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ScheduleIntervals.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Scheduler.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Schema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__SchemaAST.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Scope.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ScopedCache.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ScopedRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Secret.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__SingleProducerAsyncInput.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Sink.yaml (92%) rename {repos/effect/migration => migration}/annotations/effect__SortedMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__SortedSet.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Stream.yaml (90%) rename {repos/effect/migration => migration}/annotations/effect__StreamEmit.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__StreamHaltStrategy.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Streamable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Struct.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Subscribable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__SubscriptionRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Supervisor.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Symbol.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__SynchronizedRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TArray.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TDeferred.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TPriorityQueue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TPubSub.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TQueue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TRandom.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TReentrantLock.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TSemaphore.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TSet.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TSubscriptionRef.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Take.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestAnnotation.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestAnnotationMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestAnnotations.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestClock.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestContext.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestLive.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestServices.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__TestSized.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Tracer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Trie.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Tuple.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Types.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__UpstreamPullRequest.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__UpstreamPullStrategy.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__Utils.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-amazon-bedrock__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-anthropic__AnthropicClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-anthropic__AnthropicConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-anthropic__AnthropicTool.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-anthropic__Generated.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-anthropic__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-google.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-google__Generated.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-google__GoogleClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-google__GoogleConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-google__GoogleLanguageModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-google__GoogleTool.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-google__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__Generated.yaml (94%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__OpenAiClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__OpenAiConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__OpenAiLanguageModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__OpenAiTelemetry.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__OpenAiTokenizer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__OpenAiTool.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openai__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openrouter__Generated.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openrouter__OpenRouterClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openrouter__OpenRouterConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai-openrouter__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__AiError.yaml (72%) rename {repos/effect/migration => migration}/annotations/effect__ai__Chat.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__EmbeddingModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__IdGenerator.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__LanguageModel.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__McpSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__McpServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__Model.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__Prompt.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__Response.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__Telemetry.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__Tool.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__Toolkit.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__ai__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__Args.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__AutoCorrect.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__BuiltInOptions.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__CliApp.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__CliConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__Command.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__CommandDescriptor.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__CommandDirective.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__ConfigFile.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__HelpDoc.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__HelpDoc__Span.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__Options.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__Primitive.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__Prompt.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__Usage.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__ValidationError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cli__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__ClusterCron.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__ClusterError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__ClusterSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__ClusterWorkflowEngine.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__DeliverAt.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Entity.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__EntityAddress.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__EntityId.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__EntityProxy.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__EntityProxyServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__EntityResource.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Envelope.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__HttpRunner.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__K8sHttpClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__MachineId.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Message.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__MessageStorage.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Reply.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Runner.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__RunnerAddress.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__RunnerHealth.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__RunnerServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__RunnerStorage.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Runners.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__ShardId.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Sharding.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__ShardingConfig.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__ShardingRegistrationEvent.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__SingleRunner.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Singleton.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__SingletonAddress.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__Snowflake.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__SocketRunner.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__SqlMessageStorage.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__SqlRunnerStorage.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__TestRunner.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__cluster__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__DevTools.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__DevTools__Client.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__DevTools__Domain.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__DevTools__Server.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Event.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__EventGroup.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__EventJournal.yaml (57%) rename {repos/effect/migration => migration}/annotations/effect__experimental__EventLog.yaml (53%) create mode 100644 migration/annotations/effect__experimental__EventLogEncryption.yaml rename {repos/effect/migration => migration}/annotations/effect__experimental__EventLogRemote.yaml (79%) rename {repos/effect/migration => migration}/annotations/effect__experimental__EventLogServer.yaml (58%) rename {repos/effect/migration => migration}/annotations/effect__experimental__EventLogServer__Cloudflare.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Machine.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Machine__Procedure.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Machine__ProcedureList.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Machine__SerializableProcedureList.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__PersistedCache.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__PersistedQueue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__PersistedQueue__Redis.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Persistence.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Persistence__Lmdb.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Persistence__Redis.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__RateLimiter.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__RateLimiter__Redis.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Reactivity.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__RequestResolver.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__Sse.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__VariantSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__experimental__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__Logger.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__Metrics.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__NodeSdk.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__Otlp.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__OtlpLogger.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__OtlpMetrics.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__OtlpResource.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__OtlpTracer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__Resource.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__Tracer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__WebSdk.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__opentelemetry__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-browser__BrowserHttpClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-browser__BrowserWorker.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-browser__BrowserWorkerRunner.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-browser__Clipboard.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-browser__Geolocation.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-browser__Permissions.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-browser__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunCommandExecutor.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunContext.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunHttpPlatform.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunHttpServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunHttpServerRequest.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunKeyValueStore.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunSink.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunSocket.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunSocketServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunStream.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunTerminal.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__BunWorker.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-bun__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeMultipart.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeSink.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeSocket.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeStream.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node-shared__NodeTerminal.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeCommandExecutor.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeContext.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeHttpClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeHttpServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeKeyValueStore.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeTerminal.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__NodeWorker.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__Undici.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform-node__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__ChannelSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Command.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__CommandExecutor.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Cookies.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Effectify.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Error.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Etag.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__FetchHttpClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__FileSystem.yaml (92%) rename {repos/effect/migration => migration}/annotations/effect__platform__Headers.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApi.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiBuilder.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiEndpoint.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiGroup.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiMiddleware.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiScalar.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiSecurity.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApiSwagger.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpApp.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpBody.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpClientError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpClientRequest.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpClientResponse.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpIncomingMessage.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpLayerRouter.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpMethod.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpMiddleware.yaml (84%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpMultiplex.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpPlatform.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpRouter.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpServerError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpServerRequest.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpServerRespondable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__HttpServerResponse.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__KeyValueStore.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__MsgPack.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Multipart.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Ndjson.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__OpenApi.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__OpenApiJsonSchema.yaml (100%) create mode 100644 migration/annotations/effect__platform__Path.yaml rename {repos/effect/migration => migration}/annotations/effect__platform__PlatformConfigProvider.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__PlatformLogger.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Runtime.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Socket.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__SocketServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Template.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Terminal.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Transferable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__Url.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__UrlParams.yaml (92%) rename {repos/effect/migration => migration}/annotations/effect__platform__Worker.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__WorkerError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__WorkerRunner.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__platform__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer-ansi.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer-ansi__Ansi.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer-ansi__AnsiDoc.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer-ansi__Color.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer-ansi__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__Doc.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__DocStream.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__DocTree.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__Flatten.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__Layout.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__Optimize.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__PageWidth.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__printer__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__Rpc.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcClientError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcGroup.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcMessage.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcMiddleware.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcSerialization.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcServer.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__RpcTest.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__rpc__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-clickhouse__ClickhouseClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-clickhouse__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-d1__D1Client.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-d1__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-drizzle__Mysql.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-drizzle__Pg.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-drizzle__Sqlite.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-kysely__Kysely.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-kysely__Mssql.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-kysely__Mysql.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-kysely__Pg.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-kysely__Sqlite.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-kysely__patch.types.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-libsql__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-mssql__MssqlClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-mssql__Parameter.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-mssql__Procedure.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-mssql__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-mysql2__MysqlClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-mysql2__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-pg__PgClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-pg__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-bun__SqliteClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-bun__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-do__SqliteClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-do__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-node__SqliteClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-node__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-react-native__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql-sqlite-wasm__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__Migrator.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__Migrator__FileSystem.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__Model.yaml (100%) create mode 100644 migration/annotations/effect__sql__SqlClient.yaml rename {repos/effect/migration => migration}/annotations/effect__sql__SqlConnection.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__SqlError.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__SqlEventJournal.yaml (100%) create mode 100644 migration/annotations/effect__sql__SqlEventLogServer.yaml rename {repos/effect/migration => migration}/annotations/effect__sql__SqlPersistedQueue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__SqlResolver.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__SqlSchema.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__Statement.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__sql__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Alternative.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Applicative.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Bicovariant.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Bounded.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Chainable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Contravariant.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Coproduct.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Covariant.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Filterable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__FlatMap.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Foldable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Invariant.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Monad.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Monoid.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Of.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Pointed.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Product.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__SemiAlternative.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__SemiApplicative.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__SemiCoproduct.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__SemiProduct.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Semigroup.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__Traversable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__TraversableFilterable.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Array.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__BigInt.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Boolean.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Duration.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Effect.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Either.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Identity.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Micro.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Number.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Option.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Ordering.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Predicate.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Record.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__String.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__data__Tuple.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__typeclass__index.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__vitest__index.yaml (96%) rename {repos/effect/migration => migration}/annotations/effect__vitest__utils.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__Activity.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__DurableClock.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__DurableDeferred.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__DurableQueue.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__DurableRateLimiter.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__Workflow.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__WorkflowEngine.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__WorkflowProxy.yaml (100%) rename {repos/effect/migration => migration}/annotations/effect__workflow__WorkflowProxyServer.yaml (100%) rename {repos/effect/migration => migration}/cause.md (100%) rename {repos/effect/migration => migration}/equality.md (100%) create mode 100644 migration/error-handling.md rename {repos/effect/migration => migration}/fiber-keep-alive.md (100%) rename {repos/effect/migration => migration}/fiberref.md (100%) rename {repos/effect/migration => migration}/forking.md (100%) rename {repos/effect/migration => migration}/generators.md (100%) rename {repos/effect/migration => migration}/layer-memoization.md (58%) rename {repos/effect/migration => migration}/runtime.md (100%) rename {repos/effect/migration => migration}/schema.md (99%) rename {repos/effect/migration => migration}/scope.md (100%) rename {repos/effect/migration => migration}/services.md (100%) rename {repos/effect/migration => migration}/v3-to-v4.md (97%) rename {repos/effect/migration => migration}/yieldable.md (100%) delete mode 100644 old.patch rename {repos/effect/packages => packages}/ai/anthropic/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/ai/anthropic/README.md (100%) rename {repos/effect/packages => packages}/ai/anthropic/codegen.yml (100%) rename {repos/effect/packages => packages}/ai/anthropic/package.json (98%) rename {repos/effect/packages => packages}/ai/anthropic/src/AnthropicClient.ts (99%) rename {repos/effect/packages => packages}/ai/anthropic/src/AnthropicConfig.ts (100%) rename {repos/effect/packages => packages}/ai/anthropic/src/AnthropicError.ts (100%) rename {repos/effect/packages => packages}/ai/anthropic/src/AnthropicLanguageModel.ts (99%) rename {repos/effect/packages => packages}/ai/anthropic/src/AnthropicTelemetry.ts (100%) rename {repos/effect/packages => packages}/ai/anthropic/src/AnthropicTool.ts (100%) rename {repos/effect/packages => packages}/ai/anthropic/src/Generated.ts (100%) rename {repos/effect/packages => packages}/ai/anthropic/src/index.ts (100%) rename {repos/effect/packages => packages}/ai/anthropic/src/internal/errors.ts (84%) rename {repos/effect/packages => packages}/ai/anthropic/src/internal/utilities.ts (100%) rename {repos/effect/packages => packages}/ai/anthropic/test/AnthropicClient.test.ts (89%) rename {repos/effect/packages => packages}/ai/anthropic/test/AnthropicLanguageModel.test.ts (87%) rename {repos/effect/packages => packages}/ai/anthropic/tsconfig.json (100%) rename {repos/effect/packages => packages}/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts (100%) create mode 100644 packages/ai/cloudflare/CHANGELOG.md rename {repos/effect/packages => packages}/ai/cloudflare/README.md (89%) rename {repos/effect/packages => packages}/ai/cloudflare/package.json (98%) rename {repos/effect/packages => packages}/ai/cloudflare/src/CloudflareClient.ts (86%) rename {repos/effect/packages => packages}/ai/cloudflare/src/CloudflareDecisionModel.ts (100%) rename {repos/effect/packages => packages}/ai/cloudflare/src/CloudflareSchema.ts (100%) rename {repos/effect/packages => packages}/ai/cloudflare/src/index.ts (100%) rename {repos/effect/packages => packages}/ai/cloudflare/src/internal/errors.ts (86%) rename {repos/effect/packages => packages}/ai/cloudflare/test/CloudflareClient.test.ts (88%) rename {repos/effect/packages => packages}/ai/cloudflare/test/CloudflareDecisionModel.test.ts (100%) rename {repos/effect/packages => packages}/ai/cloudflare/tsconfig.json (100%) rename {repos/effect/packages => packages}/ai/cloudflare/typetest/CloudflareDecisionModel.tst.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/ai/openai-compat/README.md (100%) rename {repos/effect/packages => packages}/ai/openai-compat/package.json (98%) rename {repos/effect/packages => packages}/ai/openai-compat/src/OpenAiClient.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/src/OpenAiConfig.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/src/OpenAiEmbeddingModel.ts (95%) rename {repos/effect/packages => packages}/ai/openai-compat/src/OpenAiError.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/src/OpenAiLanguageModel.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/src/OpenAiTelemetry.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/src/index.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/src/internal/errors.ts (88%) rename {repos/effect/packages => packages}/ai/openai-compat/src/internal/utilities.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/test/OpenAiClient.test.ts (98%) rename {repos/effect/packages => packages}/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts (95%) rename {repos/effect/packages => packages}/ai/openai-compat/test/OpenAiLanguageModel.test.ts (99%) rename {repos/effect/packages => packages}/ai/openai-compat/test/OpenAiTelemetry.test.ts (100%) rename {repos/effect/packages => packages}/ai/openai-compat/tsconfig.json (100%) rename {repos/effect/packages => packages}/ai/openai-compat/typetest/OpenAiConfig.tst.ts (100%) rename {repos/effect/packages => packages}/ai/openai/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/ai/openai/README.md (100%) rename {repos/effect/packages => packages}/ai/openai/codegen.yaml (100%) rename {repos/effect/packages => packages}/ai/openai/package.json (98%) rename {repos/effect/packages => packages}/ai/openai/src/Generated.ts (100%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiClient.ts (100%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiClientGenerated.ts (100%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiConfig.ts (100%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiDecisionModel.ts (89%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiEmbeddingModel.ts (95%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiError.ts (92%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiLanguageModel.ts (99%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiSchema.ts (98%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiTelemetry.ts (100%) rename {repos/effect/packages => packages}/ai/openai/src/OpenAiTool.ts (100%) rename {repos/effect/packages => packages}/ai/openai/src/index.ts (100%) rename {repos/effect/packages => packages}/ai/openai/src/internal/errors.ts (90%) rename {repos/effect/packages => packages}/ai/openai/src/internal/utilities.ts (100%) rename {repos/effect/packages => packages}/ai/openai/test/OpenAiClient.test.ts (93%) rename {repos/effect/packages => packages}/ai/openai/test/OpenAiDecisionModel.test.ts (89%) rename {repos/effect/packages => packages}/ai/openai/test/OpenAiEmbeddingModel.test.ts (82%) rename {repos/effect/packages => packages}/ai/openai/test/OpenAiLanguageModel.test.ts (98%) rename {repos/effect/packages => packages}/ai/openai/test/OpenAiSchema.test.ts (97%) rename {repos/effect/packages => packages}/ai/openai/test/OpenAiTelemetry.test.ts (100%) rename {repos/effect/packages => packages}/ai/openai/tsconfig.json (100%) rename {repos/effect/packages => packages}/ai/openai/typetest/OpenAiDecisionModel.tst.ts (100%) rename {repos/effect/packages => packages}/ai/openai/typetest/OpenAiTool.tst.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/ai/openrouter/README.md (100%) rename {repos/effect/packages => packages}/ai/openrouter/codegen.yml (100%) rename {repos/effect/packages => packages}/ai/openrouter/package.json (98%) rename {repos/effect/packages => packages}/ai/openrouter/src/Generated.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/src/OpenRouterClient.ts (89%) rename {repos/effect/packages => packages}/ai/openrouter/src/OpenRouterConfig.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/src/OpenRouterDecisionModel.ts (76%) rename {repos/effect/packages => packages}/ai/openrouter/src/OpenRouterError.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/src/OpenRouterLanguageModel.ts (96%) rename {repos/effect/packages => packages}/ai/openrouter/src/OpenRouterSchema.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/src/index.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/src/internal/errors.ts (83%) rename {repos/effect/packages => packages}/ai/openrouter/src/internal/utilities.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/test/Generated.test.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/test/OpenRouterClient.test.ts (99%) rename {repos/effect/packages => packages}/ai/openrouter/test/OpenRouterDecisionModel.test.ts (94%) rename {repos/effect/packages => packages}/ai/openrouter/test/OpenRouterLanguageModel.test.ts (78%) rename {repos/effect/packages => packages}/ai/openrouter/test/OpenRouterSchema.test.ts (100%) rename {repos/effect/packages => packages}/ai/openrouter/tsconfig.json (100%) rename {repos/effect/packages => packages}/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts (100%) rename {repos/effect/packages => packages}/ai/typesafe/CHANGELOG.md (64%) rename {repos/effect/packages => packages}/ai/typesafe/README.md (100%) rename {repos/effect/packages => packages}/ai/typesafe/package.json (98%) rename {repos/effect/packages => packages}/ai/typesafe/src/TypeSafeClient.ts (100%) rename {repos/effect/packages => packages}/ai/typesafe/src/TypeSafeConfig.ts (100%) rename {repos/effect/packages => packages}/ai/typesafe/src/TypeSafeDecisionModel.ts (100%) rename {repos/effect/packages => packages}/ai/typesafe/src/TypeSafeSchema.ts (100%) rename {repos/effect/packages => packages}/ai/typesafe/src/index.ts (100%) rename {repos/effect/packages => packages}/ai/typesafe/src/internal/errors.ts (85%) rename {repos/effect/packages => packages}/ai/typesafe/test/TypeSafeClient.test.ts (95%) rename {repos/effect/packages => packages}/ai/typesafe/test/TypeSafeDecisionModel.test.ts (96%) rename {repos/effect/packages => packages}/ai/typesafe/test/TypeSafeSchema.test.ts (100%) rename {repos/effect/packages => packages}/ai/typesafe/tsconfig.json (100%) rename {repos/effect/packages => packages}/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts (100%) rename {repos/effect/packages => packages}/atom/react/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/atom/react/LICENSE (100%) rename {repos/effect/packages => packages}/atom/react/README.md (100%) rename {repos/effect/packages => packages}/atom/react/package.json (96%) rename {repos/effect/packages => packages}/atom/react/src/Hooks.ts (96%) rename {repos/effect/packages => packages}/atom/react/src/ReactHydration.ts (100%) rename {repos/effect/packages => packages}/atom/react/src/RegistryContext.ts (98%) rename {repos/effect/packages => packages}/atom/react/src/ScopedAtom.ts (100%) rename {repos/effect/packages => packages}/atom/react/src/index.ts (100%) rename {repos/effect/packages => packages}/atom/react/test/index.test.tsx (94%) rename {repos/effect/packages => packages}/atom/react/tsconfig.json (100%) rename {repos/effect/packages => packages}/atom/react/vitest.setup.ts (100%) rename {repos/effect/packages => packages}/atom/solid/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/atom/solid/LICENSE (100%) rename {repos/effect/packages => packages}/atom/solid/README.md (100%) rename {repos/effect/packages => packages}/atom/solid/package.json (98%) rename {repos/effect/packages => packages}/atom/solid/src/Hooks.ts (95%) rename {repos/effect/packages => packages}/atom/solid/src/RegistryContext.ts (98%) rename {repos/effect/packages => packages}/atom/solid/src/index.ts (100%) rename {repos/effect/packages => packages}/atom/solid/test/index.test.tsx (100%) rename {repos/effect/packages => packages}/atom/solid/tsconfig.json (100%) rename {repos/effect/packages => packages}/atom/solid/vitest.setup.ts (100%) rename {repos/effect/packages => packages}/atom/vue/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/atom/vue/LICENSE (100%) rename {repos/effect/packages => packages}/atom/vue/README.md (100%) rename {repos/effect/packages => packages}/atom/vue/package.json (97%) rename {repos/effect/packages => packages}/atom/vue/src/index.ts (92%) rename {repos/effect/packages => packages}/atom/vue/test/index.test.ts (100%) rename {repos/effect/packages => packages}/atom/vue/tsconfig.json (100%) delete mode 100644 packages/cli/.gitignore delete mode 100644 packages/cli/.madgerc delete mode 100644 packages/cli/.npmignore delete mode 100644 packages/cli/CHANGELOG.md delete mode 100755 packages/cli/bin.js delete mode 100644 packages/cli/package.json delete mode 100644 packages/cli/src/argv-patch.ts delete mode 100644 packages/cli/src/extract.ts delete mode 100644 packages/cli/src/gist.ts delete mode 100644 packages/cli/src/index.ts delete mode 100644 packages/cli/src/os-command.ts delete mode 100644 packages/cli/src/shared.ts delete mode 100644 packages/cli/src/sync-effect-app-subtree.ts delete mode 100644 packages/cli/src/sync-effect-subtree.ts delete mode 100644 packages/cli/src/sync-shared.ts delete mode 100644 packages/cli/src/update-packages.ts delete mode 100644 packages/cli/test.gists.yaml delete mode 100644 packages/cli/test/argv-patch.test.ts delete mode 100644 packages/cli/test/gist-cache.test.ts delete mode 100644 packages/cli/test/update-packages.test.ts delete mode 100644 packages/cli/tsconfig.json delete mode 100644 packages/cli/vitest.config.ts delete mode 100644 packages/e2e/CHANGELOG.md delete mode 100644 packages/e2e/package.json delete mode 100644 packages/e2e/test/repoInvalidation.e2e.test.ts delete mode 100644 packages/e2e/tsconfig.json delete mode 100644 packages/e2e/vitest.config.ts delete mode 100644 packages/effect-app/.madgerc delete mode 100644 packages/effect-app/.npmignore delete mode 100644 packages/effect-app/CHANGELOG.md delete mode 100644 packages/effect-app/package.json delete mode 100644 packages/effect-app/src/Array.ts delete mode 100644 packages/effect-app/src/Chunk.ts delete mode 100644 packages/effect-app/src/Config.ts delete mode 100644 packages/effect-app/src/Config/SecretURL.ts delete mode 100644 packages/effect-app/src/Config/internal/configSecretURL.ts delete mode 100644 packages/effect-app/src/ConfigProvider.ts delete mode 100644 packages/effect-app/src/Context.ts delete mode 100644 packages/effect-app/src/DataDependencies.ts delete mode 100644 packages/effect-app/src/DateTime.ts delete mode 100644 packages/effect-app/src/Effect.ts delete mode 100644 packages/effect-app/src/Emailer.ts delete mode 100644 packages/effect-app/src/Function.ts delete mode 100644 packages/effect-app/src/Inputify.type.ts delete mode 100644 packages/effect-app/src/Layer.ts delete mode 100644 packages/effect-app/src/Model.ts delete mode 100644 packages/effect-app/src/Model/Repository.ts delete mode 100644 packages/effect-app/src/Model/Repository/Registry.ts delete mode 100644 packages/effect-app/src/Model/Repository/dependency.ts delete mode 100644 packages/effect-app/src/Model/Repository/ext.ts delete mode 100644 packages/effect-app/src/Model/Repository/internal/internal.ts delete mode 100644 packages/effect-app/src/Model/Repository/legacy.ts delete mode 100644 packages/effect-app/src/Model/Repository/makeRepo.ts delete mode 100644 packages/effect-app/src/Model/Repository/service.ts delete mode 100644 packages/effect-app/src/Model/Repository/validation.ts delete mode 100644 packages/effect-app/src/Model/dsl.ts delete mode 100644 packages/effect-app/src/Model/filter/filterApi.ts delete mode 100644 packages/effect-app/src/Model/filter/types.ts delete mode 100644 packages/effect-app/src/Model/filter/types/errors.ts delete mode 100644 packages/effect-app/src/Model/filter/types/fields.ts delete mode 100644 packages/effect-app/src/Model/filter/types/path/common.ts delete mode 100644 packages/effect-app/src/Model/filter/types/path/eager.ts delete mode 100644 packages/effect-app/src/Model/filter/types/path/index.ts delete mode 100644 packages/effect-app/src/Model/filter/types/utils.ts delete mode 100644 packages/effect-app/src/Model/filter/types/validator.ts delete mode 100644 packages/effect-app/src/Model/query.ts delete mode 100644 packages/effect-app/src/Model/query/dsl.ts delete mode 100644 packages/effect-app/src/Model/query/new-kid-interpreter.ts delete mode 100644 packages/effect-app/src/NonEmptySet.ts delete mode 100644 packages/effect-app/src/Option.ts delete mode 100644 packages/effect-app/src/Pure.ts delete mode 100644 packages/effect-app/src/QueueMaker.ts delete mode 100644 packages/effect-app/src/RequestContext.ts delete mode 100644 packages/effect-app/src/RequestScopedDependencies.ts delete mode 100644 packages/effect-app/src/Schema.ts delete mode 100644 packages/effect-app/src/Schema/Class.ts delete mode 100644 packages/effect-app/src/Schema/FastCheck.ts delete mode 100644 packages/effect-app/src/Schema/Methods.ts delete mode 100644 packages/effect-app/src/Schema/SchemaParser.ts delete mode 100644 packages/effect-app/src/Schema/SpecialJsonSchema.ts delete mode 100644 packages/effect-app/src/Schema/SpecialOpenApi.ts delete mode 100644 packages/effect-app/src/Schema/brand.ts delete mode 100644 packages/effect-app/src/Schema/email.ts delete mode 100644 packages/effect-app/src/Schema/ext.ts delete mode 100644 packages/effect-app/src/Schema/moreStrings.ts delete mode 100644 packages/effect-app/src/Schema/numbers.ts delete mode 100644 packages/effect-app/src/Schema/phoneNumber.ts delete mode 100644 packages/effect-app/src/Schema/schema.ts delete mode 100644 packages/effect-app/src/Schema/strings.ts delete mode 100644 packages/effect-app/src/SchemaAST.ts delete mode 100644 packages/effect-app/src/Set.ts delete mode 100644 packages/effect-app/src/Store.ts delete mode 100644 packages/effect-app/src/TypeTest.ts delete mode 100644 packages/effect-app/src/Types.ts delete mode 100644 packages/effect-app/src/Widen.type.ts delete mode 100644 packages/effect-app/src/_ext/Array.ts delete mode 100644 packages/effect-app/src/_ext/ord.ext.ts delete mode 100644 packages/effect-app/src/builtin-json.d.ts delete mode 100644 packages/effect-app/src/builtin.ts delete mode 100644 packages/effect-app/src/client.ts delete mode 100644 packages/effect-app/src/client/InvalidationKeys.ts delete mode 100644 packages/effect-app/src/client/apiClientFactory.ts delete mode 100644 packages/effect-app/src/client/clientFor.ts delete mode 100644 packages/effect-app/src/client/errors.ts delete mode 100644 packages/effect-app/src/client/makeClient.ts delete mode 100644 packages/effect-app/src/faker.ts delete mode 100644 packages/effect-app/src/http.ts delete mode 100644 packages/effect-app/src/http/Request.ts delete mode 100644 packages/effect-app/src/http/internal/lib.ts delete mode 100644 packages/effect-app/src/ids.ts delete mode 100644 packages/effect-app/src/index.ts delete mode 100644 packages/effect-app/src/logger.ts delete mode 100644 packages/effect-app/src/middleware.ts delete mode 100644 packages/effect-app/src/migration/nativeEffect.ts delete mode 100644 packages/effect-app/src/rpc.ts delete mode 100644 packages/effect-app/src/rpc/Invalidation.ts delete mode 100644 packages/effect-app/src/rpc/MiddlewareMaker.ts delete mode 100644 packages/effect-app/src/rpc/README.md delete mode 100644 packages/effect-app/src/rpc/RpcContextMap.ts delete mode 100644 packages/effect-app/src/rpc/RpcMiddleware.ts delete mode 100644 packages/effect-app/src/runtime.ts delete mode 100644 packages/effect-app/src/setupRequest.ts delete mode 100644 packages/effect-app/src/toast.ts delete mode 100644 packages/effect-app/src/transform.ts delete mode 100644 packages/effect-app/src/utils.ts delete mode 100644 packages/effect-app/src/utils/effectify.ts delete mode 100644 packages/effect-app/src/utils/extend.ts delete mode 100644 packages/effect-app/src/utils/gen.ts delete mode 100644 packages/effect-app/src/utils/logLevel.ts delete mode 100644 packages/effect-app/src/utils/logger.ts delete mode 100644 packages/effect-app/src/validation.ts delete mode 100644 packages/effect-app/src/validation/validators.ts delete mode 100644 packages/effect-app/src/waitingGate.ts delete mode 100644 packages/effect-app/src/withToast.ts delete mode 100644 packages/effect-app/test/dateTimeLocal.test.ts delete mode 100644 packages/effect-app/test/moreStrings.test.ts delete mode 100644 packages/effect-app/test/nativeEffectMigration.test.ts delete mode 100644 packages/effect-app/test/rpc.test.ts delete mode 100644 packages/effect-app/test/rpcMiddlewareContext.test.ts delete mode 100644 packages/effect-app/test/schema.test.ts delete mode 100644 packages/effect-app/test/secretURL.test.ts delete mode 100644 packages/effect-app/test/special.test.ts delete mode 100644 packages/effect-app/test/utils.test.ts delete mode 100644 packages/effect-app/test/waitingGate.test.ts delete mode 100644 packages/effect-app/tsconfig.base.json delete mode 100644 packages/effect-app/tsconfig.json delete mode 100644 packages/effect-app/tsconfig.src.json delete mode 100644 packages/effect-app/tsconfig.test.json delete mode 100644 packages/effect-app/vitest.config.ts rename {repos/effect/packages => packages}/effect/ARBITRARY.md (100%) rename {repos/effect/packages => packages}/effect/CHANGELOG.md (86%) rename {repos/effect/packages => packages}/effect/CONFIG.md (100%) rename {repos/effect/packages => packages}/effect/HTTPAPI.md (81%) rename {repos/effect/packages => packages}/effect/LICENSE (100%) rename {repos/effect/packages => packages}/effect/MCP.md (55%) rename {repos/effect/packages => packages}/effect/OPTIC.md (100%) rename {repos/effect/packages => packages}/effect/README.md (100%) rename {repos/effect/packages => packages}/effect/SCHEMA.md (98%) rename {repos/effect/packages => packages}/effect/benchmark/Pool.ts (100%) create mode 100644 packages/effect/benchmark/encoding/Xml.ts create mode 100644 packages/effect/benchmark/eventlog/Bandwidth.md create mode 100644 packages/effect/benchmark/eventlog/Bandwidth.ts rename {repos/effect/packages => packages}/effect/benchmark/http/multipart.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/http/serverAllocations.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/http/tracer.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/httpapi/sseClientDecoder.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/net/NetAddress.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/rpc/RpcSerialization.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/schema/Optic.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/schema/SchemaBinary.md (100%) rename {repos/effect/packages => packages}/effect/benchmark/schema/SchemaBinary.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/schema/SchemaError.ts (100%) rename {repos/effect/packages => packages}/effect/benchmark/stream/splitLines.ts (100%) rename {repos/effect/packages => packages}/effect/package.json (83%) rename {repos/effect/packages => packages}/effect/runtimeperf/README.md (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/compare.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/config.json (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/materialize.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/run.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/stats.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/arbitrary/fixtures/native.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/README.md (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/costs.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/resources.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/aot.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/cases.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/data.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/generate.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/jit.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/valibot.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/moltar/fixtures/zod.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-checked-sources/README.md (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-checked-sources/fixtures/cases.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/AST-LAYOUT.md (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/README.md (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/RESULTS.md (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/TO-TYPE.md (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/fixtures.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/profile.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/run.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-construction/worker.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema-excess-properties/fixtures/cases.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema/fixtures/adapters.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema/fixtures/behavior.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema/fixtures/cold.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/suites/schema/fixtures/comparison.ts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/test/materialize.test.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/test/registry.test.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/test/schema-construction.test.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/test/stats.test.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/test/worker.test.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/test/worktree-diff.test.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/utils.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/worker.mts (100%) rename {repos/effect/packages => packages}/effect/runtimeperf/worktree-diff.mts (100%) rename {repos/effect/packages => packages}/effect/src/Arbitrary.ts (100%) rename {repos/effect/packages => packages}/effect/src/Array.ts (98%) rename {repos/effect/packages => packages}/effect/src/BigDecimal.ts (100%) rename {repos/effect/packages => packages}/effect/src/BigInt.ts (100%) rename {repos/effect/packages => packages}/effect/src/Boolean.ts (100%) rename {repos/effect/packages => packages}/effect/src/Brand.ts (100%) rename {repos/effect/packages => packages}/effect/src/ByteSize.ts (100%) create mode 100644 packages/effect/src/Bytes.ts rename {repos/effect/packages => packages}/effect/src/Cache.ts (100%) rename {repos/effect/packages => packages}/effect/src/Cause.ts (100%) rename {repos/effect/packages => packages}/effect/src/Channel.ts (97%) rename {repos/effect/packages => packages}/effect/src/ChannelSchema.ts (100%) rename {repos/effect/packages => packages}/effect/src/Chunk.ts (98%) rename {repos/effect/packages => packages}/effect/src/Clock.ts (100%) rename {repos/effect/packages => packages}/effect/src/Combiner.ts (100%) rename {repos/effect/packages => packages}/effect/src/Config.ts (99%) rename {repos/effect/packages => packages}/effect/src/ConfigProvider.ts (99%) rename {repos/effect/packages => packages}/effect/src/Console.ts (100%) rename {repos/effect/packages => packages}/effect/src/Context.ts (100%) rename {repos/effect/packages => packages}/effect/src/Cron.ts (98%) rename {repos/effect/packages => packages}/effect/src/Crypto.ts (100%) rename {repos/effect/packages => packages}/effect/src/Data.ts (100%) rename {repos/effect/packages => packages}/effect/src/DateTime.ts (100%) rename {repos/effect/packages => packages}/effect/src/Deferred.ts (100%) rename {repos/effect/packages => packages}/effect/src/Differ.ts (100%) rename {repos/effect/packages => packages}/effect/src/Duration.ts (93%) rename {repos/effect/packages => packages}/effect/src/Effect.ts (97%) rename {repos/effect/packages => packages}/effect/src/Effectable.ts (100%) rename {repos/effect/packages => packages}/effect/src/Equal.ts (100%) rename {repos/effect/packages => packages}/effect/src/Equivalence.ts (99%) rename {repos/effect/packages => packages}/effect/src/ErrorReporter.ts (100%) rename {repos/effect/packages => packages}/effect/src/ExecutionPlan.ts (100%) rename {repos/effect/packages => packages}/effect/src/Exit.ts (98%) rename {repos/effect/packages => packages}/effect/src/Fiber.ts (100%) rename {repos/effect/packages => packages}/effect/src/FiberHandle.ts (98%) rename {repos/effect/packages => packages}/effect/src/FiberMap.ts (98%) rename {repos/effect/packages => packages}/effect/src/FiberSet.ts (100%) rename {repos/effect/packages => packages}/effect/src/FileSystem.ts (99%) rename {repos/effect/packages => packages}/effect/src/Filter.ts (100%) rename {repos/effect/packages => packages}/effect/src/Formatter.ts (100%) rename {repos/effect/packages => packages}/effect/src/Function.ts (100%) rename {repos/effect/packages => packages}/effect/src/Graph.ts (100%) rename {repos/effect/packages => packages}/effect/src/HKT.ts (100%) rename {repos/effect/packages => packages}/effect/src/Hash.ts (100%) rename {repos/effect/packages => packages}/effect/src/HashMap.ts (100%) rename {repos/effect/packages => packages}/effect/src/HashRing.ts (100%) rename {repos/effect/packages => packages}/effect/src/HashSet.ts (100%) rename {repos/effect/packages => packages}/effect/src/Inspectable.ts (100%) rename {repos/effect/packages => packages}/effect/src/Iterable.ts (99%) rename {repos/effect/packages => packages}/effect/src/JsonPatch.ts (100%) rename {repos/effect/packages => packages}/effect/src/JsonPointer.ts (100%) rename {repos/effect/packages => packages}/effect/src/JsonSchema.ts (97%) rename {repos/effect/packages => packages}/effect/src/Latch.ts (100%) rename {repos/effect/packages => packages}/effect/src/Layer.ts (99%) rename {repos/effect/packages => packages}/effect/src/LayerMap.ts (100%) rename {repos/effect/packages => packages}/effect/src/LayerRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/LogLevel.ts (100%) rename {repos/effect/packages => packages}/effect/src/Logger.ts (100%) rename {repos/effect/packages => packages}/effect/src/ManagedRuntime.ts (79%) rename {repos/effect/packages => packages}/effect/src/Match.ts (100%) rename {repos/effect/packages => packages}/effect/src/Metric.ts (93%) rename {repos/effect/packages => packages}/effect/src/MutableHashMap.ts (100%) rename {repos/effect/packages => packages}/effect/src/MutableHashSet.ts (100%) rename {repos/effect/packages => packages}/effect/src/MutableList.ts (100%) rename {repos/effect/packages => packages}/effect/src/MutableRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/Newtype.ts (100%) rename {repos/effect/packages => packages}/effect/src/NonEmptyIterable.ts (100%) rename {repos/effect/packages => packages}/effect/src/Number.ts (99%) rename {repos/effect/packages => packages}/effect/src/Optic.ts (100%) rename {repos/effect/packages => packages}/effect/src/Option.ts (100%) rename {repos/effect/packages => packages}/effect/src/Order.ts (100%) rename {repos/effect/packages => packages}/effect/src/Ordering.ts (100%) rename {repos/effect/packages => packages}/effect/src/PartitionedSemaphore.ts (100%) rename {repos/effect/packages => packages}/effect/src/Path.ts (99%) rename {repos/effect/packages => packages}/effect/src/Pipeable.ts (100%) rename {repos/effect/packages => packages}/effect/src/PlatformError.ts (100%) rename {repos/effect/packages => packages}/effect/src/Pool.ts (100%) rename {repos/effect/packages => packages}/effect/src/Predicate.ts (100%) rename {repos/effect/packages => packages}/effect/src/PrimaryKey.ts (100%) rename {repos/effect/packages => packages}/effect/src/PubSub.ts (99%) rename {repos/effect/packages => packages}/effect/src/Pull.ts (100%) rename {repos/effect/packages => packages}/effect/src/Queue.ts (99%) rename {repos/effect/packages => packages}/effect/src/Random.ts (99%) rename {repos/effect/packages => packages}/effect/src/RcMap.ts (97%) rename {repos/effect/packages => packages}/effect/src/RcRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/Record.ts (99%) rename {repos/effect/packages => packages}/effect/src/Redactable.ts (100%) rename {repos/effect/packages => packages}/effect/src/Redacted.ts (100%) rename {repos/effect/packages => packages}/effect/src/Reducer.ts (100%) rename {repos/effect/packages => packages}/effect/src/Ref.ts (100%) rename {repos/effect/packages => packages}/effect/src/References.ts (98%) rename {repos/effect/packages => packages}/effect/src/RegExp.ts (100%) rename {repos/effect/packages => packages}/effect/src/Request.ts (100%) rename {repos/effect/packages => packages}/effect/src/RequestResolver.ts (95%) rename {repos/effect/packages => packages}/effect/src/Resource.ts (98%) rename {repos/effect/packages => packages}/effect/src/Result.ts (100%) rename {repos/effect/packages => packages}/effect/src/Runtime.ts (93%) rename {repos/effect/packages => packages}/effect/src/Schedule.ts (100%) rename {repos/effect/packages => packages}/effect/src/Scheduler.ts (100%) rename {repos/effect/packages => packages}/effect/src/Schema.ts (96%) rename {repos/effect/packages => packages}/effect/src/SchemaAST.ts (99%) rename {repos/effect/packages => packages}/effect/src/SchemaGetter.ts (98%) rename {repos/effect/packages => packages}/effect/src/SchemaIssue.ts (95%) rename {repos/effect/packages => packages}/effect/src/SchemaParser.ts (100%) rename {repos/effect/packages => packages}/effect/src/SchemaRepresentation.ts (97%) rename {repos/effect/packages => packages}/effect/src/SchemaTransformation.ts (99%) rename {repos/effect/packages => packages}/effect/src/Scope.ts (97%) rename {repos/effect/packages => packages}/effect/src/ScopedCache.ts (96%) rename {repos/effect/packages => packages}/effect/src/ScopedRef.ts (100%) create mode 100644 packages/effect/src/SemVer.ts create mode 100644 packages/effect/src/SemVerRange.ts rename {repos/effect/packages => packages}/effect/src/Semaphore.ts (100%) rename {repos/effect/packages => packages}/effect/src/Sink.ts (99%) rename {repos/effect/packages => packages}/effect/src/StandardSchema.ts (100%) rename {repos/effect/packages => packages}/effect/src/Stdio.ts (100%) rename {repos/effect/packages => packages}/effect/src/Stream.ts (98%) rename {repos/effect/packages => packages}/effect/src/String.ts (98%) rename {repos/effect/packages => packages}/effect/src/Struct.ts (100%) rename {repos/effect/packages => packages}/effect/src/SubscriptionRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/Symbol.ts (100%) rename {repos/effect/packages => packages}/effect/src/SynchronizedRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/Take.ts (100%) rename {repos/effect/packages => packages}/effect/src/Terminal.ts (100%) create mode 100644 packages/effect/src/TextDiff.ts rename {repos/effect/packages => packages}/effect/src/Tracer.ts (100%) rename {repos/effect/packages => packages}/effect/src/Trie.ts (100%) rename {repos/effect/packages => packages}/effect/src/Tuple.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxChunk.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxDeferred.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxHashMap.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxHashSet.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxPriorityQueue.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxPubSub.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxQueue.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxReentrantLock.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxSemaphore.ts (100%) rename {repos/effect/packages => packages}/effect/src/TxSubscriptionRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/Types.ts (100%) rename {repos/effect/packages => packages}/effect/src/UndefinedOr.ts (100%) rename {repos/effect/packages => packages}/effect/src/Unify.ts (100%) rename {repos/effect/packages => packages}/effect/src/Utils.ts (100%) rename {repos/effect/packages => packages}/effect/src/Version.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/AiError.ts (96%) rename {repos/effect/packages => packages}/effect/src/ai/AnthropicStructuredOutput.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/Chat.ts (98%) rename {repos/effect/packages => packages}/effect/src/ai/Decision.ts (88%) create mode 100644 packages/effect/src/ai/DecisionModel.ts rename {repos/effect/packages => packages}/effect/src/ai/EmbeddingModel.ts (87%) rename {repos/effect/packages => packages}/effect/src/ai/IdGenerator.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/LanguageModel.ts (100%) create mode 100644 packages/effect/src/ai/McpClient.ts create mode 100644 packages/effect/src/ai/McpClientError.ts rename {repos/effect/packages => packages}/effect/src/ai/McpProtocol.ts (98%) rename {repos/effect/packages => packages}/effect/src/ai/McpSchema.ts (95%) rename {repos/effect/packages => packages}/effect/src/ai/McpServer.ts (91%) rename {repos/effect/packages => packages}/effect/src/ai/Model.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/OpenAiStructuredOutput.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/Prompt.ts (97%) rename {repos/effect/packages => packages}/effect/src/ai/Response.ts (95%) rename {repos/effect/packages => packages}/effect/src/ai/ResponseIdTracker.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/Telemetry.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/Tokenizer.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/Tool.ts (99%) rename {repos/effect/packages => packages}/effect/src/ai/Toolkit.ts (87%) rename {repos/effect/packages => packages}/effect/src/ai/index.ts (90%) rename {repos/effect/packages => packages}/effect/src/ai/internal/codec-transformer.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpCore.ts (93%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpProtocol.ts (96%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpProtocol/v2024_11_05.ts (99%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpProtocol/v2025_03_26.ts (99%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpProtocol/v2025_06_18.ts (98%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpProtocol/v2025_11_25.ts (98%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts (97%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpProtocolRegistry.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpRuntime.ts (99%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpSchema/v2024_11_05.ts (94%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpSchema/v2025_03_26.ts (97%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpSchema/v2025_06_18.ts (97%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpSchema/v2025_11_25.ts (97%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpSchema/v2026_07_28.ts (92%) rename {repos/effect/packages => packages}/effect/src/ai/internal/mcpStatefulRuntime.ts (100%) rename {repos/effect/packages => packages}/effect/src/ai/internal/structured-output.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/Argument.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/CliConfig.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/CliError.ts (92%) rename {repos/effect/packages => packages}/effect/src/cli/CliOutput.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/Command.ts (92%) rename {repos/effect/packages => packages}/effect/src/cli/Completions.ts (95%) rename {repos/effect/packages => packages}/effect/src/cli/Flag.ts (95%) rename {repos/effect/packages => packages}/effect/src/cli/GlobalFlag.ts (98%) rename {repos/effect/packages => packages}/effect/src/cli/HelpDoc.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/Param.ts (96%) rename {repos/effect/packages => packages}/effect/src/cli/Primitive.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/Prompt.ts (100%) create mode 100644 packages/effect/src/cli/TerminalCapabilities.ts rename {repos/effect/packages => packages}/effect/src/cli/index.ts (91%) rename {repos/effect/packages => packages}/effect/src/cli/internal/ansi.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/internal/auto-suggest.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/internal/command.ts (96%) rename {repos/effect/packages => packages}/effect/src/cli/internal/completions/bash.ts (98%) rename {repos/effect/packages => packages}/effect/src/cli/internal/completions/descriptor.ts (92%) rename {repos/effect/packages => packages}/effect/src/cli/internal/completions/fish.ts (95%) rename {repos/effect/packages => packages}/effect/src/cli/internal/completions/powershell.ts (90%) rename {repos/effect/packages => packages}/effect/src/cli/internal/completions/zsh.ts (95%) rename {repos/effect/packages => packages}/effect/src/cli/internal/config.ts (100%) create mode 100644 packages/effect/src/cli/internal/formatter.ts rename {repos/effect/packages => packages}/effect/src/cli/internal/help.ts (100%) rename {repos/effect/packages => packages}/effect/src/cli/internal/lexer.ts (83%) rename {repos/effect/packages => packages}/effect/src/cli/internal/parser.ts (96%) rename {repos/effect/packages => packages}/effect/src/cli/internal/wizard.ts (100%) create mode 100644 packages/effect/src/cluster/ClusterCron.ts rename {repos/effect/packages => packages}/effect/src/cluster/ClusterError.ts (100%) create mode 100644 packages/effect/src/cluster/ClusterMetrics.ts rename {repos/effect/packages => packages}/effect/src/cluster/ClusterSchema.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/ClusterWorkflowEngine.ts (90%) rename {repos/effect/packages => packages}/effect/src/cluster/DeliverAt.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Entity.ts (99%) rename {repos/effect/packages => packages}/effect/src/cluster/EntityAddress.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/EntityId.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/EntityProxy.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/EntityProxyServer.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/EntityResource.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/EntityType.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Envelope.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/HttpRunner.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/K8sHttpClient.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/K8sTypes.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/MachineId.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Message.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/MessageStorage.ts (99%) rename {repos/effect/packages => packages}/effect/src/cluster/Reply.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Runner.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/RunnerAddress.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/RunnerHealth.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/RunnerServer.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/RunnerStorage.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Runners.ts (88%) rename {repos/effect/packages => packages}/effect/src/cluster/ShardId.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Sharding.ts (93%) rename {repos/effect/packages => packages}/effect/src/cluster/ShardingConfig.ts (96%) rename {repos/effect/packages => packages}/effect/src/cluster/ShardingRegistrationEvent.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/SingleRunner.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Singleton.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/SingletonAddress.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/Snowflake.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/SocketRunner.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/SqlMessageStorage.ts (93%) rename {repos/effect/packages => packages}/effect/src/cluster/SqlRunnerStorage.ts (98%) rename {repos/effect/packages => packages}/effect/src/cluster/TestRunner.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/clusterAbandon.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/entityActivation.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/entityManager.ts (91%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/entityReaper.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/hash.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/interruptors.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/resourceMap.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/resourceRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/cluster/internal/shardLock.ts (100%) rename {repos/effect/packages => packages}/effect/src/devtools/DevTools.ts (92%) rename {repos/effect/packages => packages}/effect/src/devtools/DevToolsClient.ts (84%) rename {repos/effect/packages => packages}/effect/src/devtools/DevToolsSchema.ts (100%) rename {repos/effect/packages => packages}/effect/src/devtools/DevToolsServer.ts (100%) rename {repos/effect/packages => packages}/effect/src/devtools/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/encoding/Base64.ts (100%) rename {repos/effect/packages => packages}/effect/src/encoding/Base64Url.ts (81%) rename {repos/effect/packages => packages}/effect/src/encoding/EncodingError.ts (100%) rename {repos/effect/packages => packages}/effect/src/encoding/Hex.ts (100%) rename {repos/effect/packages => packages}/effect/src/encoding/Ini.ts (99%) create mode 100644 packages/effect/src/encoding/JsonArray.ts rename {repos/effect/packages => packages}/effect/src/encoding/Ndjson.ts (66%) rename {repos/effect/packages => packages}/effect/src/encoding/SchemaBinary.ts (99%) rename {repos/effect/packages => packages}/effect/src/encoding/Sse.ts (100%) create mode 100644 packages/effect/src/encoding/Tar.ts rename {repos/effect/packages => packages}/effect/src/encoding/Toml.ts (99%) create mode 100644 packages/effect/src/encoding/Xml.ts rename {repos/effect/packages => packages}/effect/src/encoding/Yaml.ts (78%) create mode 100644 packages/effect/src/encoding/Zip.ts rename {repos/effect/packages => packages}/effect/src/encoding/index.ts (73%) rename {repos/effect/packages => packages}/effect/src/eventlog/Event.ts (100%) rename {repos/effect/packages => packages}/effect/src/eventlog/EventGroup.ts (100%) create mode 100644 packages/effect/src/eventlog/EventJournal.ts rename {repos/effect/packages => packages}/effect/src/eventlog/EventLog.ts (55%) create mode 100644 packages/effect/src/eventlog/EventLogEncryption.ts rename {repos/effect/packages => packages}/effect/src/eventlog/EventLogMessage.ts (82%) rename {repos/effect/packages => packages}/effect/src/eventlog/EventLogRemote.ts (77%) rename {repos/effect/packages => packages}/effect/src/eventlog/EventLogServer.ts (61%) rename {repos/effect/packages => packages}/effect/src/eventlog/EventLogServerEncrypted.ts (55%) rename {repos/effect/packages => packages}/effect/src/eventlog/EventLogServerUnencrypted.ts (79%) rename {repos/effect/packages => packages}/effect/src/eventlog/EventLogSessionAuth.ts (100%) create mode 100644 packages/effect/src/eventlog/EventRow.ts create mode 100644 packages/effect/src/eventlog/SqlEventJournal.ts create mode 100644 packages/effect/src/eventlog/SqlEventLogServerEncrypted.ts rename {repos/effect/packages => packages}/effect/src/eventlog/SqlEventLogServerUnencrypted.ts (74%) rename {repos/effect/packages => packages}/effect/src/eventlog/index.ts (94%) create mode 100644 packages/effect/src/eventlog/internal/conflicts.ts rename {repos/effect/packages => packages}/effect/src/eventlog/internal/identityRootSecretDerivation.ts (100%) rename {repos/effect/packages => packages}/effect/src/graphql/GraphQL.ts (100%) rename {repos/effect/packages => packages}/effect/src/graphql/GraphQLClient.ts (100%) rename {repos/effect/packages => packages}/effect/src/graphql/GraphQLClientError.ts (97%) rename {repos/effect/packages => packages}/effect/src/graphql/GraphQLGroup.ts (100%) rename {repos/effect/packages => packages}/effect/src/graphql/GraphQLMiddleware.ts (100%) rename {repos/effect/packages => packages}/effect/src/graphql/GraphQLProtocol.ts (99%) rename {repos/effect/packages => packages}/effect/src/graphql/GraphQLResolver.ts (85%) rename {repos/effect/packages => packages}/effect/src/graphql/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApi.ts (99%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiBuilder.ts (87%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiClient.ts (86%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiEndpoint.ts (87%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiError.ts (89%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiGroup.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiMiddleware.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiScalar.ts (96%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiSchema.ts (76%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiSecurity.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiSwagger.ts (85%) rename {repos/effect/packages => packages}/effect/src/http-api/HttpApiTest.ts (72%) rename {repos/effect/packages => packages}/effect/src/http-api/OpenApi.ts (92%) rename {repos/effect/packages => packages}/effect/src/http-api/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/internal/html.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/internal/httpApiScalar.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/internal/httpApiSwagger.ts (100%) rename {repos/effect/packages => packages}/effect/src/http-api/internal/mediaType.ts (100%) create mode 100644 packages/effect/src/http-api/internal/path.ts rename {repos/effect/packages => packages}/effect/src/http/Cookies.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/Etag.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/FetchHttpClient.ts (63%) rename {repos/effect/packages => packages}/effect/src/http/FindMyWay.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/FindMyWay/internal/queryString.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/FindMyWay/internal/router.ts (98%) rename {repos/effect/packages => packages}/effect/src/http/Headers.ts (51%) rename {repos/effect/packages => packages}/effect/src/http/HttpBody.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/HttpClient.ts (96%) rename {repos/effect/packages => packages}/effect/src/http/HttpClientError.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/HttpClientRequest.ts (94%) rename {repos/effect/packages => packages}/effect/src/http/HttpClientResponse.ts (83%) rename {repos/effect/packages => packages}/effect/src/http/HttpEffect.ts (94%) rename {repos/effect/packages => packages}/effect/src/http/HttpIncomingMessage.ts (86%) rename {repos/effect/packages => packages}/effect/src/http/HttpMethod.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/HttpMiddleware.ts (76%) rename {repos/effect/packages => packages}/effect/src/http/HttpPlatform.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/HttpRouter.ts (98%) rename {repos/effect/packages => packages}/effect/src/http/HttpServer.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/HttpServerError.ts (96%) rename {repos/effect/packages => packages}/effect/src/http/HttpServerRequest.ts (90%) rename {repos/effect/packages => packages}/effect/src/http/HttpServerRespondable.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/HttpServerResponse.ts (95%) rename {repos/effect/packages => packages}/effect/src/http/HttpStaticServer.ts (93%) rename {repos/effect/packages => packages}/effect/src/http/HttpStatus.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/HttpTraceContext.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/Mime.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/Multipart.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/MultipartParser.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/MultipartParser/HeadersParser.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/MultipartParser/Search.ts (100%) create mode 100644 packages/effect/src/http/MultipartParser/internal/contentType.ts rename {repos/effect/packages => packages}/effect/src/http/MultipartParser/internal/headers.ts (91%) rename {repos/effect/packages => packages}/effect/src/http/MultipartParser/internal/multipart.ts (83%) rename {repos/effect/packages => packages}/effect/src/http/MultipartParser/internal/search.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/Template.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/Url.ts (98%) rename {repos/effect/packages => packages}/effect/src/http/UrlParams.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/internal/compression.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/internal/compressionBody.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/internal/decompression.ts (87%) rename {repos/effect/packages => packages}/effect/src/http/internal/headers.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/internal/httpBody.ts (100%) create mode 100644 packages/effect/src/http/internal/lazyStream.ts rename {repos/effect/packages => packages}/effect/src/http/internal/mimeTypes.ts (100%) create mode 100644 packages/effect/src/http/internal/requestState.ts rename {repos/effect/packages => packages}/effect/src/http/internal/tracing.ts (100%) rename {repos/effect/packages => packages}/effect/src/http/internal/urlParams.ts (100%) create mode 100644 packages/effect/src/http/internal/webResponse.ts rename {repos/effect/packages => packages}/effect/src/index.ts (97%) rename {repos/effect/packages => packages}/effect/src/internal/arbitrary/array.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/arbitrary/bigDecimal.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/arbitrary/bigInt.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/arbitrary/model.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/arbitrary/regexp.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/arbitrary/runner.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/arbitrary/schema.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/array.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/core.ts (99%) rename {repos/effect/packages => packages}/effect/src/internal/count.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/dateTime.ts (98%) rename {repos/effect/packages => packages}/effect/src/internal/doNotation.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/effect.ts (97%) rename {repos/effect/packages => packages}/effect/src/internal/equal.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/errors.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/executionPlan.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/graph.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/graphCsr.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/graphql.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/graphqlHeaders.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/hash.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/hashMap.ts (99%) rename {repos/effect/packages => packages}/effect/src/internal/hashSet.ts (100%) create mode 100644 packages/effect/src/internal/hydration.ts create mode 100644 packages/effect/src/internal/inflate.ts rename {repos/effect/packages => packages}/effect/src/internal/layer.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/matcher.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/metric.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/option.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/persistence.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/random.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/rcRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/record.ts (71%) rename {repos/effect/packages => packages}/effect/src/internal/redacted.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/references.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/request.ts (87%) rename {repos/effect/packages => packages}/effect/src/internal/result.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schedule.ts (85%) rename {repos/effect/packages => packages}/effect/src/internal/schema/annotations.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/cause.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/codegen.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/compilerRegistry.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/fromJsonSchemaDocument.ts (78%) rename {repos/effect/packages => packages}/effect/src/internal/schema/fromRepresentation.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/interpreter.ts (76%) rename {repos/effect/packages => packages}/effect/src/internal/schema/make.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/parser.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/standardSchema.ts (74%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toCodeDocument.ts (97%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toCodec.ts (67%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toDifferJsonPatch.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toEncoderXml.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toEquivalence.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toFormatter.ts (95%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toIso.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toJsonSchemaDocument.ts (91%) rename {repos/effect/packages => packages}/effect/src/internal/schema/toRepresentation.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/schemaError.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/stackTraceLimit.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/stream.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/tracer.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/trie.ts (99%) rename {repos/effect/packages => packages}/effect/src/internal/ulid.ts (100%) rename {repos/effect/packages => packages}/effect/src/internal/uuid.ts (71%) create mode 100644 packages/effect/src/internal/version.ts rename {repos/effect/packages => packages}/effect/src/net/AddressResolver.ts (100%) rename {repos/effect/packages => packages}/effect/src/net/Dns.ts (100%) rename {repos/effect/packages => packages}/effect/src/net/Host.ts (100%) rename {repos/effect/packages => packages}/effect/src/net/IpInterface.ts (100%) rename {repos/effect/packages => packages}/effect/src/net/IpNetwork.ts (100%) rename {repos/effect/packages => packages}/effect/src/net/NetAddress.ts (99%) rename {repos/effect/packages => packages}/effect/src/net/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/Otlp.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/OtlpExporter.ts (95%) rename {repos/effect/packages => packages}/effect/src/observability/OtlpLogger.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/OtlpMetrics.ts (98%) rename {repos/effect/packages => packages}/effect/src/observability/OtlpResource.ts (92%) rename {repos/effect/packages => packages}/effect/src/observability/OtlpSerialization.ts (74%) rename {repos/effect/packages => packages}/effect/src/observability/OtlpTracer.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/PrometheusMetrics.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/internal/otlpEnv.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/internal/otlpProtobuf.ts (100%) rename {repos/effect/packages => packages}/effect/src/observability/internal/protobuf.ts (95%) rename {repos/effect/packages => packages}/effect/src/persistence/KeyValueStore.ts (100%) rename {repos/effect/packages => packages}/effect/src/persistence/Persistable.ts (100%) rename {repos/effect/packages => packages}/effect/src/persistence/PersistedCache.ts (95%) rename {repos/effect/packages => packages}/effect/src/persistence/PersistedQueue.ts (87%) rename {repos/effect/packages => packages}/effect/src/persistence/Persistence.ts (92%) rename {repos/effect/packages => packages}/effect/src/persistence/RateLimiter.ts (98%) rename {repos/effect/packages => packages}/effect/src/persistence/Redis.ts (100%) create mode 100644 packages/effect/src/persistence/UserDirectories.ts rename {repos/effect/packages => packages}/effect/src/persistence/index.ts (88%) rename {repos/effect/packages => packages}/effect/src/process/ChildProcess.ts (99%) rename {repos/effect/packages => packages}/effect/src/process/ChildProcessSpawner.ts (100%) rename {repos/effect/packages => packages}/effect/src/process/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/reactivity/AsyncResult.ts (99%) rename {repos/effect/packages => packages}/effect/src/reactivity/Atom.ts (94%) rename {repos/effect/packages => packages}/effect/src/reactivity/AtomHttpApi.ts (92%) rename {repos/effect/packages => packages}/effect/src/reactivity/AtomRef.ts (100%) rename {repos/effect/packages => packages}/effect/src/reactivity/AtomRegistry.ts (94%) rename {repos/effect/packages => packages}/effect/src/reactivity/AtomRpc.ts (91%) rename {repos/effect/packages => packages}/effect/src/reactivity/Hydration.ts (71%) rename {repos/effect/packages => packages}/effect/src/reactivity/Reactivity.ts (77%) rename {repos/effect/packages => packages}/effect/src/reactivity/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/Rpc.ts (97%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcClient.ts (80%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcClientError.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcGroup.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcMessage.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcMiddleware.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcSchema.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcSerialization.ts (96%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcServer.ts (98%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcTest.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/RpcWorker.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/Utils.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/rpc/internal/tracing.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/Model.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/SchemaAOTCompiler.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/SchemaAOTCompiler/Build.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/SchemaCompiler.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/SchemaCompiler/runtime.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/SchemaJITCompiler.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/SchemaJITCompiler/enable.ts (100%) rename {repos/effect/packages => packages}/effect/src/schema/VariantSchema.ts (82%) rename {repos/effect/packages => packages}/effect/src/schema/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/socket/DatagramSocket.ts (100%) rename {repos/effect/packages => packages}/effect/src/socket/Socket.ts (96%) rename {repos/effect/packages => packages}/effect/src/socket/SocketServer.ts (100%) rename {repos/effect/packages => packages}/effect/src/socket/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/sql/Migrator.ts (95%) rename {repos/effect/packages => packages}/effect/src/sql/SqlClient.ts (62%) rename {repos/effect/packages => packages}/effect/src/sql/SqlConnection.ts (100%) rename {repos/effect/packages => packages}/effect/src/sql/SqlError.ts (100%) rename {repos/effect/packages => packages}/effect/src/sql/SqlModel.ts (100%) rename {repos/effect/packages => packages}/effect/src/sql/SqlResolver.ts (95%) rename {repos/effect/packages => packages}/effect/src/sql/SqlSchema.ts (100%) rename {repos/effect/packages => packages}/effect/src/sql/SqlStream.ts (100%) rename {repos/effect/packages => packages}/effect/src/sql/Statement.ts (96%) rename {repos/effect/packages => packages}/effect/src/sql/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/testing/TestClock.ts (98%) rename {repos/effect/packages => packages}/effect/src/testing/TestConsole.ts (100%) rename {repos/effect/packages => packages}/effect/src/testing/TestCrypto.ts (100%) rename {repos/effect/packages => packages}/effect/src/testing/TestSchema.ts (97%) rename {repos/effect/packages => packages}/effect/src/testing/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/workers/Transferable.ts (100%) rename {repos/effect/packages => packages}/effect/src/workers/Worker.ts (100%) rename {repos/effect/packages => packages}/effect/src/workers/WorkerError.ts (100%) rename {repos/effect/packages => packages}/effect/src/workers/WorkerRunner.ts (100%) rename {repos/effect/packages => packages}/effect/src/workers/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/Activity.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/DurableClock.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/DurableDeferred.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/DurableQueue.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/Workflow.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/WorkflowEngine.ts (94%) rename {repos/effect/packages => packages}/effect/src/workflow/WorkflowProxy.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/WorkflowProxyServer.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/index.ts (100%) rename {repos/effect/packages => packages}/effect/src/workflow/internal/crypto.ts (100%) rename {repos/effect/packages => packages}/effect/test/Array.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/ArrayNaNIndex.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/AtomRef.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/BigDecimal.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/BigInt.test.ts (78%) rename {repos/effect/packages => packages}/effect/test/Boolean.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Brand.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ByteSize.test.ts (100%) create mode 100644 packages/effect/test/Bytes.test.ts rename {repos/effect/packages => packages}/effect/test/Cache.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/Cause.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/CauseMapAnnotations.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Channel.test.ts (80%) rename {repos/effect/packages => packages}/effect/test/Chunk.test.ts (92%) rename {repos/effect/packages => packages}/effect/test/Clock.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Combiner.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Config.test.ts (99%) rename {repos/effect/packages => packages}/effect/test/ConfigProvider.test.ts (97%) rename {repos/effect/packages => packages}/effect/test/Context.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Cron.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/Crypto.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Data.test.ts (91%) rename {repos/effect/packages => packages}/effect/test/DateTime.test.ts (92%) rename {repos/effect/packages => packages}/effect/test/Deferred.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Duration.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/Effect.test.ts (91%) rename {repos/effect/packages => packages}/effect/test/EffectEager.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/EffectKeepAlive.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Effectable.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Equal.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Equivalence.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/ExecutionPlan.test.ts (100%) create mode 100644 packages/effect/test/Exit.test.ts rename {repos/effect/packages => packages}/effect/test/Fiber.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/FiberHandle.test.ts (88%) rename {repos/effect/packages => packages}/effect/test/FiberMap.test.ts (88%) rename {repos/effect/packages => packages}/effect/test/FiberSet.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/FiberSetRuntimePropagation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/FileSystem.test-utils.ts (58%) rename {repos/effect/packages => packages}/effect/test/Formatter.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Function.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Graph.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Hash.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/HashMap.test.ts (93%) rename {repos/effect/packages => packages}/effect/test/HashRing.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/HashSet.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/HttpClient.test.ts (91%) rename {repos/effect/packages => packages}/effect/test/Iterable.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/JsonPatch.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/JsonPointer.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/JsonSchema.test.ts (99%) rename {repos/effect/packages => packages}/effect/test/Latch.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Layer.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/LayerMap.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/LayerRef.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/LogLevel.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Logger.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/ManagedRuntime.test.ts (62%) rename {repos/effect/packages => packages}/effect/test/Match.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Metric.test.ts (94%) rename {repos/effect/packages => packages}/effect/test/Migrator.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/MutableHashMap.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/MutableHashSet.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/MutableList.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Newtype.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Number.test.ts (99%) rename {repos/effect/packages => packages}/effect/test/Optic.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Option.test.ts (94%) rename {repos/effect/packages => packages}/effect/test/Order.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Ordering.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/PartitionedSemaphore.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Path.test.ts (61%) rename {repos/effect/packages => packages}/effect/test/Pathfinding.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Pool.test.ts (99%) rename {repos/effect/packages => packages}/effect/test/Predicate.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/PubSub.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/Pull.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Queue.test.ts (99%) rename {repos/effect/packages => packages}/effect/test/Random.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/RcMap.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/RcRef.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Record.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Redacted.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Reducer.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Ref.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Request.test.ts (85%) create mode 100644 packages/effect/test/RequestResolver.test.ts rename {repos/effect/packages => packages}/effect/test/Resource.test.ts (53%) rename {repos/effect/packages => packages}/effect/test/Result.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Schedule.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/Scheduler.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Scope.test.ts (99%) rename {repos/effect/packages => packages}/effect/test/ScopedCache.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/ScopedRef.test.ts (100%) create mode 100644 packages/effect/test/SemVer.test.ts create mode 100644 packages/effect/test/SemVerRange.test.ts rename {repos/effect/packages => packages}/effect/test/Semaphore.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Sink.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/StackCapture.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/StackTraceLimit.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Stdio.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Stream.test.ts (92%) rename {repos/effect/packages => packages}/effect/test/String.test.ts (97%) rename {repos/effect/packages => packages}/effect/test/Struct.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/SubscriptionRef.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Symbol.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/SynchronizedRef.modifySomeEffect.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/SynchronizedRef.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TestClock.test.ts (82%) create mode 100644 packages/effect/test/TextDiff.test.ts rename {repos/effect/packages => packages}/effect/test/Tracer.test.ts (94%) rename {repos/effect/packages => packages}/effect/test/Trie.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/Tuple.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxChunk.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxDeferred.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxHashMap.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxHashSet.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxPriorityQueue.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxPubSub.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxQueue.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxReentrantLock.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxSemaphore.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/TxSubscriptionRef.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/UndefinedOr.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/Version.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/AiError.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/ai/AnthropicStructuredOutput.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/AnthropicStructuredOutputRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/Chat.test.ts (74%) rename {repos/effect/packages => packages}/effect/test/ai/DecisionModel.test.ts (87%) rename {repos/effect/packages => packages}/effect/test/ai/EmbeddingModel.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/LanguageModel.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/ai/LanguageModelRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/LanguageModelTrackerLifecycle.test.ts (100%) create mode 100644 packages/effect/test/ai/McpClient.test.ts rename {repos/effect/packages => packages}/effect/test/ai/McpSchema.test.ts (79%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/LayerHttpRequestScoped.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/BaseProtocolTest.ts (97%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/CompletionTest.ts (89%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/ElicitationTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/LifecycleTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/LoggingTest.ts (96%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/McpConformance.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/McpConformanceFixtures.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/McpTestPeer.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/MultiRoundTripTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/PromptsTest.ts (96%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/ResourcesTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/RootsTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/SamplingTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/SubscriptionsTest.ts (98%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/ToolsTest.ts (96%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/TransportsTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpConformance/UtilitiesTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpProtocol.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/McpServer.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/ProtocolAdapters.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/TestUtils/McpHttpHarness.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/TestUtils/McpHttpResponse.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/TestUtils/McpServerLayer.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/TestUtils/McpStdioHarness.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/TestUtils/McpWorkerdHarness.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/Workerd.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/fixtures/workerd.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/v2024_11_05.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/v2025_03_26.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/v2025_06_18.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/v2025_11_25.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/McpServer/v2026_07_28.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/Model.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/OpenAiStructuredOutput.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/OpenAiStructuredOutputRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/Prompt.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/Response.test.ts (89%) rename {repos/effect/packages => packages}/effect/test/ai/ResponseIdTracker.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/Tokenizer.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/Tool.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/ai/ToolRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/ai/utils.ts (100%) rename {repos/effect/packages => packages}/effect/test/arbitrary/Arbitrary.array.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/arbitrary/Arbitrary.causes.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/arbitrary/Arbitrary.composition.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/arbitrary/Arbitrary.configuration.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/arbitrary/Arbitrary.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/Ansi.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/Arguments.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/Command.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/cli/Errors.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/cli/Help.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/Lexer.test.ts (65%) rename {repos/effect/packages => packages}/effect/test/cli/LogLevel.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/Param.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/Primitive.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/cli/Prompt.test.ts (99%) create mode 100644 packages/effect/test/cli/TerminalCapabilities.test.ts rename {repos/effect/packages => packages}/effect/test/cli/completions/completions.test.ts (93%) rename {repos/effect/packages => packages}/effect/test/cli/completions/descriptor.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/fixtures/ComprehensiveCli.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/services/MockTerminal.ts (100%) rename {repos/effect/packages => packages}/effect/test/cli/services/TestActions.ts (100%) create mode 100644 packages/effect/test/cluster/ClusterCron.test.ts rename {repos/effect/packages => packages}/effect/test/cluster/ClusterWorkflowEngine.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/cluster/Entity.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/cluster/EntityProxy.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/Envelope.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/K8sHttpClient.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/MessageStorage.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/ResourceMap.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/ResourceRef.test.ts (93%) rename {repos/effect/packages => packages}/effect/test/cluster/RunnerServer.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/cluster/RunnerStorage.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/Runners.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/cluster/Sharding.test.ts (82%) rename {repos/effect/packages => packages}/effect/test/cluster/ShardingConfig.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/TestEntity.ts (100%) rename {repos/effect/packages => packages}/effect/test/cluster/interruptors.test.ts (100%) create mode 100644 packages/effect/test/devtools/DevToolsClient.test.ts rename {repos/effect/packages => packages}/effect/test/encoding/Base64.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/encoding/Base64Url.test.ts (88%) rename {repos/effect/packages => packages}/effect/test/encoding/EncodingError.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/encoding/Hex.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/encoding/Ini.test.ts (79%) create mode 100644 packages/effect/test/encoding/JsonArray.test.ts rename {repos/effect/packages => packages}/effect/test/encoding/Ndjson.test.ts (51%) rename {repos/effect/packages => packages}/effect/test/encoding/SchemaBinary.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/encoding/Sse.test.ts (100%) create mode 100644 packages/effect/test/encoding/Tar.test.ts rename {repos/effect/packages => packages}/effect/test/encoding/Toml.test.ts (82%) create mode 100644 packages/effect/test/encoding/Xml.test.ts rename {repos/effect/packages => packages}/effect/test/encoding/Yaml.test.ts (82%) create mode 100644 packages/effect/test/encoding/Zip.test.ts create mode 100644 packages/effect/test/eventlog/EventJournal.test.ts create mode 100644 packages/effect/test/eventlog/EventJournalConflictsTest.ts create mode 100644 packages/effect/test/eventlog/EventJournalIndexedDb.test.ts create mode 100644 packages/effect/test/eventlog/EventLog.test.ts rename {repos/effect/packages => packages}/effect/test/eventlog/EventLogIdentityDerivation.test.ts (100%) create mode 100644 packages/effect/test/eventlog/EventLogMessage.test.ts create mode 100644 packages/effect/test/eventlog/EventLogRemote.test.ts create mode 100644 packages/effect/test/eventlog/EventLogServerEncrypted.test.ts create mode 100644 packages/effect/test/eventlog/EventLogServerEncryptedTest.ts rename {repos/effect/packages => packages}/effect/test/eventlog/EventLogServerUnencrypted.test.ts (60%) create mode 100644 packages/effect/test/eventlog/EventLogServerUnencryptedRegressionTest.ts rename {repos/effect/packages => packages}/effect/test/eventlog/EventLogSessionAuth.test.ts (100%) create mode 100644 packages/effect/test/eventlog/EventRow.test.ts create mode 100644 packages/effect/test/eventlog/SqlEventJournalSharedTest.ts create mode 100644 packages/effect/test/eventlog/SqlEventLogServerEncryptedStorageTest.ts rename {repos/effect/packages => packages}/effect/test/eventlog/SqlEventLogServerUnencryptedStorageTest.ts (73%) rename {repos/effect/packages => packages}/effect/test/fixtures/migrator/0001_first.js (100%) create mode 100644 packages/effect/test/fixtures/tar/README.md create mode 100644 packages/effect/test/fixtures/tar/gnu.tar.gz create mode 100644 packages/effect/test/fixtures/tar/pax.tar.gz create mode 100644 packages/effect/test/fixtures/tar/sparse-gnu.tar.gz create mode 100644 packages/effect/test/fixtures/tar/sparse-pax.tar.gz create mode 100644 packages/effect/test/fixtures/tar/ustar.tar.gz create mode 100644 packages/effect/test/fixtures/tar/v7.tar.gz rename {repos/effect/packages => packages}/effect/test/fixtures/text.txt (100%) create mode 100644 packages/effect/test/fixtures/zip/7zip.zip create mode 100644 packages/effect/test/fixtures/zip/README.md create mode 100644 packages/effect/test/fixtures/zip/infozip-stream.zip create mode 100644 packages/effect/test/fixtures/zip/infozip.zip create mode 100644 packages/effect/test/fixtures/zip/python-zip64.zip rename {repos/effect/packages => packages}/effect/test/graphql/GraphQL.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLClient.makeTest.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLClient.pages.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLClient.subscriptions.test.ts (92%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLClient.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLClientError.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLProtocol.sse.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLProtocol.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLProtocol.webSocket.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/graphql/GraphQLResolver.test.ts (77%) rename {repos/effect/packages => packages}/effect/test/graphql/fixtures.ts (95%) rename {repos/effect/packages => packages}/effect/test/http/Cookies.test.ts (100%) create mode 100644 packages/effect/test/http/FetchHttpClient.test.ts rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/case-insensitive.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/matching-order.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/methods.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/optional-params.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/params-collisions.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/path-params-match.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/querystring.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/FindMyWay/regex.test.ts (100%) create mode 100644 packages/effect/test/http/Headers.test.ts rename {repos/effect/packages => packages}/effect/test/http/HttpBody.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/HttpClient.test.ts (93%) rename {repos/effect/packages => packages}/effect/test/http/HttpClientRequest.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/HttpCompression.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/HttpDecompression.test.ts (87%) rename {repos/effect/packages => packages}/effect/test/http/HttpEffect.test.ts (94%) rename {repos/effect/packages => packages}/effect/test/http/HttpMiddleware.test.ts (86%) rename {repos/effect/packages => packages}/effect/test/http/HttpPlatform.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/HttpRouter.test.ts (64%) rename {repos/effect/packages => packages}/effect/test/http/HttpServer.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/HttpServerError.test.ts (100%) create mode 100644 packages/effect/test/http/HttpServerRequest.body.test.ts rename {repos/effect/packages => packages}/effect/test/http/HttpServerRequest.test.ts (93%) rename {repos/effect/packages => packages}/effect/test/http/HttpServerRespondable.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/HttpServerResponse.test.ts (92%) rename {repos/effect/packages => packages}/effect/test/http/HttpStaticServer.test.ts (75%) rename {repos/effect/packages => packages}/effect/test/http/HttpStatus.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/HttpTraceContext.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/Mime.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/Multipart.test.ts (91%) rename {repos/effect/packages => packages}/effect/test/http/SchemaRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/Template.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/http/Url.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/http/UrlParams.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApi.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApiBuilder.test.ts (87%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApiClient.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApiDocumentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApiEndpoint.test.ts (86%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApiError.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApiSchema.test.ts (91%) rename {repos/effect/packages => packages}/effect/test/httpapi/HttpApiSecurity.test.ts (100%) create mode 100644 packages/effect/test/httpapi/HttpApiTest.test.ts rename {repos/effect/packages => packages}/effect/test/httpapi/OpenApi.test.ts (93%) rename {repos/effect/packages => packages}/effect/test/httpapi/OpenApiRepresentation.test.ts (76%) rename {repos/effect/packages => packages}/effect/test/internal/Uuid.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/net/AddressResolver.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/net/Dns.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/net/Host.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/net/IpInterface.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/net/IpNetwork.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/net/NetAddress.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/observability/OtlpCompression.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/observability/OtlpConfig.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/observability/OtlpEnvHeaders.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/observability/OtlpExporter.test.ts (97%) rename {repos/effect/packages => packages}/effect/test/observability/OtlpLogger.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/observability/OtlpMetrics.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/observability/OtlpResource.test.ts (90%) create mode 100644 packages/effect/test/observability/OtlpSerialization.test.ts rename {repos/effect/packages => packages}/effect/test/observability/OtlpTracer.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/observability/PrometheusMetrics.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/persistence/KeyValueStore.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/persistence/KeyValueStoreTest.ts (100%) rename {repos/effect/packages => packages}/effect/test/persistence/PersistedCache.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/persistence/PersistedCacheTest.ts (70%) rename {repos/effect/packages => packages}/effect/test/persistence/PersistedQueue.test.ts (100%) create mode 100644 packages/effect/test/persistence/PersistedQueueSqlTest.ts rename {repos/effect/packages => packages}/effect/test/persistence/PersistedQueueTest.ts (83%) rename {repos/effect/packages => packages}/effect/test/persistence/RateLimiter.test.ts (92%) rename {repos/effect/packages => packages}/effect/test/persistence/RateLimiterTest.ts (87%) rename {repos/effect/packages => packages}/effect/test/persistence/Redis.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/persistence/SqlCleanupTest.ts (100%) create mode 100644 packages/effect/test/persistence/UserDirectories.test.ts rename {repos/effect/packages => packages}/effect/test/process/ChildProcess.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/process/ChildProcessSpawnerTest.ts (99%) rename {repos/effect/packages => packages}/effect/test/process/fixtures/bash/no-permissions.sh (100%) rename {repos/effect/packages => packages}/effect/test/process/fixtures/bash/parent-exits-early.sh (100%) rename {repos/effect/packages => packages}/effect/test/process/fixtures/bash/spawn-children.sh (100%) rename {repos/effect/packages => packages}/effect/test/process/fixtures/config/SHOUTING (100%) rename {repos/effect/packages => packages}/effect/test/process/fixtures/config/integer (100%) rename {repos/effect/packages => packages}/effect/test/process/fixtures/config/nested/config (100%) rename {repos/effect/packages => packages}/effect/test/process/fixtures/config/secret (100%) rename {repos/effect/packages => packages}/effect/test/reactivity/AsyncResult.test.ts (88%) rename {repos/effect/packages => packages}/effect/test/reactivity/Atom.test.ts (88%) rename {repos/effect/packages => packages}/effect/test/reactivity/AtomHttpApi.test.ts (81%) rename {repos/effect/packages => packages}/effect/test/reactivity/AtomRpc.test.ts (78%) create mode 100644 packages/effect/test/reactivity/Hydration.test.ts rename {repos/effect/packages => packages}/effect/test/rpc/Rpc.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/rpc/RpcClient.test.ts (58%) rename {repos/effect/packages => packages}/effect/test/rpc/RpcSerialization.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/rpc/RpcServer.test.ts (85%) rename {repos/effect/packages => packages}/effect/test/schema/CodePoints.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/Graph.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/HMR.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/Schema.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaAOTCompiler.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaAOTCompilerBuild.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaAST.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaCompilerApi.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaCompilerArray.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaCompilerConcurrency.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaCompilerConstruction.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaCompilerRegression.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaCompilerStartup.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaGetter.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaIssue.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaJITCompiler.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaJITCompilerCheckedSources.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaJITCompilerExcessProperties.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaJITCompilerFallback.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaParser.test.ts (89%) rename {repos/effect/packages => packages}/effect/test/schema/SchemaTransformation.test.ts (66%) rename {repos/effect/packages => packages}/effect/test/schema/TemplateLiteral.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/VariantSchema.test.ts (79%) rename {repos/effect/packages => packages}/effect/test/schema/fixtures/HMR-Class.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/fixtures/HMR-sanity-check.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/fixtures/aot-build.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/fixtures/aot-import-guard.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/fixtures/aot-runner.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/fixtures/aot.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/fixtures/construction.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/jsonSchemaRoundTrip.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/builtInRevivers.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/schema/representation/fromJson.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/fromJsonMultiDocument.test.ts (100%) create mode 100644 packages/effect/test/schema/representation/fromJsonSchemaAnnotations.test.ts rename {repos/effect/packages => packages}/effect/test/schema/representation/fromJsonSchemaDocument.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/schema/representation/fromJsonSchemaMultiDocument.test.ts (95%) rename {repos/effect/packages => packages}/effect/test/schema/representation/fromRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/fromRepresentations.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/makeCode.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/schemaToJsonSchemaDocument.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/stringTreeRoundtrip.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toCodeDocument.annotations.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toCodeDocument.test.ts (99%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toJson.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toJsonMultiDocument.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toJsonSchemaDocument.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toJsonSchemaMultiDocument.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toMultiDocument.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/representation/toRepresentations.test.ts (100%) create mode 100644 packages/effect/test/schema/standardSchemaAdapters.test.ts rename {repos/effect/packages => packages}/effect/test/schema/toCodec.test.ts (96%) rename {repos/effect/packages => packages}/effect/test/schema/toDifferJsonPatch.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/toEquivalence.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/toFormatter.test.ts (94%) rename {repos/effect/packages => packages}/effect/test/schema/toIso.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/schema/toJsonSchemaDocument.test.ts (94%) rename {repos/effect/packages => packages}/effect/test/schema/toStandardJSONSchemaV1.test.ts (94%) rename {repos/effect/packages => packages}/effect/test/schema/toStandardSchemaV1.test.ts (74%) rename {repos/effect/packages => packages}/effect/test/schema/v3-v4.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/socket/DatagramSocket.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/socket/Socket.test.ts (83%) create mode 100644 packages/effect/test/sql/SqlClient.test.ts rename {repos/effect/packages => packages}/effect/test/sql/SqlError.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/sql/SqlResolver.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/sql/SqlSchema.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/sql/Statement.test.ts (78%) rename {repos/effect/packages => packages}/effect/test/testing/TestCrypto.test.ts (61%) rename {repos/effect/packages => packages}/effect/test/testing/TestSchema.test.ts (98%) rename {repos/effect/packages => packages}/effect/test/utils/assert.ts (100%) rename {repos/effect/packages => packages}/effect/test/utils/chunkCoordination.ts (100%) rename {repos/effect/packages => packages}/effect/test/utils/counter.ts (100%) rename {repos/effect/packages => packages}/effect/test/utils/gc.ts (100%) rename {repos/effect/packages => packages}/effect/test/workers/Worker.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/workers/WorkerError.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/workflow/DurableQueue.test.ts (90%) rename {repos/effect/packages => packages}/effect/test/workflow/Workflow.test.ts (100%) rename {repos/effect/packages => packages}/effect/test/workflow/WorkflowEngine.test.ts (82%) rename {repos/effect/packages => packages}/effect/tsconfig.json (100%) rename {repos/effect/packages => packages}/effect/typeperf/README.md (100%) rename {repos/effect/packages => packages}/effect/typeperf/compare.mjs (100%) rename {repos/effect/packages => packages}/effect/typeperf/compare/httpapi.mjs (100%) rename {repos/effect/packages => packages}/effect/typeperf/config.json (100%) rename {repos/effect/packages => packages}/effect/typeperf/run.mjs (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/graphql-selections/baseline.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/graphql-selections/fixtures/opaque-class-count-5.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/graphql-selections/fixtures/struct-interface-count-5.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/graphql-selections/thresholds.json (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/baseline.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/_endpoint-selection-types.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/_grouped-api-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/_grouped-api-types.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/builder-endpoint-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/builder-handle-all-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/builder-handle-all-two-batches-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/builder-handlers-count-100.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/builder-raw-handlers-count-100.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/client-endpoint-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/client-group-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/client-groups-100x5-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/client-methods-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/client-top-level-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/endpoint-class-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/endpoint-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/group-endpoints-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/url-builder-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/fixtures/url-builder-top-level-count-500.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/httpapi/thresholds.json (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/baseline.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/array.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/brand.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/decode-unknown-effect.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/encode-unknown-effect.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/exit.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/non-empty-array.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/record.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/struct-mixed.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/struct-mutable-only.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/struct-optional-only.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/struct-required.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/struct-with-rest.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/tagged-union.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/to-codec-array-from-single.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/to-codec-json.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/to-codec-string-tree.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/to-encoded.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/to-standard-json-schema.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/to-type.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/tuple-with-rest.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/tuple.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/fixtures/union.ts (100%) rename {repos/effect/packages => packages}/effect/typeperf/suites/schema/thresholds.json (100%) rename {repos/effect/packages => packages}/effect/typetest/AiError.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Array.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Brand.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/ByteSize.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Channel.tst.ts (87%) rename {repos/effect/packages => packages}/effect/typetest/ChannelSchema.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Clock.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Config.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/ConfigProvider.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Context.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Crypto.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Data.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Effect.tst.ts (98%) rename {repos/effect/packages => packages}/effect/typetest/Effectable.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Equivalence.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Fiber.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/FileSystem.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Function.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Graph.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Iterable.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Layer.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Match.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/MigratedSubpaths.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Newtype.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Optic.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Option.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Order.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Predicate.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Queue.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Random.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Record.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Result.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Schedule.tst.ts (96%) rename {repos/effect/packages => packages}/effect/typetest/Scope.tst.ts (100%) create mode 100644 packages/effect/typetest/SemVer.tst.ts rename {repos/effect/packages => packages}/effect/typetest/Sink.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Stream.tst.ts (93%) create mode 100644 packages/effect/typetest/String.tst.ts rename {repos/effect/packages => packages}/effect/typetest/Struct.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/SynchronizedRef.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/TestSchema.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Tuple.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/Types.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/VariantSchema.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/ai/DecisionModel.tst.ts (85%) rename {repos/effect/packages => packages}/effect/typetest/ai/LanguageModel.tst.ts (100%) create mode 100644 packages/effect/typetest/ai/McpClient.tst.ts create mode 100644 packages/effect/typetest/ai/McpSchema.tst.ts rename {repos/effect/packages => packages}/effect/typetest/ai/McpServer.tst.ts (98%) rename {repos/effect/packages => packages}/effect/typetest/ai/Response.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/ai/Tool.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/ai/Toolkit.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/arbitrary/Arbitrary.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/cli/Command.tst.ts (88%) rename {repos/effect/packages => packages}/effect/typetest/cli/Param.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/cli/Prompt.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/cluster/EntityProxyServer.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/cluster/K8sHttpClient.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/cluster/Reply.tst.ts (100%) create mode 100644 packages/effect/typetest/encoding/Base64Url.tst.ts create mode 100644 packages/effect/typetest/encoding/JsonArray.tst.ts rename {repos/effect/packages => packages}/effect/typetest/encoding/Ndjson.tst.ts (100%) create mode 100644 packages/effect/typetest/encoding/Tar.tst.ts create mode 100644 packages/effect/typetest/encoding/Xml.tst.ts create mode 100644 packages/effect/typetest/encoding/Zip.tst.ts create mode 100644 packages/effect/typetest/eventlog/EventLog.tst.ts create mode 100644 packages/effect/typetest/eventlog/EventRow.tst.ts rename {repos/effect/packages => packages}/effect/typetest/graphql/GraphQL.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/graphql/GraphQLClient.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/graphql/GraphQLMiddleware.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/graphql/GraphQLProtocol.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/graphql/GraphQLResolver.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/http/HttpClient.tst.ts (100%) create mode 100644 packages/effect/typetest/http/HttpMiddleware.tst.ts rename {repos/effect/packages => packages}/effect/typetest/http/HttpPlatform.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/http/HttpRouter.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/http/HttpServer.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/HttpApi.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/HttpApiBuilder.tst.ts (93%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/HttpApiClient.tst.ts (94%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/HttpApiEndpoint.tst.ts (95%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/HttpApiGroup.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/HttpApiMiddleware.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/HttpApiSchema.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/httpapi/OpenApiRepresentation.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/net/AddressResolver.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/net/Dns.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/net/IpInterface.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/net/IpNetwork.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/net/NetAddress.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/reactivity/AsyncResult.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/reactivity/Atom.tst.ts (62%) rename {repos/effect/packages => packages}/effect/typetest/reactivity/AtomHttpApi.tst.ts (77%) rename {repos/effect/packages => packages}/effect/typetest/reactivity/AtomRpc.tst.ts (70%) create mode 100644 packages/effect/typetest/reactivity/Reactivity.tst.ts create mode 100644 packages/effect/typetest/reactivity/ReactivityKeys.tst.ts rename {repos/effect/packages => packages}/effect/typetest/rpc/Rpc.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/rpc/RpcClient.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/rpc/RpcGroup.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/rpc/RpcMessage.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/rpc/RpcMiddleware.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/rpc/RpcServer.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Array.tst.ts (100%) create mode 100644 packages/effect/typetest/schema/Enum.tst.ts rename {repos/effect/packages => packages}/effect/typetest/schema/FromJsonSchema.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Graph.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Literals.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Record.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Schema.tst.ts (99%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaAOTCompiler.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaAOTCompilerBuild.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaBuiltInRevivers.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaCompiler.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaGetter.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaJsonSchemaConsumer.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaRepresentation.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaRepresentationCompilers.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/SchemaRepresentationReviver.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Struct.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Tuple.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/Union.tst.ts (92%) rename {repos/effect/packages => packages}/effect/typetest/schema/api.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/options.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/schema/toIso.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/socket/Socket.tst.ts (100%) create mode 100644 packages/effect/typetest/sql/SqlClient.tst.ts rename {repos/effect/packages => packages}/effect/typetest/sql/SqlSchema.tst.ts (100%) rename {repos/effect/packages => packages}/effect/typetest/workflow/Workflow.tst.ts (100%) delete mode 100644 packages/eslint-codegen-model/CHANGELOG.md delete mode 100644 packages/eslint-codegen-model/examples/model-facades.ts delete mode 100644 packages/eslint-codegen-model/examples/workflow-facades.ts delete mode 100644 packages/eslint-codegen-model/index.js delete mode 100644 packages/eslint-codegen-model/package.json delete mode 100644 packages/eslint-codegen-model/src/cli.ts delete mode 100644 packages/eslint-codegen-model/src/index.ts delete mode 100644 packages/eslint-codegen-model/src/js-yaml.d.ts delete mode 100644 packages/eslint-codegen-model/src/normalise.ts delete mode 100644 packages/eslint-codegen-model/src/oxlint/index.ts delete mode 100644 packages/eslint-codegen-model/src/oxlint/rules/codegen.ts delete mode 100644 packages/eslint-codegen-model/src/presets/barrel.ts delete mode 100644 packages/eslint-codegen-model/src/presets/index.ts delete mode 100644 packages/eslint-codegen-model/src/presets/meta.ts delete mode 100644 packages/eslint-codegen-model/src/presets/model-facade.ts delete mode 100644 packages/eslint-codegen-model/src/presets/model.ts delete mode 100644 packages/eslint-codegen-model/src/shared/codegen-block.ts delete mode 100644 packages/eslint-codegen-model/src/shared/native-type-resolver.ts delete mode 100644 packages/eslint-codegen-model/src/shared/type-resolver.ts delete mode 100644 packages/eslint-codegen-model/tsconfig.json delete mode 100644 packages/eslint-shared-config/CHANGELOG.md delete mode 100644 packages/eslint-shared-config/package.json delete mode 100644 packages/eslint-shared-config/src/dprint.jsonc delete mode 100644 packages/eslint-shared-config/src/eslint.base.config.mjs delete mode 100644 packages/eslint-shared-config/src/eslint.vue.config.mjs delete mode 100644 packages/eslint-shared-config/src/oxlintrc.json delete mode 100644 packages/eslint-shared-config/src/plugin-effect-app.mjs delete mode 100644 packages/eslint-shared-config/test/no-await-effect.test.mts delete mode 100644 packages/eslint-shared-config/vitest.config.ts delete mode 100644 packages/infra/.madgerc delete mode 100644 packages/infra/.npmignore delete mode 100644 packages/infra/CHANGELOG.md delete mode 100644 packages/infra/_check.sh delete mode 100644 packages/infra/docs/cluster-storage.md delete mode 100644 packages/infra/docs/workflow-engine.md delete mode 100644 packages/infra/examples/query.ts delete mode 100644 packages/infra/package.json delete mode 100644 packages/infra/src/CUPS.ts delete mode 100644 packages/infra/src/ClusterCosmos.ts delete mode 100644 packages/infra/src/ClusterServiceBus.ts delete mode 100644 packages/infra/src/ContextProvider.ts delete mode 100644 packages/infra/src/Emailer/Sendgrid.ts delete mode 100644 packages/infra/src/Emailer/fake.ts delete mode 100644 packages/infra/src/MainFiberSet.ts delete mode 100644 packages/infra/src/QueueMaker/errors.ts delete mode 100644 packages/infra/src/SQL.ts delete mode 100644 packages/infra/src/SQL/Model.ts delete mode 100644 packages/infra/src/Store/Cosmos.ts delete mode 100644 packages/infra/src/Store/Cosmos/query.ts delete mode 100644 packages/infra/src/Store/Disk.ts delete mode 100644 packages/infra/src/Store/Memory.ts delete mode 100644 packages/infra/src/Store/SQL.ts delete mode 100644 packages/infra/src/Store/SQL/Pg.ts delete mode 100644 packages/infra/src/Store/SQL/query.ts delete mode 100644 packages/infra/src/Store/codeFilter.ts delete mode 100644 packages/infra/src/Store/index.test.ts.bak delete mode 100644 packages/infra/src/Store/index.ts delete mode 100644 packages/infra/src/Store/jsonDocument.ts delete mode 100644 packages/infra/src/Store/utils.ts delete mode 100644 packages/infra/src/WorkflowEngineCosmos.ts delete mode 100644 packages/infra/src/WorkflowEngineSqlite.ts delete mode 100644 packages/infra/src/arbs.ts delete mode 100644 packages/infra/src/codec.ts delete mode 100644 packages/infra/src/cosmos-client.ts delete mode 100644 packages/infra/src/errorReporter.ts delete mode 100644 packages/infra/src/errors.ts delete mode 100644 packages/infra/src/fileUtil.ts delete mode 100644 packages/infra/src/index.ts delete mode 100644 packages/infra/src/internal/RequestContextMiddleware.ts delete mode 100644 packages/infra/src/internal/auth.ts delete mode 100644 packages/infra/src/internal/events.ts delete mode 100644 packages/infra/src/internal/health.ts delete mode 100644 packages/infra/src/layerUtils.ts delete mode 100644 packages/infra/src/logger.ts delete mode 100644 packages/infra/src/logger/jsonLogger.ts delete mode 100644 packages/infra/src/logger/logFmtLogger.ts delete mode 100644 packages/infra/src/logger/shared.ts delete mode 100644 packages/infra/src/middlewares.ts delete mode 100644 packages/infra/src/mongo-client.ts delete mode 100644 packages/infra/src/otel.ts delete mode 100644 packages/infra/src/rateLimit.ts delete mode 100644 packages/infra/src/redis-client.ts delete mode 100644 packages/infra/src/reportError.ts delete mode 100644 packages/infra/src/routing.ts delete mode 100644 packages/infra/src/routing/middleware.ts delete mode 100644 packages/infra/src/routing/middleware/RouterMiddleware.ts delete mode 100644 packages/infra/src/routing/middleware/middleware.ts delete mode 100644 packages/infra/src/routing/schema/jwt.ts delete mode 100644 packages/infra/src/routing/tsort.ts delete mode 100644 packages/infra/src/routing/utils.ts delete mode 100644 packages/infra/src/test.ts delete mode 100644 packages/infra/src/util.ts delete mode 100644 packages/infra/src/vitest.ts delete mode 100644 packages/infra/test/arbs-magnitude.test.ts delete mode 100644 packages/infra/test/arbs.test.ts delete mode 100644 packages/infra/test/auth.test.ts delete mode 100644 packages/infra/test/cluster-cosmos.test.ts delete mode 100644 packages/infra/test/cluster-servicebus.test.ts delete mode 100644 packages/infra/test/cluster-sqlite.test.ts delete mode 100644 packages/infra/test/contextProvider.test.ts delete mode 100644 packages/infra/test/controller.test.ts delete mode 100644 packages/infra/test/cosmos-container-throughput.test.ts delete mode 100644 packages/infra/test/cosmos-query.test.ts delete mode 100644 packages/infra/test/fixtures.ts delete mode 100644 packages/infra/test/json-lower.test.ts delete mode 100644 packages/infra/test/layerUtils.test.ts delete mode 100644 packages/infra/test/query.test.ts delete mode 100644 packages/infra/test/rawQuery.test.ts delete mode 100644 packages/infra/test/repository-ext.test.ts delete mode 100644 packages/infra/test/repository-legacy-document.test.ts delete mode 100644 packages/infra/test/requires.test.ts delete mode 100644 packages/infra/test/router-generator.test.ts delete mode 100644 packages/infra/test/routing-interruptibility.test.ts delete mode 100644 packages/infra/test/rpc-context-map-streaming.test.ts delete mode 100644 packages/infra/test/rpc-e2e-invalidation.test.ts delete mode 100644 packages/infra/test/rpc-multi-middleware.test.ts delete mode 100644 packages/infra/test/rpc-stream-fullstack.test.ts delete mode 100644 packages/infra/test/sql-store.test.ts delete mode 100644 packages/infra/test/validateSample.test.ts delete mode 100644 packages/infra/test/workflow-engine-cosmos.test.ts delete mode 100644 packages/infra/test/workflow-engine-sqlite.test.ts delete mode 100644 packages/infra/tsconfig.examples.json delete mode 100644 packages/infra/tsconfig.json delete mode 100644 packages/infra/tsconfig.src.json delete mode 100644 packages/infra/tsconfig.test.json delete mode 100644 packages/infra/types/modules.d.ts delete mode 100644 packages/infra/vitest.config.ts rename {repos/effect/packages => packages}/opentelemetry/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/opentelemetry/LICENSE (100%) rename {repos/effect/packages => packages}/opentelemetry/README.md (100%) rename {repos/effect/packages => packages}/opentelemetry/package.json (99%) rename {repos/effect/packages => packages}/opentelemetry/src/NodeSdk.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/src/OtelLogger.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/src/OtelMetrics.ts (84%) rename {repos/effect/packages => packages}/opentelemetry/src/OtelTracer.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/src/Resource.ts (93%) rename {repos/effect/packages => packages}/opentelemetry/src/WebSdk.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/src/index.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/src/internal/attributes.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/src/internal/metrics.ts (94%) rename {repos/effect/packages => packages}/opentelemetry/src/internal/utilities.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/test/OtelLogger.test.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/test/OtelMetrics.test.ts (83%) rename {repos/effect/packages => packages}/opentelemetry/test/OtelTracer.test.ts (100%) rename {repos/effect/packages => packages}/opentelemetry/test/Resource.test.ts (52%) rename {repos/effect/packages => packages}/opentelemetry/tsconfig.json (100%) rename {repos/effect/packages => packages}/platform/browser/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/platform/browser/LICENSE (100%) rename {repos/effect/packages => packages}/platform/browser/README.md (100%) rename {repos/effect/packages => packages}/platform/browser/package.json (98%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserCrypto.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserHttpClient.ts (92%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserKeyValueStore.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserPersistence.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserRuntime.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserSocket.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserStream.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserWorker.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/BrowserWorkerRunner.ts (98%) rename {repos/effect/packages => packages}/platform/browser/src/Clipboard.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/Geolocation.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/IndexedDb.ts (97%) rename {repos/effect/packages => packages}/platform/browser/src/IndexedDbDatabase.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/IndexedDbQueryBuilder.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/IndexedDbTable.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/IndexedDbVersion.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/Permissions.ts (100%) rename {repos/effect/packages => packages}/platform/browser/src/index.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/BrowserCrypto.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/BrowserHttpClient.test.ts (80%) rename {repos/effect/packages => packages}/platform/browser/test/BrowserKeyValueStore.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/BrowserPersistence.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/BrowserPersistencePersistedCache.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/BrowserWorkerRunner.test.ts (83%) rename {repos/effect/packages => packages}/platform/browser/test/IndexedDbDatabase.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/IndexedDbQueryBuilder.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/IndexedDbTable.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/IndexedDbVersion.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/Permissions.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/RpcWorker.test.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/fixtures/rpc-e2e.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/fixtures/rpc-schemas.ts (100%) rename {repos/effect/packages => packages}/platform/browser/test/fixtures/rpc-worker.ts (100%) rename {repos/effect/packages => packages}/platform/browser/tsconfig.json (100%) rename {repos/effect/packages => packages}/platform/browser/vitest.setup.ts (100%) rename {repos/effect/packages => packages}/platform/bun/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/platform/bun/LICENSE (100%) rename {repos/effect/packages => packages}/platform/bun/README.md (100%) rename {repos/effect/packages => packages}/platform/bun/benchmark/DatagramSocket.ts (100%) rename {repos/effect/packages => packages}/platform/bun/package.json (95%) rename {repos/effect/packages => packages}/platform/bun/src/BunAddressResolver.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunChildProcessSpawner.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunClusterHttp.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunClusterSocket.ts (95%) rename {repos/effect/packages => packages}/platform/bun/src/BunCrypto.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunDatagramSocket.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunDns.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunFileSystem.ts (100%) create mode 100644 packages/platform/bun/src/BunHttpClient.ts rename {repos/effect/packages => packages}/platform/bun/src/BunHttpPlatform.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunHttpServer.ts (91%) rename {repos/effect/packages => packages}/platform/bun/src/BunHttpServerRequest.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunMultipart.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunPath.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunRedis.ts (74%) rename {repos/effect/packages => packages}/platform/bun/src/BunRuntime.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunServices.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunSink.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunSocket.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunSocketServer.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunStdio.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunStream.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunTerminal.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunWorker.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/BunWorkerRunner.ts (100%) rename {repos/effect/packages => packages}/platform/bun/src/index.ts (100%) rename {repos/effect/packages => packages}/platform/bun/test/BunAddressResolver.test.ts (100%) rename {repos/effect/packages => packages}/platform/bun/test/BunClusterHttp.test.ts (100%) rename {repos/effect/packages => packages}/platform/bun/test/BunDatagramSocket.test.ts (95%) rename {repos/effect/packages => packages}/platform/bun/test/BunDns.integration.test.ts (100%) create mode 100644 packages/platform/bun/test/BunHttpClient.test.ts rename {repos/effect/packages => packages}/platform/bun/test/BunHttpCompression.test.ts (100%) rename {repos/effect/packages => packages}/platform/bun/test/BunHttpPlatform.test.ts (100%) rename {repos/effect/packages => packages}/platform/bun/test/BunHttpServer.test.ts (72%) rename {repos/effect/packages => packages}/platform/bun/test/BunMultipart.test.ts (100%) rename {repos/effect/packages => packages}/platform/bun/test/BunSocket.test.ts (100%) rename {repos/effect/packages => packages}/platform/bun/test/BunStream.test.ts (100%) rename {repos/effect/packages => packages}/platform/bun/tsconfig.json (100%) rename {repos/effect/packages => packages}/platform/deno/CHANGELOG.md (80%) rename {repos/effect/packages => packages}/platform/deno/LICENSE (100%) rename {repos/effect/packages => packages}/platform/deno/README.md (100%) rename {repos/effect/packages => packages}/platform/deno/benchmark/DatagramSocket.ts (100%) rename {repos/effect/packages => packages}/platform/deno/package.json (94%) rename {repos/effect/packages => packages}/platform/deno/src/DenoAddressResolver.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoChildProcessSpawner.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoClusterHttp.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoClusterSocket.ts (89%) rename {repos/effect/packages => packages}/platform/deno/src/DenoCrypto.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoDatagramSocket.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoDns.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoFileSystem.ts (99%) create mode 100644 packages/platform/deno/src/DenoHttpClient.ts rename {repos/effect/packages => packages}/platform/deno/src/DenoHttpPlatform.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoHttpServer.ts (82%) rename {repos/effect/packages => packages}/platform/deno/src/DenoHttpServerRequest.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoKeyValueStore.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoMultipart.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoPath.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoRedis.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoRuntime.ts (89%) rename {repos/effect/packages => packages}/platform/deno/src/DenoServices.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoSocket.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoSocketServer.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoStdio.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoTerminal.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoWorker.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/DenoWorkerRunner.ts (98%) rename {repos/effect/packages => packages}/platform/deno/src/index.ts (100%) rename {repos/effect/packages => packages}/platform/deno/src/internal/error.ts (98%) rename {repos/effect/packages => packages}/platform/deno/test/DenoAddressResolver.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoChildProcessSpawner.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoCrypto.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoDatagramSocket.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoDatagramSocketUnsupported.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoDns.integration.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoFileSystem.test.ts (100%) create mode 100644 packages/platform/deno/test/DenoHttpClient.test.ts rename {repos/effect/packages => packages}/platform/deno/test/DenoHttpCompression.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoHttpPlatform.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoHttpServer.test.ts (69%) rename {repos/effect/packages => packages}/platform/deno/test/DenoKeyValueStore.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoPath.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoRedis.integration.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoSocket.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoSocketServer.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoStdio.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoTerminal.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/DenoWorkerRunner.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/RpcWorker.test.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/cluster/SocketRunner.test.ts (78%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/deno-terminal.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/rpc-e2e.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/rpc-schemas.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/rpc-worker.ts (100%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/text.txt (100%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/tls/ca.pem (100%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/tls/cert.pem (100%) rename {repos/effect/packages => packages}/platform/deno/test/fixtures/tls/key.pem (100%) rename {repos/effect/packages => packages}/platform/deno/test/internal/error.test.ts (95%) rename {repos/effect/packages => packages}/platform/deno/tsconfig.json (100%) rename {repos/effect/packages => packages}/platform/node-shared/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/platform/node-shared/LICENSE (100%) rename {repos/effect/packages => packages}/platform/node-shared/README.md (100%) rename {repos/effect/packages => packages}/platform/node-shared/benchmark/DatagramSocket.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/benchmark/Socket.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/package.json (95%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeAddressResolver.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeChildProcessSpawner.ts (99%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeClusterSocket.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeCrypto.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeDatagramSocket.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeDns.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeFileSystem.ts (97%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeHttpCompression.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodePath.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeRuntime.ts (100%) create mode 100644 packages/platform/node-shared/src/NodeSink.ts rename {repos/effect/packages => packages}/platform/node-shared/src/NodeSocket.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeSocketServer.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeStdio.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeStream.ts (82%) rename {repos/effect/packages => packages}/platform/node-shared/src/NodeTerminal.ts (91%) rename {repos/effect/packages => packages}/platform/node-shared/src/index.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/src/internal/nodeChildProcessSpawner.ts (100%) create mode 100644 packages/platform/node-shared/src/internal/stream.ts rename {repos/effect/packages => packages}/platform/node-shared/src/internal/utils.ts (98%) rename {repos/effect/packages => packages}/platform/node-shared/test/AddressResolver.test-utils.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/test/Dns.test-utils.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/test/HttpPlatform.test-utils.ts (100%) create mode 100644 packages/platform/node-shared/test/HttpServerHttp2.test-utils.ts rename {repos/effect/packages => packages}/platform/node-shared/test/NodeAddressResolver.test.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/test/NodeChildProcessSpawner.test.ts (84%) rename {repos/effect/packages => packages}/platform/node-shared/test/NodeDatagramSocket.test.ts (90%) rename {repos/effect/packages => packages}/platform/node-shared/test/NodeDns.integration.test.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/test/NodeFileSystem.test.ts (88%) rename {repos/effect/packages => packages}/platform/node-shared/test/NodePath.test.ts (100%) create mode 100644 packages/platform/node-shared/test/NodeSink.test.ts rename {repos/effect/packages => packages}/platform/node-shared/test/NodeStdio.test.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/test/NodeStream.test.ts (68%) rename {repos/effect/packages => packages}/platform/node-shared/test/NodeTerminal.test.ts (94%) rename {repos/effect/packages => packages}/platform/node-shared/test/fixtures/buffered-input.ts (100%) rename {repos/effect/packages => packages}/platform/node-shared/test/fixtures/helloworld.tar.gz (100%) rename {repos/effect/packages => packages}/platform/node-shared/test/fixtures/node-terminal.ts (81%) rename {repos/effect/packages => packages}/platform/node-shared/test/fixtures/process-group.ts (64%) rename {repos/effect/packages => packages}/platform/node-shared/tsconfig.json (100%) rename {repos/effect/packages => packages}/platform/node/CHANGELOG.md (86%) rename {repos/effect/packages => packages}/platform/node/LICENSE (100%) rename {repos/effect/packages => packages}/platform/node/README.md (100%) rename {repos/effect/packages => packages}/platform/node/package.json (87%) rename {repos/effect/packages => packages}/platform/node/src/NodeAddressResolver.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeChildProcessSpawner.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeClusterHttp.ts (98%) rename {repos/effect/packages => packages}/platform/node/src/NodeClusterSocket.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeCrypto.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeDatagramSocket.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeDns.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeFileSystem.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeHttpClient.ts (68%) rename {repos/effect/packages => packages}/platform/node/src/NodeHttpIncomingMessage.ts (94%) rename {repos/effect/packages => packages}/platform/node/src/NodeHttpPlatform.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeHttpServer.ts (52%) rename {repos/effect/packages => packages}/platform/node/src/NodeHttpServerRequest.ts (60%) rename {repos/effect/packages => packages}/platform/node/src/NodeMultipart.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeMultipartParser.ts (95%) rename {repos/effect/packages => packages}/platform/node/src/NodePath.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeRedis.ts (75%) rename {repos/effect/packages => packages}/platform/node/src/NodeRuntime.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeServices.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeSink.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeSocket.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeSocketServer.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeStdio.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeStream.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeTerminal.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/NodeWorker.ts (98%) rename {repos/effect/packages => packages}/platform/node/src/NodeWorkerRunner.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/Undici.ts (100%) rename {repos/effect/packages => packages}/platform/node/src/index.ts (100%) create mode 100644 packages/platform/node/src/internal/http.ts create mode 100644 packages/platform/node/src/internal/upgrade.ts rename {repos/effect/packages => packages}/platform/node/test/GraphQLProtocol.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/HttpApi.test.ts (81%) rename {repos/effect/packages => packages}/platform/node/test/HttpStaticServer.test.ts (94%) rename {repos/effect/packages => packages}/platform/node/test/HttpStaticServerConditional.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/KeyValueStore.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/MultipartParser.test.ts (99%) rename {repos/effect/packages => packages}/platform/node/test/NodeCrypto.test.ts (100%) create mode 100644 packages/platform/node/test/NodeHttpClient.test.ts rename {repos/effect/packages => packages}/platform/node/test/NodeHttpCompression.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/NodeHttpPlatform.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/NodeHttpServer.test.ts (61%) create mode 100644 packages/platform/node/test/NodeMultipart.test.ts rename {repos/effect/packages => packages}/platform/node/test/NodeRedis.integration.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/NodeSocket.test.ts (92%) rename {repos/effect/packages => packages}/platform/node/test/NodeWorkerRunner.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/OpenApi.test.ts (98%) rename {repos/effect/packages => packages}/platform/node/test/RpcServer.test.ts (90%) rename {repos/effect/packages => packages}/platform/node/test/__snapshots__/HttpApi.test.ts.snap (98%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/ClusterCron.test.ts (96%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/Entity.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/Locks.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/Persistence.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/README.md (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/Residency.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/Smoke.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/StreamDisconnect.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/Transport.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/Workflow.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/globalSetup.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster-integration/harness.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster/MessageStorageTest.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/cluster/SocketRunner.test.ts (95%) rename {repos/effect/packages => packages}/platform/node/test/cluster/SqlMessageStorage.integration.test.ts (88%) rename {repos/effect/packages => packages}/platform/node/test/cluster/SqlRunnerStorage.integration.test.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server-outside.txt (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server/conditional.txt (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server/custom/home.html (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server/file.binx (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server/guide/index.html (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server/hello.txt (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server/index.html (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/http-static-server/range.txt (100%) rename repos/effect/packages/sql/mssql/test/utils.ts => packages/platform/node/test/fixtures/mssql-utils.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/mysql2-utils.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/pg-utils.ts (60%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/rpc-e2e.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/rpc-schemas.ts (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/text.txt (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/tls/cert.pem (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/tls/key.pem (100%) rename {repos/effect/packages => packages}/platform/node/test/fixtures/worker-runner.ts (100%) rename {repos/effect/packages => packages}/platform/node/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/clickhouse/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/sql/clickhouse/LICENSE (100%) rename {repos/effect/packages => packages}/sql/clickhouse/README.md (100%) rename {repos/effect/packages => packages}/sql/clickhouse/package.json (98%) rename {repos/effect/packages => packages}/sql/clickhouse/src/ClickhouseClient.ts (89%) rename {repos/effect/packages => packages}/sql/clickhouse/src/ClickhouseMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/clickhouse/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/clickhouse/test/Client.test.ts (53%) rename {repos/effect/packages => packages}/sql/clickhouse/test/SqlErrorClassification.test.ts (91%) rename {repos/effect/packages => packages}/sql/clickhouse/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/d1/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/sql/d1/LICENSE (100%) rename {repos/effect/packages => packages}/sql/d1/README.md (100%) rename {repos/effect/packages => packages}/sql/d1/package.json (95%) rename {repos/effect/packages => packages}/sql/d1/src/D1Client.ts (99%) rename {repos/effect/packages => packages}/sql/d1/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/d1/test/Client.test.ts (100%) rename {repos/effect/packages => packages}/sql/d1/test/Resolver.test.ts (100%) rename {repos/effect/packages => packages}/sql/d1/test/utils.ts (100%) rename {repos/effect/packages => packages}/sql/d1/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/libsql/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/sql/libsql/LICENSE (100%) rename {repos/effect/packages => packages}/sql/libsql/README.md (100%) rename {repos/effect/packages => packages}/sql/libsql/package.json (96%) rename {repos/effect/packages => packages}/sql/libsql/src/LibsqlClient.ts (96%) rename {repos/effect/packages => packages}/sql/libsql/src/LibsqlMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/libsql/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/libsql/test/Client.integration.test.ts (76%) rename {repos/effect/packages => packages}/sql/libsql/test/Resolver.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/libsql/test/util.ts (100%) rename {repos/effect/packages => packages}/sql/libsql/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/mssql/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/sql/mssql/LICENSE (100%) rename {repos/effect/packages => packages}/sql/mssql/README.md (100%) rename {repos/effect/packages => packages}/sql/mssql/package.json (94%) rename {repos/effect/packages => packages}/sql/mssql/src/MssqlClient.ts (97%) rename {repos/effect/packages => packages}/sql/mssql/src/MssqlMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/mssql/src/Parameter.ts (100%) rename {repos/effect/packages => packages}/sql/mssql/src/Procedure.ts (100%) rename {repos/effect/packages => packages}/sql/mssql/src/index.ts (100%) create mode 100644 packages/sql/mssql/test/Binary.test.ts rename {repos/effect/packages => packages}/sql/mssql/test/Client.test.ts (92%) create mode 100644 packages/sql/mssql/test/Persistence.integration.test.ts rename {repos/effect/packages => packages}/sql/mssql/test/SqlErrorClassification.test.ts (97%) create mode 100644 packages/sql/mssql/test/SqlEventLogServerEncrypted.integration.test.ts create mode 100644 packages/sql/mssql/test/utils.ts rename {repos/effect/packages => packages}/sql/mssql/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/mysql2/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/sql/mysql2/LICENSE (100%) rename {repos/effect/packages => packages}/sql/mysql2/README.md (100%) rename {repos/effect/packages => packages}/sql/mysql2/package.json (94%) rename {repos/effect/packages => packages}/sql/mysql2/src/MysqlClient.ts (99%) rename {repos/effect/packages => packages}/sql/mysql2/src/MysqlMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/test/Client.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/test/KeyValueStore.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/test/Model.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/test/MysqlClient.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/test/Persistence.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/test/SqlErrorClassification.test.ts (96%) create mode 100644 packages/sql/mysql2/test/SqlEventLogServerEncrypted.integration.test.ts rename {repos/effect/packages => packages}/sql/mysql2/test/SqlEventLogServerUnencrypted.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/mysql2/test/utils.ts (87%) rename {repos/effect/packages => packages}/sql/mysql2/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/pg/CHANGELOG.md (92%) rename {repos/effect/packages => packages}/sql/pg/LICENSE (100%) rename {repos/effect/packages => packages}/sql/pg/README.md (100%) rename {repos/effect/packages => packages}/sql/pg/benchmark/PgClient.ts (100%) rename {repos/effect/packages => packages}/sql/pg/benchmark/PgCodec.ts (100%) rename {repos/effect/packages => packages}/sql/pg/benchmark/README.md (100%) rename {repos/effect/packages => packages}/sql/pg/package.json (92%) rename {repos/effect/packages => packages}/sql/pg/src/PgAuth.ts (100%) rename {repos/effect/packages => packages}/sql/pg/src/PgClient.ts (82%) rename {repos/effect/packages => packages}/sql/pg/src/PgConnection.ts (90%) create mode 100644 packages/sql/pg/src/PgMigrator.ts rename {repos/effect/packages => packages}/sql/pg/src/PgPool.ts (53%) rename {repos/effect/packages => packages}/sql/pg/src/PgProtocol.ts (100%) rename {repos/effect/packages => packages}/sql/pg/src/PgTypes.ts (100%) rename {repos/effect/packages => packages}/sql/pg/src/index.ts (100%) create mode 100644 packages/sql/pg/src/internal/config.ts rename {repos/effect/packages => packages}/sql/pg/src/internal/connection.ts (100%) rename {repos/effect/packages => packages}/sql/pg/src/internal/password.ts (100%) rename {repos/effect/packages => packages}/sql/pg/src/internal/sqlError.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/Client.integration.test.ts (90%) create mode 100644 packages/sql/pg/test/ContainerTeardown.test.ts rename {repos/effect/packages => packages}/sql/pg/test/KeyValueStore.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/Persistence.integration.test.ts (98%) rename {repos/effect/packages => packages}/sql/pg/test/PgAuth.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/PgConnection.in-process.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/PgConnection.integration.test.ts (97%) rename {repos/effect/packages => packages}/sql/pg/test/PgConnection.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/PgMigrator.integration.test.ts (100%) create mode 100644 packages/sql/pg/test/PgMigrator.test.ts create mode 100644 packages/sql/pg/test/PgPool.in-process.test.ts rename {repos/effect/packages => packages}/sql/pg/test/PgPool.integration.test.ts (98%) rename {repos/effect/packages => packages}/sql/pg/test/PgProtocol.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/PgTypes.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/SqlErrorClassification.test.ts (100%) create mode 100644 packages/sql/pg/test/SqlEventJournal.integration.test.ts create mode 100644 packages/sql/pg/test/SqlEventLogServerEncrypted.integration.test.ts rename {repos/effect/packages => packages}/sql/pg/test/SqlEventLogServerUnencrypted.integration.test.ts (94%) rename {repos/effect/packages => packages}/sql/pg/test/StartupParameters.integration.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/TransactionAcquire.test.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/fixtures/goldens.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/fixtures/regenerate.ts (100%) rename {repos/effect/packages => packages}/sql/pg/test/utils.ts (74%) rename {repos/effect/packages => packages}/sql/pg/tsconfig.json (100%) create mode 100644 packages/sql/pg/typetest/PgClient.tst.ts rename {repos/effect/packages => packages}/sql/pglite/CHANGELOG.md (85%) rename {repos/effect/packages => packages}/sql/pglite/LICENSE (100%) rename {repos/effect/packages => packages}/sql/pglite/README.md (100%) rename {repos/effect/packages => packages}/sql/pglite/package.json (96%) rename {repos/effect/packages => packages}/sql/pglite/src/PgliteClient.ts (99%) rename {repos/effect/packages => packages}/sql/pglite/src/PgliteMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/pglite/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/pglite/test/Client.test.ts (91%) rename {repos/effect/packages => packages}/sql/pglite/test/Json.test.ts (100%) rename {repos/effect/packages => packages}/sql/pglite/test/Migrator.test.ts (100%) rename {repos/effect/packages => packages}/sql/pglite/test/PersistedQueue.test.ts (100%) rename {repos/effect/packages => packages}/sql/pglite/test/SqlErrorClassification.test.ts (93%) create mode 100644 packages/sql/pglite/test/SqlRunnerStorage.test.ts rename {repos/effect/packages => packages}/sql/pglite/test/Transaction.test.ts (83%) rename {repos/effect/packages => packages}/sql/pglite/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/sqlite-bun/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/sql/sqlite-bun/LICENSE (100%) rename {repos/effect/packages => packages}/sql/sqlite-bun/README.md (100%) rename {repos/effect/packages => packages}/sql/sqlite-bun/package.json (96%) rename {repos/effect/packages => packages}/sql/sqlite-bun/src/SqliteClient.ts (98%) rename {repos/effect/packages => packages}/sql/sqlite-bun/src/SqliteMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-bun/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-bun/test/Client.test.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-bun/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/sqlite-do/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/sql/sqlite-do/LICENSE (100%) rename {repos/effect/packages => packages}/sql/sqlite-do/README.md (100%) rename {repos/effect/packages => packages}/sql/sqlite-do/package.json (95%) rename {repos/effect/packages => packages}/sql/sqlite-do/src/SqliteClient.ts (99%) rename {repos/effect/packages => packages}/sql/sqlite-do/src/SqliteMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-do/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-do/test/Client.test.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-do/test/Miniflare.test.ts (100%) create mode 100644 packages/sql/sqlite-do/test/SqlEventJournal.test.ts create mode 100644 packages/sql/sqlite-do/test/fixtures/eventJournal.ts rename {repos/effect/packages => packages}/sql/sqlite-do/test/fixtures/transaction.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-do/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/sqlite-node/CHANGELOG.md (92%) rename {repos/effect/packages => packages}/sql/sqlite-node/LICENSE (100%) rename {repos/effect/packages => packages}/sql/sqlite-node/README.md (100%) rename {repos/effect/packages => packages}/sql/sqlite-node/package.json (98%) rename {repos/effect/packages => packages}/sql/sqlite-node/src/SqliteClient.ts (98%) rename {repos/effect/packages => packages}/sql/sqlite-node/src/SqliteMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-node/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-node/test/Client.test.ts (84%) rename {repos/effect/packages => packages}/sql/sqlite-node/test/KeyValueStore.test.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-node/test/Persistence.test.ts (90%) rename {repos/effect/packages => packages}/sql/sqlite-node/test/Resolver.test.ts (100%) create mode 100644 packages/sql/sqlite-node/test/SqlEventJournal.test.ts rename {repos/effect/packages => packages}/sql/sqlite-node/test/SqlEventLogServerEncrypted.test.ts (83%) rename {repos/effect/packages => packages}/sql/sqlite-node/test/SqlEventLogServerUnencrypted.test.ts (79%) rename {repos/effect/packages => packages}/sql/sqlite-node/test/SqlModelServices.test.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-node/test/SqliteMigrator.test.ts (67%) rename {repos/effect/packages => packages}/sql/sqlite-node/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/LICENSE (100%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/README.md (100%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/package.json (98%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/src/SqliteClient.ts (99%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/src/SqliteMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/test/Client.test.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-react-native/tsconfig.json (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/LICENSE (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/README.md (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/package.json (98%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/src/OpfsWorker.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/src/SqliteClient.ts (99%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/src/SqliteMigrator.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/src/index.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/src/internal/opfsWorker.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/src/sqlite-wasm.d.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/test/Client.test.ts (95%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/test/Memory.test.ts (100%) rename {repos/effect/packages => packages}/sql/sqlite-wasm/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/codegen.schema.json (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/package.json (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/Config.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/Discovery.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/Generator.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/Glob.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/PostProcess.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/SpecFetcher.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/src/main.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/test/Generator.test.ts (86%) rename {repos/effect/packages => packages}/tools/ai-codegen/test/OpenAiGeneration.test.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/test/OpenRouterTranscriptions.test.ts (100%) rename {repos/effect/packages => packages}/tools/ai-codegen/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/ai-docgen/package.json (100%) rename {repos/effect/packages => packages}/tools/ai-docgen/src/Glob.ts (100%) rename {repos/effect/packages => packages}/tools/ai-docgen/src/main.ts (100%) rename {repos/effect/packages => packages}/tools/ai-docgen/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/api-diff/README.md (100%) rename {repos/effect/packages => packages}/tools/api-diff/package.json (96%) rename {repos/effect/packages => packages}/tools/api-diff/src/Annotations.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/ApiDiff.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Cli.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Diff.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Discovery.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Error.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Json.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/MigrationDoc.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Model.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Report.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Snapshot.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/Worktrees.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Annotations.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Cli.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Diff.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Discovery.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Error.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/MigrationDoc.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Report.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Snapshot.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/Worktrees.test.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/test/utils.ts (100%) rename {repos/effect/packages => packages}/tools/api-diff/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/bundle/README.md (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/arbitrary-combinators.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/basic.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/batching.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/brand.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/cache.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/config.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/differ.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/http-client.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/http-router.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/logger.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/metric.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/optic.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/pubsub.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/queue.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schedule.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-bigdecimal.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-binary.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-class.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-fromJsonSchemaDocument.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-representation-roundtrip.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-string-transformation.ts (72%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-string.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-template-literal.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-toArbitrary.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-toCodeDocument.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-toCodecJson.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-toEquivalence.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-toFormatter.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-toJsonSchemaDocument.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema-toRepresentation.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/schema.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/stm.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/fixtures/stream.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/package.json (98%) rename {repos/effect/packages => packages}/tools/bundle/src/Cli.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/src/Fixtures.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/src/Plugins.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/src/Reporter.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/src/Rollup.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/test/HttpRouterEsbuild.test.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/test/Plugins.test.ts (100%) rename {repos/effect/packages => packages}/tools/bundle/tsconfig.fixtures.json (100%) rename {repos/effect/packages => packages}/tools/bundle/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/bundle/tsconfig.src.json (100%) rename {repos/effect/packages => packages}/tools/docgen/CHANGELOG.md (82%) rename {repos/effect/packages => packages}/tools/docgen/LICENSE (100%) rename {repos/effect/packages => packages}/tools/docgen/README.md (100%) rename {repos/effect/packages => packages}/tools/docgen/package.json (97%) rename {repos/effect/packages => packages}/tools/docgen/schema.json (100%) rename {repos/effect/packages => packages}/tools/docgen/src/CLI.ts (100%) rename {repos/effect/packages => packages}/tools/docgen/src/Checker.ts (75%) rename {repos/effect/packages => packages}/tools/docgen/src/Configuration.ts (78%) rename {repos/effect/packages => packages}/tools/docgen/src/Core.ts (61%) rename {repos/effect/packages => packages}/tools/docgen/src/Domain.ts (100%) rename {repos/effect/packages => packages}/tools/docgen/src/Parser.ts (67%) rename {repos/effect/packages => packages}/tools/docgen/src/Printer.ts (81%) rename {repos/effect/packages => packages}/tools/docgen/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/docgen/src/index.ts (100%) rename {repos/effect/packages => packages}/tools/docgen/src/internal/markdown-toc.d.ts (100%) rename {repos/effect/packages => packages}/tools/docgen/test/Checker.test.ts (92%) rename {repos/effect/packages => packages}/tools/docgen/test/Configuration.test.ts (100%) rename {repos/effect/packages => packages}/tools/docgen/test/Core.test.ts (98%) rename {repos/effect/packages => packages}/tools/docgen/test/Parser.test.ts (99%) rename {repos/effect/packages => packages}/tools/docgen/test/fixtures/invalid-json.txt (100%) rename {repos/effect/packages => packages}/tools/docgen/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/doctest/CHANGELOG.md (81%) rename {repos/effect/packages => packages}/tools/doctest/README.md (100%) rename {repos/effect/packages => packages}/tools/doctest/package.json (95%) rename {repos/effect/packages => packages}/tools/doctest/src/Plugin.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/src/Protocol.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/src/Runner.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/src/Runtime.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/src/Source.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/src/Transform.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/src/index.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/Plugin.test.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/Protocol.test.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/Runtime.test.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/Source.test.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/Transform.test.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/fixtures/markdown/docs/helper.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/fixtures/markdown/docs/typed.md (100%) rename {repos/effect/packages => packages}/tools/doctest/test/fixtures/markdown/docs/typed.mdx (100%) rename {repos/effect/packages => packages}/tools/doctest/test/fixtures/markdown/src/typed.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/test/fixtures/markdown/vitest.config.ts (100%) rename {repos/effect/packages => packages}/tools/doctest/tsconfig.json (100%) create mode 100644 packages/tools/graphql-generator/CHANGELOG.md rename {repos/effect/packages => packages}/tools/graphql-generator/LICENSE (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/README.md (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/package.json (98%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/Config.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/Generator.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Ast.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Cli.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Diagnostic.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Emitter.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Generate.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Glob.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/IntrospectionReader.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Lexer.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Parser.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Printer.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/SchemaModel.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/SdlReader.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Validate.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/internal/Watch.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/src/main.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Config.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Diagnostic.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.abstract.test.ts (80%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.bindings.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.diagnostics.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.directives.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.mapping.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.naming.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.output.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.runtime.abstract.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.runtime.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.snapshots.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Generator.template.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Glob.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/GraphQLGeneratorCli.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Lexical.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Parser.executable.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Parser.typeSystem.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Printer.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/SchemaReaders.fixtures.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Validate.test.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/Watch.test.ts (96%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/README.md (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/documents/fragments.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/documents/issues.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/documents/scalars.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/documents/timeline.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/operations/issue-timeline.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/operations/search.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/github/schema.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/introspection/modern.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/introspection/modern.json (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/sdl/extensions.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/sdl/one-of.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/sdl/subscriptions.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/fixtures/subscriptions/rooms.graphql (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/generated/subscriptions/rooms.graphql.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/generated/subscriptions/shared.graphql.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/utils/ast.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/utils/generator.ts (81%) rename {repos/effect/packages => packages}/tools/graphql-generator/test/utils/model.ts (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/graphql-generator/typetest/SelectionShape.tst.ts (100%) rename {repos/effect/packages => packages}/tools/jsdocs/package.json (98%) rename {repos/effect/packages => packages}/tools/jsdocs/src/Jsdocs.ts (100%) rename {repos/effect/packages => packages}/tools/jsdocs/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/jsdocs/test/jsdocs.test.ts (100%) rename {repos/effect/packages => packages}/tools/jsdocs/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/npm-promote/README.md (100%) rename {repos/effect/packages => packages}/tools/npm-promote/package.json (91%) rename {repos/effect/packages => packages}/tools/npm-promote/src/Cli.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/GitHub.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/NpmPromote.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/NpmPromoteError.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/Process.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/Release.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/TrackingIssue.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/WebsiteDeploy.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/Workspace.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/test/NpmPromote.test.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/test/Release.test.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/test/utils.ts (100%) rename {repos/effect/packages => packages}/tools/npm-promote/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/CHANGELOG.md (87%) rename {repos/effect/packages => packages}/tools/openapi-generator/LICENSE (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/README.md (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/package.json (95%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/HttpApiTransformer.ts (98%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/JsonSchemaGenerator.ts (85%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/OpenApiGenerator.ts (80%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/OpenApiPatch.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/OpenApiTransformer.ts (91%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/ParsedOperation.ts (95%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/Utils.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/src/main.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/JsonSchemaGenerator.test.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/JsonSchemaGeneratorRepresentation.test.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/OpenApiGenerator.test.ts (81%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/OpenApiGeneratorCli.test.ts (92%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/OpenApiPatch.test.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/OpenApiTransformer.test.ts (90%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/Utils.test.ts (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/cli-basic-spec.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/cli-warning-spec.json (83%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/invalid-op.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/missing-path.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/missing-value.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/valid-add.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/valid-multiple.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/valid-patch.yaml (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/valid-remove.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/test/fixtures/patches/valid-replace.json (100%) rename {repos/effect/packages => packages}/tools/openapi-generator/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/oxc/oxlintrc.json (100%) rename {repos/effect/packages => packages}/tools/oxc/package.json (96%) rename {repos/effect/packages => packages}/tools/oxc/src/oxlint/index.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/src/oxlint/rules/no-bigint-literals.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/src/oxlint/rules/no-import-from-barrel-package.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/src/oxlint/rules/no-js-extension-imports.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/src/oxlint/rules/no-opaque-instance-fields.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/src/oxlint/rules/no-unused-internal.ts (97%) rename {repos/effect/packages => packages}/tools/oxc/test/no-import-from-barrel-package.test.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/test/no-js-extension-imports.test.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/test/no-opaque-instance-fields.test.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/test/no-unused-internal.test.ts (84%) rename {repos/effect/packages => packages}/tools/oxc/test/utils.ts (100%) rename {repos/effect/packages => packages}/tools/oxc/tsconfig.json (100%) rename {repos/effect/packages => packages}/tools/utils/package.json (100%) rename {repos/effect/packages => packages}/tools/utils/src/Codegen.ts (98%) rename {repos/effect/packages => packages}/tools/utils/src/Glob.ts (100%) rename {repos/effect/packages => packages}/tools/utils/src/bin.ts (100%) rename {repos/effect/packages => packages}/tools/utils/src/commands/codegen.ts (100%) rename {repos/effect/packages => packages}/tools/utils/test/Codegen.test.ts (76%) rename {repos/effect/packages => packages}/tools/utils/tsconfig.json (100%) rename {repos/effect/packages => packages}/vitest/CHANGELOG.md (88%) rename {repos/effect/packages => packages}/vitest/LICENSE (100%) rename {repos/effect/packages => packages}/vitest/README.md (92%) rename {repos/effect/packages => packages}/vitest/package.json (96%) rename {repos/effect/packages => packages}/vitest/src/index.ts (89%) rename {repos/effect/packages => packages}/vitest/src/internal/internal.ts (78%) rename {repos/effect/packages => packages}/vitest/src/utils.ts (100%) rename {repos/effect/packages => packages}/vitest/test/arbitrary-configuration.test.ts (100%) rename {repos/effect/packages => packages}/vitest/test/fixtures.test.ts (84%) rename {repos/effect/packages => packages}/vitest/test/index.test.ts (81%) rename {repos/effect/packages => packages}/vitest/test/isolation.test.ts (100%) rename {repos/effect/packages => packages}/vitest/test/layer-concurrency.test.ts (100%) rename {repos/effect/packages => packages}/vitest/test/nested-isolation.test.ts (100%) create mode 100644 packages/vitest/test/timeout.test.ts rename {repos/effect/packages => packages}/vitest/tsconfig.json (100%) rename {repos/effect/packages => packages}/vitest/typetest/index.tst.ts (95%) delete mode 100644 packages/vue-components/.prettierrc delete mode 100644 packages/vue-components/.storybook/main.ts delete mode 100644 packages/vue-components/.storybook/preview.ts delete mode 100644 packages/vue-components/.storybook/tsconfig.json delete mode 100644 packages/vue-components/.vscode/extensions.json delete mode 100644 packages/vue-components/CHANGELOG.md delete mode 100644 packages/vue-components/README.md delete mode 100644 packages/vue-components/__tests__/FixedNuxtErrorBoundary.test.ts delete mode 100644 packages/vue-components/__tests__/IntegerValidation.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/CustomFilterFieldSchema.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/DateValidation.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/DecimalSeparatorInput.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/DecodeTransformFallback.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/DefaultValuesPriority.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/Defaults.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/Defaults.values.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/DialogBlockingI18n.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/EmailFormatDetection.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/EmptyStringValidation.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/GeneralErrors.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/IndexedInputErrors.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/InputAttributes.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/InputReactivity.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/InputRegistry.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/IntersectionError.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/Meta.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/NullableFieldInitialization.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/NullableNestedStructValidation.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/OmegaAutoGen.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/OptionalKey.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/RedactedMeta.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/SelectMultipleLocalization.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/SubmitEffect.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/SubmitErrorClear.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/TaggedUnionBehavior.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/TaggedUnionLegacyWarning.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/TaggedUnionNested.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/TaggedUnionRequired.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/TaggedUnionRoot.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/TransformationSchema.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/TransformedStruct.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/UnregisteredFieldError.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/WithDefaultConstructorPersistency.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaForm/createUseFormWithCustomInput.test.ts delete mode 100644 packages/vue-components/__tests__/OmegaFormTest.vue delete mode 100644 packages/vue-components/__tests__/OmegaIntlProvider.vue delete mode 100644 packages/vue-components/__tests__/migration/schema-v4-migration.test.ts delete mode 100644 packages/vue-components/__tests__/setup.ts delete mode 100644 packages/vue-components/__tests__/test-array-meta.test.ts delete mode 100644 packages/vue-components/__tests__/test-nullor-duplicate.test.ts delete mode 100644 packages/vue-components/__tests__/test-union-meta.test.ts delete mode 100644 packages/vue-components/__tests__/tsconfig.json delete mode 100644 packages/vue-components/docs/superpowers/plans/2026-04-27-omegaform-refactor.md delete mode 100644 packages/vue-components/docs/superpowers/specs/2026-04-27-omegaform-refactor-design.md delete mode 100644 packages/vue-components/env.d.ts delete mode 100644 packages/vue-components/eslint.config.mjs delete mode 100644 packages/vue-components/package.json delete mode 100644 packages/vue-components/src/assets/.keep delete mode 100644 packages/vue-components/src/components/CommandButton.vue delete mode 100644 packages/vue-components/src/components/Dialog.vue delete mode 100644 packages/vue-components/src/components/FixedNuxtErrorBoundary.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/InputProps.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaArray.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaAutoGen.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaErrorsInternal.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaFormInput.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaInput.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaInputVuetify.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaInternalInput.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaTaggedUnion.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaTaggedUnionInternal.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/OmegaWrapper.vue delete mode 100644 packages/vue-components/src/components/OmegaForm/blockDialog.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/createUseFormWithCustomInput.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/decimalSeparatorInput.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/errors.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/getOmegaStore.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/hocs.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/index.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/inputs.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/meta/checks.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/meta/createMeta.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/meta/defaults.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/meta/redacted.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/meta/types.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/meta/walker.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/onMountedWithCleanup.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/persistency.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/submit.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/types.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/useOmegaForm.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/useRegisterField.ts delete mode 100644 packages/vue-components/src/components/OmegaForm/validation/localized.ts delete mode 100644 packages/vue-components/src/components/index.ts delete mode 100644 packages/vue-components/src/components/style.css delete mode 100644 packages/vue-components/src/index.ts delete mode 100644 packages/vue-components/src/reset.css delete mode 100644 packages/vue-components/src/utils/index.ts delete mode 100644 packages/vue-components/stories/Commands.stories.ts delete mode 100644 packages/vue-components/stories/Commands/One.vue delete mode 100644 packages/vue-components/stories/Commands/components.ts delete mode 100644 packages/vue-components/stories/Commands/helpers.ts delete mode 100644 packages/vue-components/stories/Commands/useEffectRouter.ts delete mode 100644 packages/vue-components/stories/Dialog.stories.ts delete mode 100644 packages/vue-components/stories/Dialog/AsyncBlocking.vue delete mode 100644 packages/vue-components/stories/Dialog/AsyncBlockingContent.vue delete mode 100644 packages/vue-components/stories/Dialog/SimpleBlocking.vue delete mode 100644 packages/vue-components/stories/Dialog/SimpleBlockingContent.vue delete mode 100644 packages/vue-components/stories/Dialog/UploadBlocking.vue delete mode 100644 packages/vue-components/stories/Dialog/UploadBlockingContent.vue delete mode 100644 packages/vue-components/stories/FixedNuxtErrorBoundary.stories.ts delete mode 100644 packages/vue-components/stories/FixedNuxtErrorBoundary/Demo.vue delete mode 100644 packages/vue-components/stories/OmegaForm.stories.ts delete mode 100644 packages/vue-components/stories/OmegaForm/Array.vue delete mode 100644 packages/vue-components/stories/OmegaForm/AutoGeneration.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Booleans.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Clearable.vue delete mode 100644 packages/vue-components/stories/OmegaForm/ComplexForm.vue delete mode 100644 packages/vue-components/stories/OmegaForm/CustomInput.vue delete mode 100644 packages/vue-components/stories/OmegaForm/CustomInputClassName.vue delete mode 100644 packages/vue-components/stories/OmegaForm/CustomLabelSlot.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Date.vue delete mode 100644 packages/vue-components/stories/OmegaForm/DecodeTransformFallback.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Defaults.vue delete mode 100644 packages/vue-components/stories/OmegaForm/DialogBlockingExamples.vue delete mode 100644 packages/vue-components/stories/OmegaForm/EmailForm.vue delete mode 100644 packages/vue-components/stories/OmegaForm/EnterSubmitRepro.vue delete mode 100644 packages/vue-components/stories/OmegaForm/FormTaggedUnion.vue delete mode 100644 packages/vue-components/stories/OmegaForm/IndexedTable.vue delete mode 100644 packages/vue-components/stories/OmegaForm/InputRegistry.vue delete mode 100644 packages/vue-components/stories/OmegaForm/IntegerValidationGerman.vue delete mode 100644 packages/vue-components/stories/OmegaForm/IntersectionExample.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Meta.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Null.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Nullable.vue delete mode 100644 packages/vue-components/stories/OmegaForm/NullableNestedStruct.vue delete mode 100644 packages/vue-components/stories/OmegaForm/NumberInput.vue delete mode 100644 packages/vue-components/stories/OmegaForm/OmitConstructorDefaults.vue delete mode 100644 packages/vue-components/stories/OmegaForm/OptionalKey.vue delete mode 100644 packages/vue-components/stories/OmegaForm/PersistencyForm.vue delete mode 100644 packages/vue-components/stories/OmegaForm/ProgrammaticallyHandleSubmitCheckErrors.vue delete mode 100644 packages/vue-components/stories/OmegaForm/RatingInput.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Redacted.vue delete mode 100644 packages/vue-components/stories/OmegaForm/RootLevelTaggedUnion.vue delete mode 100644 packages/vue-components/stories/OmegaForm/RootLevelTaggedUnionLegacy.vue delete mode 100644 packages/vue-components/stories/OmegaForm/SetErrorOnSubmit.vue delete mode 100644 packages/vue-components/stories/OmegaForm/SimpleForm.vue delete mode 100644 packages/vue-components/stories/OmegaForm/SimpleFormVuetifyDefault.vue delete mode 100644 packages/vue-components/stories/OmegaForm/SumExample.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Tanstack.vue delete mode 100644 packages/vue-components/stories/OmegaForm/Union.vue delete mode 100644 packages/vue-components/stories/OmegaForm/UsingOmegaForm.vue delete mode 100644 packages/vue-components/stories/OmegaForm/WindowExitPrevention.vue delete mode 100644 packages/vue-components/stories/OmegaForm/WithDefaultConstructor.vue delete mode 100644 packages/vue-components/stories/OmegaForm/createUseFormWIthCustomInput.vue delete mode 100644 packages/vue-components/stories/OmegaForm/form.Input.vue delete mode 100644 packages/vue-components/stories/README.md delete mode 100644 packages/vue-components/stories/tsconfig.json delete mode 100644 packages/vue-components/tsconfig.build.json delete mode 100644 packages/vue-components/tsconfig.json delete mode 100644 packages/vue-components/types/vuetify-shims.d.ts delete mode 100644 packages/vue-components/vite.config.mts delete mode 100644 packages/vue-components/vitest.config.mts delete mode 100644 packages/vue/.madgerc delete mode 100644 packages/vue/.npmignore delete mode 100644 packages/vue/.prettierignore delete mode 100644 packages/vue/CHANGELOG.md delete mode 100644 packages/vue/docs/atom-query-api-redesign.md delete mode 100644 packages/vue/docs/mutation-command-atoms.md delete mode 100644 packages/vue/docs/query-key-invalidation.md delete mode 100644 packages/vue/examples/streamMutation.ts delete mode 100644 packages/vue/package.json delete mode 100644 packages/vue/src/atomQuery.ts delete mode 100644 packages/vue/src/commander.ts delete mode 100644 packages/vue/src/confirm.ts delete mode 100644 packages/vue/src/dependencyMetadata.ts delete mode 100644 packages/vue/src/errorReporter.ts delete mode 100644 packages/vue/src/form.ts delete mode 100644 packages/vue/src/index.ts delete mode 100644 packages/vue/src/internal/tanstackQuery.ts delete mode 100644 packages/vue/src/intl.ts delete mode 100644 packages/vue/src/lib.ts delete mode 100644 packages/vue/src/liveQueryInvalidation.ts delete mode 100644 packages/vue/src/makeClient.ts delete mode 100644 packages/vue/src/makeContext.ts delete mode 100644 packages/vue/src/makeIntl.ts delete mode 100644 packages/vue/src/makeUseCommand.ts delete mode 100644 packages/vue/src/mutate.ts delete mode 100644 packages/vue/src/query.ts delete mode 100644 packages/vue/src/queryLifetime.ts delete mode 100644 packages/vue/src/routeParams.ts delete mode 100644 packages/vue/src/runtime.ts delete mode 100644 packages/vue/src/suspense.ts delete mode 100644 packages/vue/src/toast.ts delete mode 100644 packages/vue/src/withToast.ts delete mode 100644 packages/vue/test/Mutation.test.ts delete mode 100644 packages/vue/test/dependencyInvalidation.test.ts delete mode 100644 packages/vue/test/form-validation-errors.test.ts delete mode 100644 packages/vue/test/form.test.ts delete mode 100644 packages/vue/test/inactiveInvalidation.test.ts delete mode 100644 packages/vue/test/interrupt-refetch-repro.test.ts delete mode 100644 packages/vue/test/lib.test.ts delete mode 100644 packages/vue/test/liveQueryInvalidation.test.ts delete mode 100644 packages/vue/test/makeClient.test.ts delete mode 100644 packages/vue/test/query-span.test.ts delete mode 100644 packages/vue/test/queryOptions.test.ts delete mode 100644 packages/vue/test/streamFinal.test.ts delete mode 100644 packages/vue/test/streamFn.test.ts delete mode 100644 packages/vue/test/stubs.ts delete mode 100644 packages/vue/test/suspendWaiting.test.ts delete mode 100644 packages/vue/test/suspense-regression.test.ts delete mode 100644 packages/vue/test/suspense.test.ts delete mode 100644 packages/vue/tsconfig.examples.json delete mode 100644 packages/vue/tsconfig.json delete mode 100644 packages/vue/tsconfig.src.json delete mode 100644 packages/vue/tsconfig.test.json delete mode 100644 packages/vue/vitest.config.ts delete mode 100644 patches/.gitkeep delete mode 100644 patches/@ben_12__eslint-plugin-dprint.patch delete mode 100644 patches/@changesets__cli@2.31.0.patch rename {repos/effect/patches => patches}/@changesets__get-github-info@1.0.1.patch (100%) delete mode 100644 patches/@sendgrid__mail.patch delete mode 100644 patches/@tanstack__query-core.patch delete mode 100644 patches/ts-plugin-sort-import-suggestions.patch delete mode 100644 patches/typescript.patch delete mode 100644 repos/effect/.agents/skills/changesets/authoring.md delete mode 100644 repos/effect/.changeset/config.json delete mode 100644 repos/effect/.envrc delete mode 100644 repos/effect/.gitignore delete mode 100644 repos/effect/.oxlintrc.json delete mode 100644 repos/effect/.vscode/extensions.json delete mode 100644 repos/effect/.vscode/settings.json delete mode 100644 repos/effect/README.md delete mode 100644 repos/effect/flake.lock delete mode 100644 repos/effect/flake.nix delete mode 100644 repos/effect/migration/annotations/effect__platform__Path.yaml delete mode 100644 repos/effect/migration/annotations/effect__sql__SqlClient.yaml delete mode 100644 repos/effect/migration/annotations/effect__sql__SqlEventLogServer.yaml delete mode 100644 repos/effect/migration/error-handling.md delete mode 100644 repos/effect/package.json delete mode 100644 repos/effect/packages/ai/cloudflare/CHANGELOG.md delete mode 100644 repos/effect/packages/effect/src/ai/DecisionModel.ts delete mode 100644 repos/effect/packages/effect/src/cluster/ClusterCron.ts delete mode 100644 repos/effect/packages/effect/src/cluster/ClusterMetrics.ts delete mode 100644 repos/effect/packages/effect/src/eventlog/EventJournal.ts delete mode 100644 repos/effect/packages/effect/src/eventlog/EventLogEncryption.ts delete mode 100644 repos/effect/packages/effect/src/eventlog/SqlEventJournal.ts delete mode 100644 repos/effect/packages/effect/src/eventlog/SqlEventLogServerEncrypted.ts delete mode 100644 repos/effect/packages/effect/src/http-api/internal/path.ts delete mode 100644 repos/effect/packages/effect/src/http/MultipartParser/internal/contentType.ts delete mode 100644 repos/effect/packages/effect/src/http/internal/preResponseHandler.ts delete mode 100644 repos/effect/packages/effect/src/internal/version.ts delete mode 100644 repos/effect/packages/effect/test/Exit.test.ts delete mode 100644 repos/effect/packages/effect/test/RequestResolver.test.ts delete mode 100644 repos/effect/packages/effect/test/devtools/DevToolsClient.test.ts delete mode 100644 repos/effect/packages/effect/test/eventlog/EventJournal.test.ts delete mode 100644 repos/effect/packages/effect/test/eventlog/EventLog.test.ts delete mode 100644 repos/effect/packages/effect/test/eventlog/EventLogMessage.test.ts delete mode 100644 repos/effect/packages/effect/test/eventlog/EventLogRemote.test.ts delete mode 100644 repos/effect/packages/effect/test/http/FetchHttpClient.test.ts delete mode 100644 repos/effect/packages/effect/test/http/Headers.test.ts delete mode 100644 repos/effect/packages/effect/test/observability/OtlpSerialization.test.ts delete mode 100644 repos/effect/packages/effect/test/persistence/PersistedQueueSqlTest.ts delete mode 100644 repos/effect/packages/effect/test/sql/SqlClient.test.ts delete mode 100644 repos/effect/packages/platform/bun/src/BunHttpClient.ts delete mode 100644 repos/effect/packages/platform/deno/src/DenoHttpClient.ts delete mode 100644 repos/effect/packages/platform/node-shared/src/NodeSink.ts delete mode 100644 repos/effect/packages/platform/node-shared/test/NodeSink.test.ts delete mode 100644 repos/effect/packages/platform/node/test/NodeHttpClient.test.ts delete mode 100644 repos/effect/packages/sql/mssql/test/Binary.test.ts delete mode 100644 repos/effect/packages/sql/mssql/test/Persistence.integration.test.ts delete mode 100644 repos/effect/packages/sql/pg/src/PgMigrator.ts delete mode 100644 repos/effect/packages/sql/pg/test/PgMigrator.test.ts delete mode 100644 repos/effect/packages/sql/pg/test/PgPool.in-process.test.ts delete mode 100644 repos/effect/packages/sql/pg/test/SqlEventJournal.integration.test.ts delete mode 100644 repos/effect/packages/sql/sqlite-node/test/SqlEventJournal.test.ts delete mode 100644 repos/effect/packages/tools/graphql-generator/CHANGELOG.md delete mode 100644 repos/effect/packages/vitest/test/timeout.test.ts delete mode 100644 repos/effect/pnpm-lock.yaml delete mode 100644 repos/effect/pnpm-workspace.yaml delete mode 100644 repos/effect/scripts/codemods/jsdoc.ts delete mode 100644 repos/effect/tsconfig.base.json delete mode 100644 repos/effect/vitest.config.ts delete mode 100644 repos/effect/vitest.setup.ts rename {repos/effect/scratchpad => scratchpad}/index.ts (100%) rename {repos/effect/scratchpad => scratchpad}/package.json (100%) rename {repos/effect/scratchpad => scratchpad}/tsconfig.json (74%) delete mode 100644 scripts/agent-gh.mjs delete mode 100644 scripts/agent-pr-ready.mjs delete mode 100644 scripts/agent-pre-push.mjs rename {repos/effect/scripts => scripts}/bundle-analyze.sh (100%) rename {repos/effect/scripts => scripts}/bundle-compare-selected.sh (100%) rename {repos/effect/scripts => scripts}/bundle-compare.sh (100%) rename {repos/effect/scripts => scripts}/check-dist-types.mjs (60%) rename {repos/effect/scripts => scripts}/circular.mjs (100%) delete mode 100644 scripts/clean-dist.sh rename {repos/effect/scripts => scripts}/clean.mjs (100%) rename {repos/effect/scripts => scripts}/codemod.mjs (100%) create mode 100644 scripts/codemods/jsdoc.test.ts create mode 100644 scripts/codemods/jsdoc.ts delete mode 100644 scripts/convert_patch.js rename {repos/effect/scripts => scripts}/copy-ai-docs.mjs (100%) rename {repos/effect/scripts => scripts}/effect-bot.mjs (100%) delete mode 100644 scripts/extract.sh delete mode 100755 scripts/hooks/post-checkout.test.sh delete mode 100644 scripts/install-git-hooks.mjs delete mode 100644 scripts/lib/agent-env.mjs delete mode 100644 scripts/lib/agent-gh-policy.mjs delete mode 100644 scripts/lib/agent-pr-state.mjs delete mode 100644 scripts/lib/agent-ship-gate-cache.mjs delete mode 100644 scripts/mergeTsConfig.mjs rename {repos/effect/scripts => scripts}/package-scalar.mjs (100%) rename {repos/effect/scripts => scripts}/package-swagger.mjs (100%) rename {repos/effect/scripts => scripts}/package.json (100%) delete mode 100644 scripts/rewrite-dist-declaration-imports.mjs rename {repos/effect/scripts => scripts}/set-strip-internal.mjs (100%) rename {repos/effect/scripts => scripts}/setup-agents.mjs (100%) delete mode 100644 scripts/sort.js delete mode 100644 scripts/stagePublishPackage.mjs delete mode 100644 scripts/sync-effect-subtree.js rename {repos/effect/scripts => scripts}/tsconfig.json (72%) rename {repos/effect/scripts => scripts}/version.mjs (100%) rename {repos/effect/scripts => scripts}/worktree-setup.sh (100%) delete mode 100644 t3.json delete mode 100644 tsconfig.all.json rename repos/effect/tsconfig.json => tsconfig.json (100%) delete mode 100644 tsconfig.lib.json rename repos/effect/tsconfig.packages.json => tsconfig.packages.json (100%) delete mode 100644 tsconfig.plugins.json rename repos/effect/tsconfig.tests.json => tsconfig.tests.json (93%) rename repos/effect/tstyche.json => tstyche.json (100%) delete mode 100644 vite.config.base.ts rename repos/effect/vitest.docs.ts => vitest.docs.ts (100%) delete mode 100644 wiki/Home.md delete mode 100644 wiki/ResourceClients.md delete mode 100644 wiki/bag-of-tricks.md delete mode 100644 "wiki/effect\342\200\220app\342\200\220cli.md" delete mode 100644 wiki/error-reporter-migration.md delete mode 100644 wiki/repository-derived-query-invalidation.md diff --git a/repos/effect/.agents/AGENTS.md b/.agents/AGENTS.md similarity index 100% rename from repos/effect/.agents/AGENTS.md rename to .agents/AGENTS.md diff --git a/repos/effect/.agents/skills/ai-docs/SKILL.md b/.agents/skills/ai-docs/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/ai-docs/SKILL.md rename to .agents/skills/ai-docs/SKILL.md diff --git a/repos/effect/.agents/skills/bundle-analysis/SKILL.md b/.agents/skills/bundle-analysis/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/bundle-analysis/SKILL.md rename to .agents/skills/bundle-analysis/SKILL.md diff --git a/repos/effect/.agents/skills/changesets/SKILL.md b/.agents/skills/changesets/SKILL.md similarity index 91% rename from repos/effect/.agents/skills/changesets/SKILL.md rename to .agents/skills/changesets/SKILL.md index 5b6b4d5f2..36c078d79 100644 --- a/repos/effect/.agents/skills/changesets/SKILL.md +++ b/.agents/skills/changesets/SKILL.md @@ -18,8 +18,8 @@ focused validation are complete. 4. Record a reason no changeset is required, or update the PR's existing changeset. Create one only when the PR does not have one. When required, read [authoring.md](authoring.md). -5. Validate package names, frontmatter, bump policy, and a concise - consumer-facing description. +5. Validate package names, frontmatter, bump policy, and a one-sentence + consumer-facing description, longer only for breaks or required migration. Do not include unrelated worktree changes. @@ -71,5 +71,6 @@ output is mechanical evidence, not a semantic-version decision. The task is complete when every affected contract and published package is accounted for and either the no-changeset decision is explicit or the PR has one -consolidated, valid changeset by default. Its release note describes the consumer -impact and migration for every break without repeating the PR description. +consolidated, valid changeset by default. Its release note is one sentence of +consumer impact, plus migration steps for every break, with no implementation +detail. diff --git a/.agents/skills/changesets/authoring.md b/.agents/skills/changesets/authoring.md new file mode 100644 index 000000000..a1158ddf3 --- /dev/null +++ b/.agents/skills/changesets/authoring.md @@ -0,0 +1,56 @@ +# Authoring changesets + +Update the changeset already introduced by the PR. If there is none, create one +`.changeset/.md` for the complete PR: + +```md +--- +"effect": patch +"@effect/affected-package": patch +--- + +Fix `Effect.cachedInvalidateWithTTL` caching a stale result when invalidated while the effect is running. +``` + +Write one short sentence, usually under 25 words, that names the changed API +and states what consumers will notice. That sentence is the whole entry unless +the change is breaking or consumers must migrate. + +Leave out how the change works: internal modules, algorithms, data structures, +refactors, motivation, benchmarks, test coverage, and development history. They +belong in the PR description. Cut any sentence a consumer would not act on. + +- Bad: "Rework the `MemoMap` lookup so `Layer.provide` checks identity before + building, adding a cache keyed by layer reference and tests for shared + layers." +- Good: "Fix composed layers being rebuilt for every parent that references + them." + +Add detail only for a break or required migration: what existing code must +change and how. Use compact bullets, one break per bullet, when several breaks +need migration steps. Several non-breaking fixes or additions still share one +sentence. + +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: + +- Use `patch` for compatible fixes, `minor` for compatible additions, and + `major` for breaks. +- A break confined to APIs tagged `@stability unstable` is `minor`. A break + confined to APIs tagged `@stability experimental` is `patch`. APIs tagged + `@stability stable` follow strict semver. +- Use `major` only when a maintainer has approved a major release. +- The release queue retargets PRs with `minor` changesets to `v4/next-minor` + and PRs with `major` changesets to `v4/next-major`. Choose the level the + change requires, not the branch you want it to land on. +- Ask when release intent is ambiguous. + +Use a `### Breaking changes` section when several breaks need separate +scanning. Include before/after examples only when they materially clarify +migration. + +Validate frontmatter against published package names and inspect nearby current +changesets for release convention. Do not copy the length of longer entries. +Never run `changeset-version` or `changeset-publish` as contributor validation. diff --git a/repos/effect/.agents/skills/ci-maintenance/SKILL.md b/.agents/skills/ci-maintenance/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/ci-maintenance/SKILL.md rename to .agents/skills/ci-maintenance/SKILL.md diff --git a/repos/effect/.agents/skills/ci-maintenance/privileged-workflows.md b/.agents/skills/ci-maintenance/privileged-workflows.md similarity index 100% rename from repos/effect/.agents/skills/ci-maintenance/privileged-workflows.md rename to .agents/skills/ci-maintenance/privileged-workflows.md diff --git a/repos/effect/.agents/skills/dependency-maintenance/SKILL.md b/.agents/skills/dependency-maintenance/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/dependency-maintenance/SKILL.md rename to .agents/skills/dependency-maintenance/SKILL.md diff --git a/repos/effect/.agents/skills/dependency-maintenance/coordinated-upgrades.md b/.agents/skills/dependency-maintenance/coordinated-upgrades.md similarity index 100% rename from repos/effect/.agents/skills/dependency-maintenance/coordinated-upgrades.md rename to .agents/skills/dependency-maintenance/coordinated-upgrades.md diff --git a/repos/effect/.agents/skills/dependency-maintenance/manifest-roles.md b/.agents/skills/dependency-maintenance/manifest-roles.md similarity index 100% rename from repos/effect/.agents/skills/dependency-maintenance/manifest-roles.md rename to .agents/skills/dependency-maintenance/manifest-roles.md diff --git a/repos/effect/.agents/skills/effect-development/SKILL.md b/.agents/skills/effect-development/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/effect-development/SKILL.md rename to .agents/skills/effect-development/SKILL.md diff --git a/repos/effect/.agents/skills/jsdocs/SKILL.md b/.agents/skills/jsdocs/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/jsdocs/SKILL.md rename to .agents/skills/jsdocs/SKILL.md diff --git a/repos/effect/.agents/skills/jsdocs/categories.md b/.agents/skills/jsdocs/categories.md similarity index 100% rename from repos/effect/.agents/skills/jsdocs/categories.md rename to .agents/skills/jsdocs/categories.md diff --git a/repos/effect/.agents/skills/jsdocs/declarations.md b/.agents/skills/jsdocs/declarations.md similarity index 100% rename from repos/effect/.agents/skills/jsdocs/declarations.md rename to .agents/skills/jsdocs/declarations.md diff --git a/repos/effect/.agents/skills/jsdocs/examples.md b/.agents/skills/jsdocs/examples.md similarity index 100% rename from repos/effect/.agents/skills/jsdocs/examples.md rename to .agents/skills/jsdocs/examples.md diff --git a/repos/effect/.agents/skills/migration-guidance/SKILL.md b/.agents/skills/migration-guidance/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/migration-guidance/SKILL.md rename to .agents/skills/migration-guidance/SKILL.md diff --git a/repos/effect/.agents/skills/migration-guidance/annotations.md b/.agents/skills/migration-guidance/annotations.md similarity index 100% rename from repos/effect/.agents/skills/migration-guidance/annotations.md rename to .agents/skills/migration-guidance/annotations.md diff --git a/repos/effect/.agents/skills/migration-guidance/generation.md b/.agents/skills/migration-guidance/generation.md similarity index 100% rename from repos/effect/.agents/skills/migration-guidance/generation.md rename to .agents/skills/migration-guidance/generation.md diff --git a/repos/effect/.agents/skills/package-development/SKILL.md b/.agents/skills/package-development/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/package-development/SKILL.md rename to .agents/skills/package-development/SKILL.md diff --git a/repos/effect/.agents/skills/package-development/dependencies.md b/.agents/skills/package-development/dependencies.md similarity index 100% rename from repos/effect/.agents/skills/package-development/dependencies.md rename to .agents/skills/package-development/dependencies.md diff --git a/repos/effect/.agents/skills/package-development/publishing.md b/.agents/skills/package-development/publishing.md similarity index 100% rename from repos/effect/.agents/skills/package-development/publishing.md rename to .agents/skills/package-development/publishing.md diff --git a/repos/effect/.agents/skills/package-development/registration.md b/.agents/skills/package-development/registration.md similarity index 100% rename from repos/effect/.agents/skills/package-development/registration.md rename to .agents/skills/package-development/registration.md diff --git a/repos/effect/.agents/skills/performance-analysis/SKILL.md b/.agents/skills/performance-analysis/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/performance-analysis/SKILL.md rename to .agents/skills/performance-analysis/SKILL.md diff --git a/repos/effect/.agents/skills/performance-analysis/runtime.md b/.agents/skills/performance-analysis/runtime.md similarity index 100% rename from repos/effect/.agents/skills/performance-analysis/runtime.md rename to .agents/skills/performance-analysis/runtime.md diff --git a/repos/effect/.agents/skills/performance-analysis/types.md b/.agents/skills/performance-analysis/types.md similarity index 100% rename from repos/effect/.agents/skills/performance-analysis/types.md rename to .agents/skills/performance-analysis/types.md diff --git a/repos/effect/.agents/skills/pull-requests/SKILL.md b/.agents/skills/pull-requests/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/pull-requests/SKILL.md rename to .agents/skills/pull-requests/SKILL.md diff --git a/repos/effect/.agents/skills/test-development/SKILL.md b/.agents/skills/test-development/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/test-development/SKILL.md rename to .agents/skills/test-development/SKILL.md diff --git a/repos/effect/.agents/skills/test-development/displayed-types.md b/.agents/skills/test-development/displayed-types.md similarity index 100% rename from repos/effect/.agents/skills/test-development/displayed-types.md rename to .agents/skills/test-development/displayed-types.md diff --git a/repos/effect/.agents/skills/test-development/runtime.md b/.agents/skills/test-development/runtime.md similarity index 100% rename from repos/effect/.agents/skills/test-development/runtime.md rename to .agents/skills/test-development/runtime.md diff --git a/repos/effect/.agents/skills/test-development/types.md b/.agents/skills/test-development/types.md similarity index 100% rename from repos/effect/.agents/skills/test-development/types.md rename to .agents/skills/test-development/types.md diff --git a/repos/effect/.agents/skills/vendored-assets/SKILL.md b/.agents/skills/vendored-assets/SKILL.md similarity index 100% rename from repos/effect/.agents/skills/vendored-assets/SKILL.md rename to .agents/skills/vendored-assets/SKILL.md diff --git a/repos/effect/.agents/skills/vendored-assets/browser-assets.md b/.agents/skills/vendored-assets/browser-assets.md similarity index 100% rename from repos/effect/.agents/skills/vendored-assets/browser-assets.md rename to .agents/skills/vendored-assets/browser-assets.md diff --git a/.changeset/README.md b/.changeset/README.md deleted file mode 100644 index e5b6d8d6a..000000000 --- a/.changeset/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Changesets - -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) - -We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json index f246cc8e0..b776b5a75 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,17 +1,56 @@ { - "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", - "changelog": "@changesets/cli/changelog", + "$schema": "https://unpkg.com/@changesets/config@4.0.0-next.8/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "Effect-TS/effect" }], "commit": false, + "format": false, + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [ + "scratchpad", + "scripts" + ], + "privatePackages": false, "fixed": [ [ - "effect-app", - "@effect-app/infra", - "@effect-app/vue", - "@effect-app/vue-components" + "effect", + "@effect/ai-anthropic", + "@effect/ai-cloudflare", + "@effect/ai-openai", + "@effect/ai-typesafe", + "@effect/ai-openai-compat", + "@effect/ai-openrouter", + "@effect/atom-react", + "@effect/atom-solid", + "@effect/atom-vue", + "@effect/docgen", + "@effect/doctest", + "@effect/graphql-generator", + "@effect/openapi-generator", + "@effect/opentelemetry", + "@effect/platform-browser", + "@effect/platform-bun", + "@effect/platform-deno", + "@effect/platform-node", + "@effect/platform-node-shared", + "@effect/sql-clickhouse", + "@effect/sql-d1", + "@effect/sql-libsql", + "@effect/sql-mssql", + "@effect/sql-mysql2", + "@effect/sql-pg", + "@effect/sql-pglite", + "@effect/sql-sqlite-bun", + "@effect/sql-sqlite-do", + "@effect/sql-sqlite-node", + "@effect/sql-sqlite-react-native", + "@effect/sql-sqlite-wasm", + "@effect/vitest" ] ], - "linked": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch" + "snapshot": { + "useCalculatedVersion": false, + "prereleaseTemplate": "{tag}-{commit}" + } } diff --git a/.changeset/node-24-22-1.md b/.changeset/node-24-22-1.md deleted file mode 100644 index 59d651ccf..000000000 --- a/.changeset/node-24-22-1.md +++ /dev/null @@ -1,5 +0,0 @@ ---- - ---- - -Pin the development toolchain to Node.js 24.22.1, the latest 24.x release. diff --git a/.changeset/update-effect-4.0.5.md b/.changeset/update-effect-4.0.5.md deleted file mode 100644 index 768ce9713..000000000 --- a/.changeset/update-effect-4.0.5.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"effect-app": patch -"@effect-app/cli": patch ---- - -Upgrade Effect packages to npm `4.0.5`. `TaggedUnion` and `ExtendTaggedUnion` reject duplicate `_tag` values at compile time, matching `Schema.toTaggedUnion`. JSON Schema for `PositiveInt` and `NonNegativeInt` now includes `Schema.Int`'s safe-integer maximum. diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 824fd22d2..000000000 --- a/.claude/settings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git commit -m ':*)", - "Bash(pnpm -r check)", - "Bash(pnpm lint-fix:*)", - "Bash(git commit -m ' *)", - "Read(//home/patroza/pj/effect-app/**)", - "Bash(git checkout *)", - "Bash(pnpm --filter @effect-app/vue check)", - "Bash(git -C /home/patroza/pj/effect-app/libs status)", - "Bash(git -C /home/patroza/pj/effect-app/libs diff --stat)", - "Bash(git -C /home/patroza/pj/effect-app/libs log -5 --oneline)", - "Bash(git -C /home/patroza/pj/effect-app/libs add packages/infra/src/Model/Repository/ext.ts packages/infra/src/Model/Repository/internal/internal.ts packages/infra/src/Model/Repository/service.ts .changeset/repo-mutations-accept-empty-arrays.md)", - "Bash(git -C /home/patroza/pj/effect-app/libs commit -m ' *)" - ] - } -} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 4796bc0c5..000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# Specifies which operating system image to use. -FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu - -WORKDIR /home/vscode - -# curl and ca-certificates are needed for volta installation -RUN apt-get update \ - && apt-get install -y \ - curl \ - ca-certificates \ - --no-install-recommends - -# Changes user to vscode and the SHELL to bash -USER vscode -SHELL ["/bin/bash", "-c"] - -# since we're starting non-interactive shell, we wil need to tell bash to load .bashrc manually -ENV BASH_ENV ~/.bashrc -# needed by volta() function -ENV VOLTA_HOME /home/vscode/.volta -# make sure packages managed by volta will be in PATH -ENV PATH $VOLTA_HOME/bin:$PATH - -# download volta -RUN curl https://get.volta.sh | bash -# change the working directory to the one where the project lives -WORKDIR /workspaces/macs-configurator - -# and install node and pnpm -RUN volta install node@24.22.1 -RUN npm i -g pnpm - -RUN echo - -ENV SHELL=/bin/bash - -RUN echo - -# prevent `npm or yarn` usage in the shell now that pnpm is installed -WORKDIR /home/vscode -COPY usepnpm.sh /tmp/ -RUN cat /tmp/usepnpm.sh >> ~/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 6a267802a..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Effect App Libs Codespace", - "build": { - "dockerfile": "Dockerfile" - }, - // see https://containers.dev/implementors/json_reference/#remoteUser - // you can change the user name to whatever you want, this is used to avoid installing stuff in the image as root - "remoteUser": "vscode", - "postCreateCommand": "pnpm i && pnpm build", - "hostRequirements": { - "cpus": 4, - "memory": "8gb" - } -} diff --git a/.devcontainer/humanlog.sh b/.devcontainer/humanlog.sh deleted file mode 100644 index d792a7551..000000000 --- a/.devcontainer/humanlog.sh +++ /dev/null @@ -1,2 +0,0 @@ - export HUMANLOG_INSTALL="/home/vscode/.humanlog" - export PATH="$HUMANLOG_INSTALL/bin:$PATH" \ No newline at end of file diff --git a/.devcontainer/usepnpm.sh b/.devcontainer/usepnpm.sh deleted file mode 100644 index 2c34d4098..000000000 --- a/.devcontainer/usepnpm.sh +++ /dev/null @@ -1,26 +0,0 @@ -NPM_PATH=$(which npm) -npm () { - if [ -e pnpm-lock.yaml ] - then - echo "Please use PNPM with this project" - elif [ -e yarn.lock ] - then - echo "Please use Yarn with this project" - else - $NPM_PATH "$@" - fi -} - - -YARN_PATH=$(which yarn) -yarn () { - if [ -e pnpm-lock.yaml ] - then - echo "Please use PNPM with this project" - elif [ -e package-lock.json ] - then - echo "Please use NPM with this project" - else - $YARN_PATH "$@" - fi -} \ No newline at end of file diff --git a/.envrc b/.envrc index 03113d3db..3550a30f2 100644 --- a/.envrc +++ b/.envrc @@ -1,5 +1 @@ use flake - -# Repo `gh` policy shim: publishing runs the ship gate first (see AGENTS.md). -# Installed by scripts/install-git-hooks.mjs; run `direnv allow` once after cloning. -PATH_add .tools/bin diff --git a/.githooks/README.md b/.githooks/README.md deleted file mode 100644 index 147308b75..000000000 --- a/.githooks/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Native Git hooks - -`pnpm install` configures `core.hooksPath=.githooks` via -`scripts/install-git-hooks.mjs`. Tracked hooks exist in every linked worktree. - -## `post-checkout` - -### 1. Clear TypeScript project emit (always on branch / worktree checkout) - -`pnpm check` is `tsgo --build` with composite + incremental. Package outputs -(`.tsbuildinfo` + `.d.ts` / `.js`) live under `packages/*/dist` and are -gitignored. - -After a branch switch, **sources** follow HEAD but **dist** can still belong to -another tree. tsgo may treat projects as up to date and typecheck new sources -against old declarations → phantom type errors that disappear on -`pnpm rbuild` (`clean` + check). Same class of failure as -[scanner#1495](https://github.com/macs-holding/scanner/pull/1495) / -[scanner#2166](https://github.com/macs-holding/scanner/pull/2166). - -On every **branch** checkout (`post-checkout` flag `1`), this hook deletes: - -- `packages/*/dist`, `packages/*/build`, root `dist` if present -- any remaining `*.tsbuildinfo` outside `node_modules` / `repos` / `.git` - -File-only checkouts (flag `0`) are left alone. - -**This is a workaround.** Root cause belongs in tsgo incremental invalidation -(e.g. [typescript-go#2666](https://github.com/microsoft/typescript-go/issues/2666), -[#4664](https://github.com/microsoft/typescript-go/issues/4664), -[#4262](https://github.com/microsoft/typescript-go/issues/4262)). Remove when -upstream is reliable. Do not re-enable cross-tree dist reuse without an exact -source-identity key. - -### 2. Reconcile pnpm (when package/lock changed) - -Records package/lockfile state under `node_modules` so T3 handoff does not -repeat an install Git already completed. - -When T3 launches the configured worktree setup script, it sets -`T3CODE_DEFER_DEPENDENCY_INSTALL=1`. Cleanup still runs synchronously, while the -setup terminal installs dependencies asynchronously with hardlink imports from -the shared pnpm store. Raw Git worktrees still install during checkout. Their -install failure is best-effort; T3 can request the real exit status with -`T3CODE_WORKTREE_PREPARATION_STRICT=1` without removing the created worktree. - -## `pre-commit` / `pre-push` - -lint-staged on commit; agent ship gate on push (see repo AGENTS.md). diff --git a/.githooks/post-checkout b/.githooks/post-checkout deleted file mode 100755 index e938e7375..000000000 --- a/.githooks/post-checkout +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/sh -# 1) After a branch/worktree checkout: drop TypeScript project-reference emit so -# tsgo --build never typechecks against foreign-tree .tsbuildinfo / .d.ts. -# 2) Reconcile pnpm when package.json / lockfile changed. - -PATH="$HOME/.local/bin:$HOME/.local/share/pnpm:$HOME/.local/share/mise/shims:/run/current-system/sw/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:$PATH" -export PATH - -# post-checkout also runs after file-only checkouts; those are not a tree switch. -[ "${3:-0}" = "1" ] || exit 0 - -wt_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 -[ -f "$wt_root/package.json" ] || exit 0 - -# --- TS build-cache wipe (every branch / worktree checkout) ----------------- -# -# Why: `pnpm check` is `tsgo --build` with composite + incremental. Package -# outputs live in packages/*/dist (gitignored). After checkout/rebase, sources -# belong to HEAD but dist may still be from another branch. tsgo can treat -# projects as up-to-date and typecheck new sources against old .d.ts → phantom -# errors that vanish on `pnpm rbuild` (clean+check). -# -# Same class as macs-holding/scanner#1495 / #2166 and upstream typescript-go -# incremental gaps (#2666, #4664, #4262, #3787). -# -# Workaround only — remove when upstream invalidation is reliable. Do not -# re-introduce foreign dist reuse without an exact source-identity key. -# -clean_ts_project_emit() { - root=$1 - removed=0 - - for d in \ - "$root"/packages/*/dist \ - "$root"/packages/*/build \ - "$root"/dist - do - [ -e "$d" ] || continue - rm -rf "$d" - removed=1 - done - - if [ -d "$root" ]; then - tsbuild_files=$( - find "$root" \ - \( -path "$root/node_modules" -o -path "$root/repos" -o -path "$root/.git" -o -path "$root/.run" -o -path "$root/build" \) -prune -o \ - -type f -name '*.tsbuildinfo' -print 2>/dev/null || true - ) - if [ -n "$tsbuild_files" ]; then - printf '%s\n' "$tsbuild_files" | while IFS= read -r f; do - [ -n "$f" ] || continue - rm -f "$f" - done - removed=1 - fi - fi - - if [ "$removed" = 1 ]; then - echo "post-checkout: cleared TypeScript project emit (dist / tsbuildinfo) under $root" >&2 - echo "post-checkout: workaround for tsgo stale incremental cache after tree switch — see .githooks/README.md (remove when upstream fixed)" >&2 - fi -} - -clean_ts_project_emit "$wt_root" - -# T3 launches the configured worktree setup command in a visible terminal. Keep -# cleanup synchronous, but leave dependency installation to that async setup. -if [ "${T3CODE_DEFER_DEPENDENCY_INSTALL:-0}" = "1" ]; then - echo "post-checkout: dependency installation deferred to T3 workspace initialization" >&2 - exit 0 -fi - -# --- pnpm install when lockfile / package.json changed ----------------------- -[ -f "$wt_root/pnpm-lock.yaml" ] || exit 0 -state_file="$wt_root/node_modules/.pnpm-checkout-state" -state=$(cksum "$wt_root/package.json" "$wt_root/pnpm-lock.yaml" | cksum | awk '{print $1 ":" $2}') -if [ -f "$wt_root/node_modules/.modules.yaml" ] && [ "$(cat "$state_file" 2>/dev/null)" = "$state" ]; then - exit 0 -fi -command -v pnpm >/dev/null 2>&1 || { - echo "post-checkout: pnpm not on PATH; node_modules not installed" >&2 - exit 1 -} - -export CI=1 -echo "post-checkout: pnpm install in $wt_root" >&2 -cd "$wt_root" || exit 1 -if pnpm install --frozen-lockfile --prefer-offline \ - --config.package-import-method=hardlink \ - --config.confirmModulesPurge=false -then - printf '%s\n' "$state" >"$state_file" -else - status=$? - echo "post-checkout: dependency preparation failed (exit $status); worktree remains usable but degraded" >&2 - [ "${T3CODE_WORKTREE_PREPARATION_STRICT:-0}" = "1" ] && exit "$status" - exit 0 -fi diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100755 index 8fd0ddf8c..000000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 1 -if [ ! -x "$root/node_modules/.bin/lint-staged" ]; then - echo "pre-commit: worktree preparation is incomplete (lint-staged is missing)" >&2 - echo "pre-commit: run pnpm install before committing" >&2 - exit 1 -fi -# The binary directly, not `pnpm lint-staged`: pnpm runs a dependency-status -# check first and will try to purge node_modules when the lockfile moved, which -# aborts without a TTY — the commit then fails with a pnpm error that says -# nothing about linting. The guard above already established this path exists. -exec "$root/node_modules/.bin/lint-staged" diff --git a/.githooks/pre-push b/.githooks/pre-push deleted file mode 100755 index 6ba4a4cba..000000000 --- a/.githooks/pre-push +++ /dev/null @@ -1,17 +0,0 @@ -# Agent-only ship gate (humans: no-op). See scripts/agent-pre-push.mjs. -# Every push (draft included): check + lint + test. No browser/API suite exists -# here, so there is nothing a draft could usefully defer. -# Publish: pnpm pr:ready, or raw `gh pr ready` — the shim runs the same gate first. -# -# Humans: SKIP_AGENT_PREPUSH=1 git push -# Agents: never SKIP_AGENT_PREPUSH / never --no-verify -# Nested Git commands must discover their own repositories, not inherit this -# hook's GIT_DIR/GIT_WORK_TREE from the outer push. -for name in $(git rev-parse --local-env-vars); do - unset "$name" -done -# Plain `node`, not `pnpm exec node`: pnpm runs a dependency-status check first -# and will try to purge node_modules when the lockfile moved (a rebase does it), -# which aborts without a TTY — the hook then fails with a pnpm error instead of -# anything about the gate. The gate puts node_modules/.bin on PATH itself. -node scripts/agent-pre-push.mjs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 65e15abd8..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: [patroza] diff --git a/repos/effect/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml similarity index 92% rename from repos/effect/.github/actions/setup/action.yaml rename to .github/actions/setup/action.yaml index 0f97b61a1..7f3a2a2de 100644 --- a/repos/effect/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -21,6 +21,8 @@ runs: with: node-version: 26.4.0 package-manager-cache: ${{ env.NSC_CONTAINER_REGISTRY == '' }} + cache: ${{ env.NSC_CONTAINER_REGISTRY == '' && 'pnpm' || '' }} + cache-dependency-path: pnpm-lock.yaml - name: Configure pnpm cache if: ${{ env.NSC_CONTAINER_REGISTRY != '' }} uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1 diff --git a/.github/npm-trusted-publishing.md b/.github/npm-trusted-publishing.md deleted file mode 100644 index 0eaf53c68..000000000 --- a/.github/npm-trusted-publishing.md +++ /dev/null @@ -1,85 +0,0 @@ -# npm Trusted Publishing (OIDC) - -Publish from GitHub Actions without a long-lived `NPM_TOKEN`. -npm mints a short-lived credential per workflow run via OIDC. - -Official docs: https://docs.npmjs.com/trusted-publishers/ - -## Prerequisites - -- You must be an **owner** (or have settings access) on each package below. -- Workflow that publishes: `.github/workflows/release.yaml` (filename is `release.yaml`). -- GitHub repo: `effect-app/libs` (public, GitHub-hosted runners). - -## Packages to configure - -Configure **each** publishable package (one trusted publisher per package): - -| Package | npm settings URL | -| ---------------------------------- | --------------------------------------------------------------------- | -| `effect-app` | https://www.npmjs.com/package/effect-app/access | -| `@effect-app/infra` | https://www.npmjs.com/package/@effect-app/infra/access | -| `@effect-app/vue` | https://www.npmjs.com/package/@effect-app/vue/access | -| `@effect-app/vue-components` | https://www.npmjs.com/package/@effect-app/vue-components/access | -| `@effect-app/cli` | https://www.npmjs.com/package/@effect-app/cli/access | -| `@effect-app/eslint-codegen-model` | https://www.npmjs.com/package/@effect-app/eslint-codegen-model/access | -| `@effect-app/eslint-shared-config` | https://www.npmjs.com/package/@effect-app/eslint-shared-config/access | - -If the UI shows **Settings** / **Trusted Publisher** instead of `/access`, use that page for the same package. - -## Exact steps on npmjs.com (per package) - -Repeat for every package in the table: - -1. Open the package page → **Settings** (or the access URL above). -2. Find **Trusted Publisher**. -3. Under **Select your publisher**, choose **GitHub Actions**. -4. Fill in exactly: - - **Organization or user:** `effect-app` - - **Repository:** `libs` - - **Workflow filename:** `release.yaml`\ - (filename only, including `.yaml` — not `.yml`, not a path) - - **Environment name:** leave **empty** (unless you later add a GitHub Environment to the job) - - **Allowed actions:** select **`npm publish`** (required) -5. Save. - -npm does **not** validate the config on save — mismatches only show up at publish time (often as `E404`). - -## Order of operations (important) - -1. **Configure trusted publishers** for all packages above (npm side). -2. **Merge** the PR that enables OIDC in `.github/workflows/release.yaml` (`id-token: write`, no `NPM_TOKEN` on publish). -3. On push to `main`, the Changesets workflow publishes any unpublished versions. -4. Confirm success in the Actions log: look for\ - `No NPM_TOKEN found, but OIDC is available - using npm trusted publishing`. -5. After a successful OIDC publish, optionally harden each package: - - **Publishing access** → **Require two-factor authentication and disallow tokens** - - Revoke the old GitHub secret `NPM_TOKEN` and any automation tokens on npm. - -Do **not** set `NPM_TOKEN` on the publish step once OIDC is intended — `changesets/action` will use the long-lived token instead of OIDC when that env var is present. - -## GitHub workflow - -Live workflow: **`.github/workflows/release.yaml`**. - -It includes: - -- `permissions.id-token: write` -- Node `24.22.1` (latest 24.x; npm 12 needs Node ≥ `24.15`) -- Workflow upgrades to `npm@latest` after setup-node (OIDC needs npm ≥ `11.5.1`) -- **No** `NPM_TOKEN` on the publish step -- Package `repository.url` points at `https://github.com/effect-app/libs.git` (set in package.json) - -## Troubleshooting - -| Symptom | Likely cause | -| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| `E404` / “could not be found or you do not have permission” | Trusted publisher mismatch (org/repo/workflow filename), or OIDC not used because `NPM_TOKEN` is still set | -| `ENEEDAUTH` / unable to authenticate | Missing `id-token: write`, old npm CLI, or self-hosted runner (not supported) | -| Only some packages publish | Trusted publisher missing on those packages | -| `EBADENGINE` installing `npm@latest` | Node pin too old (npm 12 needs ≥ `24.15`); use `24.22.1` or newer 24.x | - -## Optional hardening - -- GitHub **Environment** (e.g. `npm`) on the `version` job with required reviewers; if used, put the same environment name in every package’s trusted publisher config. -- Keep a **read-only** granular npm token only if install needs private packages (not required for this public monorepo today). diff --git a/repos/effect/.github/workflows/ai-codegen.yml b/.github/workflows/ai-codegen.yml similarity index 100% rename from repos/effect/.github/workflows/ai-codegen.yml rename to .github/workflows/ai-codegen.yml diff --git a/repos/effect/.github/workflows/bundle-comment.yml b/.github/workflows/bundle-comment.yml similarity index 100% rename from repos/effect/.github/workflows/bundle-comment.yml rename to .github/workflows/bundle-comment.yml diff --git a/repos/effect/.github/workflows/check.yml b/.github/workflows/check.yml similarity index 96% rename from repos/effect/.github/workflows/check.yml rename to .github/workflows/check.yml index fbb21ced9..6666cb00d 100644 --- a/repos/effect/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -50,13 +50,15 @@ jobs: - name: Install dependencies uses: ./.github/actions/setup with: - deno-version: v2.9.7 + deno-version: v2.9.6 - run: pnpm check env: # Leave memory headroom for the runner by collecting the Go heap earlier. GOMEMLIMIT: 4GiB - run: deno check . - run: pnpm test-types --target '>=5.9' + env: + TSTYCHE_FETCH_TIMEOUT: "120" build: name: Build @@ -116,6 +118,8 @@ jobs: runs-on: namespace-profile-linux-default env: EFFECT_INTEGRATION_TESTS: "1" + # MSSQL integration tests are too flaky for CI; run them locally instead. + EFFECT_MSSQL_INTEGRATION_TESTS: "0" # The ephemeral runner provides cleanup without a Ryuk sidecar. TESTCONTAINERS_RYUK_DISABLED: "true" permissions: @@ -130,7 +134,7 @@ jobs: bun: "" command: pnpm test --max-concurrency=10 - runtime: Deno - deno: v2.9.7 + deno: v2.9.6 bun: "" command: deno task test --max-concurrency=10 - runtime: Bun @@ -143,9 +147,8 @@ jobs: - name: Pre-pull test container images run: | docker pull ghcr.io/tursodatabase/libsql-server:main & - docker pull postgres:alpine & + docker pull postgres:14-alpine & docker pull mysql:lts & - docker pull mcr.microsoft.com/mssql/server:2022-latest & docker pull vitess/vttestserver:mysql80 & docker pull redis:alpine & docker pull coredns/coredns:1.14.7 & diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 53432c967..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: CI - -on: - # push: - # branches: [main] # CI on main branch is handled by release.yaml workflow - pull_request: - branches: [main] - workflow_call: - inputs: - publish_snapshot: - description: Run snapshot publish inside this reusable workflow - type: boolean - default: false - workflow_dispatch: - -env: - CI: true - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - pull-requests: write - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 24.22.1 - - - name: Install pnpm - uses: pnpm/action-setup@v6.1.0 - with: - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: | - pnpm install --frozen-lockfile || { - echo "::warning::pnpm install failed; wiping store and retrying" - rm -rf "$(pnpm store path)" - pnpm install --frozen-lockfile - } - - - name: Run lint - run: pnpm lint - - build: - name: Build - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 24.22.1 - - - name: Install pnpm - uses: pnpm/action-setup@v6.1.0 - with: - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: | - pnpm install --frozen-lockfile || { - echo "::warning::pnpm install failed; wiping store and retrying" - rm -rf "$(pnpm store path)" - pnpm install --frozen-lockfile - } - - - name: Run build - run: pnpm build - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: build-output - path: packages/*/dist - retention-days: 1 - if-no-files-found: error - - test: - name: Test - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 24.22.1 - - - name: Install pnpm - uses: pnpm/action-setup@v6.1.0 - with: - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: | - pnpm install --frozen-lockfile || { - echo "::warning::pnpm install failed; wiping store and retrying" - rm -rf "$(pnpm store path)" - pnpm install --frozen-lockfile - } - - - name: Run tests - run: pnpm test - - # On main, Changesets calls this workflow and then runs `version` with - # `needs: [ci]`. Keep snapshot out of that reusable path so version does - # not wait for pkg.pr.new. Main snapshot runs as a sibling in release.yaml. - snapshot: - name: Publish Snapshot - needs: [build] - if: | - inputs.publish_snapshot || - (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || - github.event_name == 'workflow_dispatch' - uses: ./.github/workflows/publish-snapshot.yml - permissions: - contents: read - pull-requests: write - secrets: inherit diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index eb9719ecc..000000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr *)' diff --git a/repos/effect/.github/workflows/cluster.yml b/.github/workflows/cluster.yml similarity index 100% rename from repos/effect/.github/workflows/cluster.yml rename to .github/workflows/cluster.yml diff --git a/repos/effect/.github/workflows/effect-bot.yml b/.github/workflows/effect-bot.yml similarity index 100% rename from repos/effect/.github/workflows/effect-bot.yml rename to .github/workflows/effect-bot.yml diff --git a/repos/effect/.github/workflows/npm-promote.yml b/.github/workflows/npm-promote.yml similarity index 100% rename from repos/effect/.github/workflows/npm-promote.yml rename to .github/workflows/npm-promote.yml diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml deleted file mode 100644 index 50e8c17f2..000000000 --- a/.github/workflows/publish-snapshot.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Publish Snapshot - -on: - workflow_call: - -permissions: - contents: read - pull-requests: write - -env: - CI: true - -jobs: - snapshot: - name: Publish Snapshot - timeout-minutes: 20 - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 24.22.1 - - - name: Install pnpm - uses: pnpm/action-setup@v6.1.0 - with: - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: | - pnpm install --frozen-lockfile || { - echo "::warning::pnpm install failed; wiping store and retrying" - rm -rf "$(pnpm store path)" - pnpm install --frozen-lockfile - } - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: build-output - path: packages/ - - - name: Publish snapshots to pkg.pr.new - id: publish - run: pnpm exec pkg-pr-new publish --pnpm ./packages/* - - - name: Write snapshot usage summary - id: usage - shell: bash - run: | - PACKAGE_LOCATORS="${{ steps.publish.outputs.packages }}" - PACKAGE_URLS="${{ steps.publish.outputs.urls }}" - PUBLISHED_SHA="${{ steps.publish.outputs.sha }}" - if [ -z "$PACKAGE_LOCATORS" ] || [ -z "$PACKAGE_URLS" ] || [ -z "$PUBLISHED_SHA" ]; then - echo "pkg-pr-new did not return expected outputs (packages/urls/sha)" >&2 - exit 1 - fi - - { - echo "## Snapshot publish complete" - echo - echo "- SHA: \`${PUBLISHED_SHA}\`" - echo - echo "Published via pkg.pr.new." - echo - echo "### Install package locators" - echo "\`\`\`bash" - echo "$PACKAGE_LOCATORS" - echo "\`\`\`" - echo - echo "### Package URLs" - echo "\`\`\`text" - echo "$PACKAGE_URLS" - echo "\`\`\`" - } >> "$GITHUB_STEP_SUMMARY" - - { - echo "package_locators<> "$GITHUB_OUTPUT" diff --git a/.github/workflows/publish-wiki.yml b/.github/workflows/publish-wiki.yml deleted file mode 100644 index c66cc7707..000000000 --- a/.github/workflows/publish-wiki.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Publish wiki -on: - push: - branches: [main] - paths: - - wiki/** - - .github/workflows/publish-wiki.yml -concurrency: - group: publish-wiki - cancel-in-progress: true -permissions: - contents: write -jobs: - publish-wiki: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: Andrew-Chen-Wang/github-wiki-action@v4 diff --git a/repos/effect/.github/workflows/release-queue.yml b/.github/workflows/release-queue.yml similarity index 95% rename from repos/effect/.github/workflows/release-queue.yml rename to .github/workflows/release-queue.yml index c23e57485..5f2bd7180 100644 --- a/repos/effect/.github/workflows/release-queue.yml +++ b/.github/workflows/release-queue.yml @@ -35,7 +35,7 @@ jobs: if: github.event.pull_request env: GITHUB_TOKEN: ${{ secrets.EFFECT_BOT_GH }} - - uses: Effect-TS/next-release-action@63daaa626282bc42fad44dcb89c7f7db9c53b786 + - uses: Effect-TS/next-release-action@9c7fe33d6e497924ae795d53c0c1cf75d48c96c2 with: github_token: ${{ secrets.EFFECT_BOT_GH }} base_branch: main diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 470d02444..000000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: Changesets -on: - push: - branches: - - main -# id-token: write enables npm Trusted Publishing (OIDC). -# Do not pass NPM_TOKEN on the publish step — changesets/action prefers a -# long-lived token when set and will skip OIDC. -# See .github/npm-trusted-publishing.md for npmjs.com setup. -permissions: - contents: write - pull-requests: write - id-token: write -env: - CI: true - PNPM_CACHE_FOLDER: .pnpm-store -jobs: - ci: - uses: ./.github/workflows/ci.yml - - snapshot: - name: Publish Snapshot - needs: [ci] - if: | - github.actor != 'github-actions[bot]' && - !contains(github.event.head_commit.message, 'chore: update versions') - uses: ./.github/workflows/publish-snapshot.yml - permissions: - contents: read - pull-requests: write - secrets: inherit - - version: - needs: [ci] - timeout-minutes: 15 - runs-on: ubuntu-latest - steps: - - name: checkout code repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - # Latest 24.x. npm@12 needs Node >= 24.15. - node-version: 24.22.1 - registry-url: https://registry.npmjs.org - - # Trusted publishing requires npm CLI >= 11.5.1; keep CLI current on this Node line. - - name: Ensure npm supports OIDC trusted publishing - run: npm install -g npm@latest && npm --version - - - uses: pnpm/action-setup@v6.1.0 - name: Install pnpm - id: pnpm-install - with: - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: install packages - run: | - pnpm install --frozen-lockfile || { - echo "::warning::pnpm install failed; wiping store and retrying" - rm -rf "$(pnpm store path)" - pnpm install --frozen-lockfile - } - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: create and publish versions - uses: changesets/action@v1 - with: - version: pnpm ci:version - commit: "chore: update versions" - title: "chore: update versions" - publish: pnpm ci:publish - createGithubReleases: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/repos/effect/.github/workflows/release.yml b/.github/workflows/release.yml similarity index 100% rename from repos/effect/.github/workflows/release.yml rename to .github/workflows/release.yml diff --git a/repos/effect/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml similarity index 100% rename from repos/effect/.github/workflows/snapshot.yml rename to .github/workflows/snapshot.yml diff --git a/.gitignore b/.gitignore index e98229014..26bba5ae5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,64 +1,41 @@ -.claude/worktrees -.DS_Store -node_modules/ -*.log -**/*/.next/ -packages/**/dist/ -!**/.eslintrc*.js -.build/ -dist/ -**/bin/*.js -**/bin/*.js.map -**/bin/*.d.ts -**/bin/*.d.ts.map -tsconfig.tsbuildinfo -.tsbuildinfo -.ultra.cache.json -.jest-cache -.next -.data/ -.npmrc -.cache/ -.opencode/ -.env.local +# Generated by Direnv .env -# CMake -cmake-build-*/ - - -# File-based project format -*.iws - -# IntelliJ -out/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# End of https://www.toptal.com/developers/gitignore/api/intellij - -.infra/*_rev.yaml +# Dot files and directories +**/.* +!/.agents/ +!/.changeset/ +!/.envrc +!/.github/ +!/.gitignore +!/.oxlintrc.json +!/.vscode/ + +# Generated by TypeScript +dist/ +build/ +lib/ +**/*.tsbuildinfo -# vitest? -*.timestamp-*.mjs +# Generated by Pnpm +node_modules/ -# nix -.direnv +# Generated by scripts +coverage/ +tmp/ +stats.txt +# Generated by MacOS +.DS_Store -storybook-static/ -run.sh +# Scratchpad files +scratchpad/**/* -# Generated publish staging directories -packages/*/.publish/ +# Agent instructions +/AGENTS.md +/packages/**/AGENTS.md +/packages/**/CLAUDE.md +/packages/**/ai-docs/ -# agent gh shim (generated) + local ship-gate state -.tools/ -.run/ +# Agent config +opencode.json diff --git a/.ncurc.json b/.ncurc.json deleted file mode 100644 index be9722c1c..000000000 --- a/.ncurc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "reject": [ - "glob", - "@types/glob", - "jwks-rsa", - "applicationinsights", - "@faker-js/faker", - "redis", - "@types/redis", - "react-hook-form", - "eslint-plugin-codegen", - "@tanstack/*" - ], - "dep": [ - "prod", - "dev", - "optional", - "peer" - ] -} diff --git a/.npmrc b/.npmrc deleted file mode 100644 index ad2a213cd..000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -# @effect-app:registry=https://npm.pkg.github.com diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 474d750b5..000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v24.22.1 diff --git a/.oxlintrc.json b/.oxlintrc.json index fd83f9442..3fcaaf516 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,47 +1,37 @@ { - "$schema": "./node_modules/@effect/tsgo/oxlint-schema.json", - "extends": ["./packages/eslint-shared-config/src/oxlintrc.json"], - "options": { - "typeAware": true - }, - "plugins": ["effecttsgo"], - "rules": { - "effecttsgo/any-unknown-in-error-context": "off", - "effecttsgo/effect-fn-iife": "off", - "effecttsgo/global-error-in-effect-catch": "off", - "effecttsgo/layer-merge-all-with-dependencies": "off", - "effecttsgo/lazy-effect": "off", - "effecttsgo/return-effect-in-gen": "off", - "effecttsgo/run-effect-inside-effect": "off", - // Stays off. A Schema brand type argument (`{ readonly brand: unique symbol }`) - // is the wrong nominal marker here. If we brand a class, use a protected field - // so a plain object is not a class instance unless it comes from `new` / `.make`: - // class Foo extends Schema.TaggedClass()("Foo", { /* fields */ }) { - // declare protected "~brand~": never - // } - "effecttsgo/schema-class-missing-brand": "off", - "effecttsgo/schema-sync-in-effect": "off", - "effecttsgo/strict-effect-provide": "off", - "effecttsgo/unsafe-effect-type-assertion": "off", - "effecttsgo/unstable-api-usage": "off", - "effecttsgo/floating-effect": "off", - "effecttsgo/multiple-effect-provide": "off", - "effecttsgo/unknown-in-effect-catch": "off", - "effecttsgo/missing-effect-service-dependency": "error", - "effecttsgo/effect-fn-opportunity": "warn", - "effecttsgo/global-error-in-effect-failure": "warn", - "effecttsgo/prefer-schema-over-json": "warn" - }, - // ignorePatterns overwrites (not merges) the base — all standard patterns must be repeated here. + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "extends": ["./packages/tools/oxc/oxlintrc.json"], "ignorePatterns": [ - "**/dist/**", - "**/node_modules/**", - "**/*.d.ts", - "**/*.js", - "**/*.jsx", - "**/vitest.config.ts", - "**/eslint.*.mjs", + "LLMS.md", + "**/dist", + "**/build", + "**/docs", + "**/*.tsbuildinfo", "**/CHANGELOG.md", - "repos/**" - ] + "packages/effect/typeperf/**/*", + "packages/effect/src/StandardSchema.ts", + "packages/tools/graphql-generator/test/generated/**/*.graphql.ts", + "!scratchpad/**/*", + "**/.*", + "!.agents", + "!.agents/**/*", + "!.changeset", + "!.changeset/**/*", + "!.envrc", + "!.github", + "!.github/**/*", + "!.gitignore", + "!.oxlintrc.json", + "!.vscode", + "!.vscode/**/*" + ], + "jsPlugins": ["@effect/oxc/oxlint"], + "overrides": [{ + "files": ["**/{test,typetest,examples,ai-docs,benchmark,bundle,scripts,scratchpad}/**"], + "rules": { + "effect/no-bigint-literals": "off", + "eslint/no-console": "off", + "effect/no-import-from-barrel-package": "off" + } + }] } diff --git a/.specs/Update-to-latest-Effect.md b/.specs/Update-to-latest-Effect.md deleted file mode 100644 index 88edfa6d0..000000000 --- a/.specs/Update-to-latest-Effect.md +++ /dev/null @@ -1,23 +0,0 @@ -# Update to latest effect packages - -## Rules - -- only include root `package.json` and packages under the `packages` folders - -## Steps - -1. run `pnpm test` and `pnpm lint-fix` to compare later -2. update package.json files -3. run `pnpm i` -4. commit the dep bump (package.json files + `pnpm-lock.yaml`) — `pnpm subtree:effect` runs `git subtree pull` which **requires a clean working tree** (`fatal: working tree has modifications. Cannot add.` otherwise). Commit before syncing. -5. run `pnpm subtree:effect` to sync the `repos/effect` subtree to the same version (creates its own squash-merge commit), then run `pnpm i` inside it. -6. run test and lint again, compare to from before the update. -7. create a changeset describing the changes -8. prepare commit (changeset + any test/lint fallout) - -If new errors occur, first describe the problem, propose solutions and wait for answers. - -## Notes - -- subtree url: `https://github.com/Effect-TS/effect.git`, prefix `repos/effect`, tag `effect@`. -- v4 currently lives under the npm `rc` dist-tag (`beta` is frozen at `4.0.0-beta.107`; npm `latest` is still v3). Find target with `npm view effect dist-tags` — prefer `rc` if present, else `beta`. diff --git a/.specs/changeset-publishing.md b/.specs/changeset-publishing.md deleted file mode 100644 index 7bd8ac4a6..000000000 --- a/.specs/changeset-publishing.md +++ /dev/null @@ -1,138 +0,0 @@ -# Changesets Publishing - -## Summary - -Publish `effect-app`, `infra`, `vue` and `vue-components` under unified -versions using changesets' `fixed` groups. - -## Background - -All public packages must move in lockstep under a single version number (`4.0.0-beta.0`, `4.0.0-beta.1`, ...) -so consumers can install a coherent set of packages at any beta iteration. -Changesets supports this via **fixed version groups** (all packages in a group share the same -version). - -## Goals - -- All 4 public library packages publish the same version on every release. -- Developers continue using the standard `pnpm changeset` workflow. - -## Non-goals - -- No changes to the build system, CI pipeline scripts, or package contents. -- No independent versioning — all packages move together. -- No canary/nightly publishing strategy. - -## How it works - -### Fixed groups - -The `fixed` field in `.changeset/config.json` takes an array of package-name -arrays. All packages in a group are bumped to the **same version** whenever any -one of them is included in a changeset. By placing all 24 public packages in one -group, a single changeset touching any package triggers a version bump for every -package. - -### Version stability during pre-release - -A common concern: won't a `minor` or `patch` changeset during the beta change -the base version (e.g., `4.0.0-beta.1` → `4.1.0-beta.0`)? No — changesets -prevents this via `pre.json`. - -`pre.json` stores two critical fields: - -- **`initialVersions`**: the version of each package when pre-release mode was - entered (e.g., `3.0.0`). -- **`changesets`**: the IDs of every changeset applied since entering - pre-release mode. - -When `changeset version` runs, it computes the target version relative to -`initialVersions` using the **cumulative** bump of all tracked changesets (both -already-applied ones in `pre.json.changesets` and new pending ones). Because the -initial major changeset is always in the tracked set, the highest bump type is -always `major`, and the base version stays at `4.0.0` regardless of subsequent -patch or minor changesets. Only the beta counter increments. - -Example: - -1. Enter pre-release, `initialVersions` = `3.0.0` -2. Major changeset → `changeset version` → `4.0.0-beta.0` (major from `3.0.0`) -3. Developer adds a patch changeset → `changeset version` → `4.0.0-beta.1` - (major still highest → base unchanged, counter increments) -4. Developer adds a minor changeset → `changeset version` → `4.0.0-beta.2` - (major still highest → base unchanged, counter increments) - -The base semver never drifts during the beta period. - -### Combined behavior - -With both features active, every `changeset version` + `changeset publish` cycle -produces a release where all 24 packages share `4.0.0-beta.`. - -## Setup steps - -### 1. Update `.changeset/config.json` - -Add the `fixed` group and change `access` to `"public"`: - -## Ongoing workflow - -### Adding changesets - -Developers continue using `pnpm changeset` as usual. Because of the `fixed` -group, the bump type (patch/minor/major) of any individual changeset is -effectively irrelevant to the version number — all packages are already on a -major bump track. However, the changeset message still serves as the changelog -entry, so meaningful descriptions are important. - -### Publishing a new version - -```bash -npx changeset version # bumps to 4.0.0-beta.(n+1) -npx changeset publish # publishes all packages under "beta" tag -``` - -The counter auto-increments. No manual version editing is needed. - -### CI integration - -The existing CI publish workflow (typically `changeset version` + -`changeset publish` in a GitHub Action) works without modification. The -pre-release mode and fixed group are configuration-only — no script changes -required. - -## Graduating to stable - -When ready to release `4.0.0`: - -```bash -npx changeset pre exit # removes pre-release mode -npx changeset version # sets all packages to 4.0.0 -npx changeset publish # publishes to "latest" dist-tag -``` - -After exiting, the `fixed` group can remain to keep packages in lockstep for -future `4.x` releases, or be removed if independent versioning is desired. - -## Edge cases - -### Workspace dependency ranges - -`updateInternalDependencies: "patch"` ensures workspace `dependencies` and -`peerDependencies` are updated whenever a dependency's version changes. Because -all packages share the same version, cross-references will always point to the -current beta. Verify that dependency ranges use `workspace:^` (pnpm protocol) so -they resolve correctly during development and are rewritten to concrete ranges on -publish. - -### npm version collision safety - -Changesets' pre-release counter prevents collisions — `4.0.0-beta.0`, -`4.0.0-beta.1`, etc. are distinct versions. npm's immutability guarantees that a -published version can never be overwritten. - -## Files to modify - -| File | Change | -| ------------------------ | --------------------------------------------- | -| `.changeset/config.json` | Add `fixed` array, set `access` to `"public"` | diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6f2a533fc..1a118e9e7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,6 @@ { "recommendations": [ "oxc.oxc-vscode", - "dprint.dprint", - "dbaeumer.vscode-eslint", - "editorconfig.editorconfig", - "Vue.volar" + "dprint.dprint" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 3a998c7db..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "type": "node", - "request": "launch", - "name": "Debug TSC", - "program": "${workspaceFolder}/node_modules/tsc.js", - "args": [ - "--build tsconfig.all.json" - ], - "sourceMaps": false, - "protocol": "inspector", - "configurations": [ - { - "name": "Attach by Process ID", - "processId": "${command:PickProcess}", - "request": "attach", - "skipFiles": [ - "/**" - ], - "type": "node" - } - ] -} diff --git a/.vscode/model.code-snippets b/.vscode/model.code-snippets deleted file mode 100644 index 3802d7720..000000000 --- a/.vscode/model.code-snippets +++ /dev/null @@ -1,77 +0,0 @@ -{ - "ModelCodegen": { - "prefix": "genmod", - "body": [ - "// codegen:start {preset: model}" - ] - }, - "ResourceCodegen": { - "prefix": "genrsc", - "body": [ - "// codegen:start {preset: meta, sourcePrefix: src/resources/}" - ] - }, - "ModelComplete": { - "prefix": "moc", - "body": [ - "export class $1 extends S.ExtendedClass<$1, $1.Encoded>()({", - "$2", - "}) {}" - ], - "description": "Defines a Model signature" - }, - "Model": { - "prefix": "mo", - "body": [ - "export class $1 extends S.Class<$1>()({$2}) {}", - "" - ], - "description": "Defines a Model signature" - }, - "UnionOpaque": { - "prefix": "un", - "body": [ - "const $1__ = union({ $2 })", - "const $1_ = enhanceClassUnion(OpaqueSchema<$1, $1.Encoded>()($1__))", - "export type $1 = To & UnionBrand", - "export interface $1Schema extends Identity {}", - "export const $1: $1Schema = $1_", - "export namespace $1 {", - " export type Encoded = Encoded & UnionBrand", - "}" - ] - }, - "Request": { - "prefix": "req", - "body": [ - "export class $1 extends S.Req<$1>()(\"$1\", {", - " $2", - "}, { success: $3 }) {}", - "" - ], - "description": "Defines a Request signature" - }, - "Response": { - "prefix": "res", - "body": [ - "export class Response extends S.Class", - " Effect.gen(function*() {", - " //const userRepo = yield* UserRepo", - "", - " return {", - " $2: $2($3)", - " }", - " })" - ] - } -} \ No newline at end of file diff --git a/.vscode/service.code-snippets b/.vscode/service.code-snippets deleted file mode 100644 index b3314afcb..000000000 --- a/.vscode/service.code-snippets +++ /dev/null @@ -1,14 +0,0 @@ -{ - "ServiceConstructor": { - "prefix": "svc", - "body": [ - "export class $1 extends Effect.Service<$1>()(\"$1\", {", - " dependencies: [$3],", - " effect: Effect.gen(function*() {", - " $2", - " return {}", - " })", - "}) {}", - ] - } -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 362ed5f03..672eea785 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,69 +1,51 @@ { - "files.exclude": { - "**/.git": false - }, - "explorer.sortOrderLexicographicOptions": "upper", - "typescript.preferences.includePackageJsonAutoImports": "on", - "typescript.experimental.expandableHover": true, - "js/ts.hover.maximumLength": 5000, - "typescript.preferences.autoImportFileExcludePatterns": [ - "node_modules/@sentry/node", - "node_modules/vitest/dist", - "node_modules/@azure/cosmos", - "node_modules/effect-app/Schema", - "node_modules/lodash", - "node_modules/@types/lodash" - ], - "typescript.tsdk": "node_modules/typescript/lib", - "eslint.workingDirectories": [ - { - "pattern": "apps/*" - }, - { - "pattern": "packages/*" - } - ], + "dprint.path": "node_modules/.bin/dprint", "editor.formatOnSave": true, + "editor.defaultFormatter": "dprint.dprint", "editor.formatOnSaveMode": "file", "editor.codeActionsOnSave": { - "source.fixAll.oxc": "explicit", - "source.fixAll.eslint": "explicit", - "source.addMissingImports": "explicit" + "source.fixAll.oxc": "explicit" + }, + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": false + }, + "editor.acceptSuggestionOnCommitCharacter": true, + "editor.acceptSuggestionOnEnter": "on", + "editor.quickSuggestionsDelay": 10, + "editor.suggestOnTriggerCharacters": true, + "editor.tabCompletion": "off", + "editor.suggest.localityBonus": true, + "editor.suggestSelection": "recentlyUsed", + "editor.wordBasedSuggestions": "matchingDocuments", + "editor.parameterHints.enabled": true, + "files.insertFinalNewline": true, + "[typescript]": { + "editor.defaultFormatter": "dprint.dprint" }, - "editor.defaultFormatter": "dprint.dprint", - "[vue]": { + "[typescriptreact]": { "editor.defaultFormatter": "dprint.dprint" }, "[javascript]": { "editor.defaultFormatter": "dprint.dprint" }, - "[javascriptreact]": { + "[json]": { "editor.defaultFormatter": "dprint.dprint" }, - "[typescript]": { + "[jsonc]": { "editor.defaultFormatter": "dprint.dprint" }, - "[typescriptreact]": { + "[markdown]": { "editor.defaultFormatter": "dprint.dprint" }, - "eslint.validate": [ - "javascript", - "typescript", - "vue" - ], - "eslint.format.enable": false, - "csv-preview.separator": ";", - "cSpell.words": [ - "codegen", - "composables", - "pipeable", - "tsplus" + "deno.enable": true, + "deno.lint": false, + "deno.enablePaths": [ + "./packages/platform/deno" ], - "files.watcherExclude": { - "**/target": true - }, - "chat.tools.terminal.autoApprove": { - "git add": true, - "git commit": true - } + "js/ts.tsdk.path": "./node_modules/typescript/bin", + "js/ts.tsdk.additionalLocations": ["./node_modules/typescript/bin"], + "js/ts.tsdk.promptToUseWorkspaceVersion": true, + "js/ts.experimental.useTsgo": true } diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 2d9f2e8a6..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "tasks": [ - { - "label": "build", - "type": "shell", - "command": "pnpm watch", - "options": { - "cwd": "${workspaceRoot}" - }, - "group": { - "kind": "build", - "isDefault": true - }, - "isBackground": true, - "presentation": { - "group": "watch-build" - }, - "problemMatcher": [ - { - "base": "$tsc-watch", - "fileLocation": [ - "relative", - "${workspaceRoot}" - ] - } - ] - }, - { - "label": "Run Storybook OmegaForm", - "type": "shell", - "command": "pnpm storybook", - "options": { - "cwd": "${workspaceRoot}/packages/vue-components" - }, - "problemMatcher": [] - } - ] -} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 0ca05556b..000000000 --- a/AGENTS.md +++ /dev/null @@ -1,217 +0,0 @@ -# Agent Instructions - -This is the Effect App library repository, focusing on functional programming patterns and effect systems in TypeScript, wrapping and extending the Effect library. - -## Development Workflow - -- The git base branch is `main` -- Use `pnpm` as the package manager - -### Pull requests - -Always open a PR for agent work that changes the repo — do not leave finished work only on a local branch. - -- **Draft early**: open a draft PR as soon as there is a meaningful commit (or when starting multi-step work that will land), so review/CI can track progress while the rest of the work continues. Pushes to a draft run the same gate as any other push — the draft is about review status, not about skipping checks. -- **Keep the PR current**: push commits as you go; update the PR body if scope shifts. -- **Ready when done**: publish with `pnpm pr:ready`, or with plain `gh pr ready` — both run the ship gate first and undraft only if it passes. Do not hand-run the checks beforehand and do not leave a finished, validated change as draft. -- **Base**: target `main` unless the work is explicitly stacked on another branch. - -### The gate runs the checks — you do not - -`.githooks/pre-push` is the agent ship gate. It is a **no-op for humans** and -fires only for coding agents (`GROK_AGENT` / `T3_AGENT` / `AI_AGENT` / Claude / -Cursor / Codex env markers). - -It runs on **every** agent push — draft, ready, or no PR at all. There is no -browser or API suite here that a draft could usefully defer, and a pushed commit -that does not compile or whose tests fail is worth nothing to a reviewer whatever -the PR says. - -The gate is `pnpm check` → `pnpm lint` → `pnpm test`, which is what CI runs and -nothing more. This is a library monorepo — no application to stand up, no browser -suite — so the unit run _is_ the gate. It caches the validated HEAD SHA in -`.run/agent-ship-gate.json`, so **one commit is validated once** however many -times you push or publish it. Force a re-run with `AGENT_SHIP_GATE_FORCE=1`. - -**Do not hand-run `pnpm check` / `lint` / `test` as routine verification.** -Validating the same commit repeatedly costs the same each time and proves nothing -the first run did not. While iterating, narrow proof is the right tool — the one -test you are fixing, or a typecheck of the package you touched. Whole-gate runs -belong to the push. - -`.envrc` puts the `gh` shim on `PATH`; run **`direnv allow`** once after cloning -(`command -v gh` should print `.tools/bin/gh`). Never `--no-verify`, and never set -`SKIP_AGENT_PREPUSH` — that is the human escape hatch. - -### Core Principles - -- **Zero Tolerance for Errors**: All automated checks must pass -- **Root causes, not workarounds**: When something fails — a type error, a runtime crash, a failing test, a hanging fiber — diagnose the invariant that was violated and fix it there. Do **not** mask the symptom with a wrapper, a revive helper, a `try/catch`, a `JSON.parse(JSON.stringify(...))`, or any other shape-coercion. Each of these is a tell that you skipped the diagnosis. -- **No `as any` / `as unknown` casts**: A specific case of the rule above. Casts hide the type system telling you the shape is wrong. Understand the actual types and fix the root cause. If a type mismatch exists, find the correct v4 API, update the type signatures, or restructure the code. -- **No hand-rolled deserialization**: Anything stored as JSON and read back must round-trip through a Schema codec (`S.fromJsonString(S.toCodecJson(...))`, `S.encodeEffect` / `S.decodeEffect`). Class prototypes — `Exit`, `Cause`, `Workflow.Result`, `Schema.Class` instances — do not survive `JSON.parse`. The schema layer is the canonical reconstruction path; writing a custom `reviveX` helper is a smell that the codec is missing. -- **Search prior art before writing helpers**: Before introducing a new utility for a cross-boundary concern (serialization, encoding, retries, context propagation, OCC), grep the repo and `repos/effect` for the established pattern. The cluster engine, the existing Cosmos adapter, the SQL stores — they have already solved most of these problems. Copying the established pattern beats inventing a parallel one. -- **Check newer branches before resuming work**: A new session may resume on `main` with no checkout of relevant feature branches. Before extending a topic, run `git branch -a | grep ` and inspect the latest version with `git show :`. Session-local memory of "how X works" may be from an older draft that has since been replaced. -- **Clarity over Cleverness**: Choose clear, maintainable solutions -- **Conciseness**: Keep code and any wording concise and to the point. Sacrifice grammar for the sake of concision. -- **Reduce comments**: Avoid comments unless absolutely required to explain unusual or complex logic. Comments in jsdocs are acceptable. -- **Look for effect sources inside `repos/effect`** -- **Never import local `repos` files**: Always use the latest online versions of packages instead. -- **Never webfetch from the effect repos**: just use the locally included under `repos` - -### When you hit an error, before writing any fix - -Errors are signals, not nuisances. Run this checklist: - -1. **State the failure precisely.** Quote the exact error or behavior. `"object is not iterable"` is not the same as `"wrong type"`. The message names the violated invariant. -2. **Name the boundary.** Where in the data flow did the value lose the property the consumer needs? At the storage write, the storage read, the network hop, the schema decode, the context provision? -3. **Search for prior art at that boundary.** `grep` the repo + `repos/effect` for how similar values cross the same boundary. If a pattern exists, use it. -4. **Only then write code.** If your fix re-implements something the searched-for pattern already does (revive prototypes, retry on OCC, encode JSON), stop — use the existing pattern instead. - -Anti-patterns that mean you skipped the checklist: - -- `JSON.parse` of a value that contains tagged classes, followed by manual prototype reconstruction. -- `try/catch` around a yield that swallows the error and returns a fabricated value. -- Adding a second `as any` to make a first `as any` typecheck. -- Adding a sleep / retry to "give it time to work" instead of finding the missing wake signal. -- Disabling a hook (`--no-verify`) or a check to make the diff land. - -### Validation - -The ship gate owns validation — see _The gate runs the checks — you do not_. It -runs `pnpm check` → `pnpm lint` → `pnpm test` on every push and on publish, once -per commit. - -`pnpm lint-fix` is the one thing worth running by hand, because it _writes_: it -formats and auto-fixes across packages, and the gate only reports what it would -have fixed. Run it when you are done editing, stage what it changes, then push. - -If type checking fails in a way that makes no sense against the diff, `pnpm clean` -clears the caches — stale incremental state produces phantom errors. - - - - -## Code Style Guidelines - -**Always** look at existing code in the repository to learn and follow -established patterns before writing new code. - -Do not worry about getting code formatting perfect while writing. Use `pnpm lint-fix` -to automatically format code according to the project's style guidelines. - -## Prefer `Effect.fnUntraced` over functions that return `Effect.gen` - -Instead of writing: - -```ts -const fn = (param: string) => - Effect.gen(function*() { - // ... - }) -``` - -Prefer: - -```ts -const fn = Effect.fnUntraced(function*(param: string) { - // ... -}) -``` - -## Using `Context.Service` - -Prefer the class syntax when working with `Context.Service`. For example: - -```ts -import { Context } from "effect-app" - -class MyService extends Context.Service number -}>()("MyService") {} -``` - -## Checking Array is not empty - -Avoid `.length > 0` or `.length === 0` or `!.length` or `!!.length` checks, use `Array.isArrayNonEmpty` for type narrowing by default. - - - - - - - -## Per-request `Effect.provide(layer)` must isolate its MemoMap - -`Effect.provide(self, layer)` resolves its `MemoMap` from the ambient fiber -context. On an HTTP server, that MemoMap lives on the server fiber and is -shared by every request that server handles. The first request to build a -stateful layer (anything using `Layer.effect` / `Effect.acquireRelease`) -memoizes the resulting value onto the server fiber; every subsequent request -then receives the _same instance_ — including its `clear()` / dispose -finalizer, which now fires at the wrong time. - -When you call `Effect.provide(layer)` (or `Stream.provide(layer)`) inside a -per-request hot path: - -- Pass `{ local: true }` if the layer is pure / stateless or you genuinely - want it scoped to _this_ effect only, or -- Build it explicitly against the request scope with a fresh `MemoMap` - (`Layer.makeMemoMap` + `Layer.buildWithMemoMap(layer, memoMap, requestScope)`) - — see `provideOnRequestScope` in `packages/infra/src/setupRequest.ts`. - -If neither option is taken and the layer carries state, the state leaks -across requests. Concrete repro lives in -`packages/infra/test/rpc-context-map-streaming.test.ts` (the overlapping -requests case). - -## Changesets - -All pull requests must include a changeset. You can create changesets in the -`.changeset/` directory. - -The have the following format: - -```md ---- -"package-name": patch | minor | major ---- - -A description of the change. -``` diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 8359239d2..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,3 +0,0 @@ -# CLAUDE.md - -Strictly follow the rules in ./AGENTS.md diff --git a/repos/effect/LICENSE b/LICENSE similarity index 100% rename from repos/effect/LICENSE rename to LICENSE diff --git a/repos/effect/LLMS.md b/LLMS.md similarity index 100% rename from repos/effect/LLMS.md rename to LLMS.md diff --git a/repos/effect/MIGRATION.md b/MIGRATION.md similarity index 100% rename from repos/effect/MIGRATION.md rename to MIGRATION.md diff --git a/README.md b/README.md index 96d052283..4d9effea2 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,98 @@ -# Practical use of Effect-TS + -This is an opinionated library for full-stack [Effect-TS](https://github.com/Effect-TS/core). -(See repositories for more info and discord link, articles, youtube videos, etc). +[![pkg.pr.new](https://img.shields.io/badge/pkg.pr.new-Effect--TS%2Feffect-black)](https://pkg.pr.new/~/Effect-TS/effect) -WIP [docs](https://github.com/effect-ts-app/docs) +# Effect -See https://github.com/effect-ts-app/boilerplate for a sample app use. +Effect is a library for building robust, maintainable, type-safe, and production grade applications in TypeScript. It helps you handle the hard problems at scale: typed errors, dependency injection, structured concurrency, scheduling, tracing, and unified schema validation. -## Package boundaries +> **Effect 4.x is a long-term support (LTS) release.** If you are upgrading from Effect 3.x, follow the [migration guide](MIGRATION.md). -- `effect-app`: service contracts and runtime-agnostic base logic. -- `@effect-app/infra`: backend / Node adapters. -- `@effect-app/vue`: Vue / browser adapters. - -Migration targets introduced in this repo include: - -- `@effect-app/infra/Emailer/service` -> `effect-app/Emailer` -- `@effect-app/infra/QueueMaker/service` -> `effect-app/QueueMaker` -- `@effect-app/infra/Store/service` -> `effect-app/Store` -- `@effect-app/infra/Model/*` -> `effect-app/Model/*` -- `@effect-app/vue/runtime` -> `effect-app/runtime` -- `@effect-app/vue/toast` -> `effect-app/toast` -- `@effect-app/vue/withToast` -> `effect-app/withToast` - -## Update Effect Subtree - -`repos/effect` is a git subtree of `https://github.com/Effect-TS/effect-smol.git`. - -Recommended (uses the currently pinned `effect` / `@effect/*` version from package.json): - -```sh -pnpm subtree:effect -``` - -Or directly via the CLI: - -```sh -effa sync-effect -``` - -Use a different repository URL: - -```sh -effa sync-effect --url https://github.com/Effect-TS/effect-smol.git -``` - -Manual update to a specific ref: - -```sh -git subtree pull --prefix=repos/effect https://github.com/Effect-TS/effect-smol.git --squash -``` - -Example: - -```sh -git subtree pull --prefix=repos/effect https://github.com/Effect-TS/effect-smol.git @effect/ai-anthropic@4.0.0-beta.47 --squash -``` - -## Update Effect App Libs Subtree - -Apps that vendor this repository as `repos/libs` can sync it to the pinned -`effect-app` / `@effect-app/*` version in their package.json: - -```sh -effa sync-effect-app -``` - -The command first tries Changesets package tags such as -`effect-app@4.0.0-beta.271` or `@effect-app/infra@4.0.0-beta.271`. If no -matching tag exists, it fetches `https://github.com/effect-app/libs.git` into a -local git cache and selects the latest commit on the default branch whose -package manifest still has the pinned version. - -Escape hatch for manual refs: +## Installation ```sh -effa sync-effect-app --ref main -effa sync-effect-app --ref latest # latest main -``` - -Both `sync-effect` and `sync-effect-app` fetch selected commits with -`--no-tags`, so syncing subtrees does not import upstream package tags into the -app repository. - -The clean release convention is to publish git tags for package versions -(`effect-app@`, `@effect-app/infra@`, etc.) from the release -workflow. The history scan exists because this repo still has older `v*` tags -but not current package-version tags. - -## Deployment - -Uses [Changesets](https://github.com/changesets/changesets/blob/main/README.md) - -1. make changes -2. generate and include changeset `pnpm changeset` -3. wait for build which creates a PR -4. inspect the PR, merge when alright -5. await new build and new package deployments - -## Thanks - -- All contributors -- Michael Arnaldi, Max Brown and the Effect-TS contributors for Effect - - ZIO Contributors for the excellent ZIO -- Anyone else we're forgetting.. - -## How to view OmegaForm docs - -On Mac: - -1. `Shift + Command + P` -2. Select `Run Task` -3. Select `Run Storybook OmegaForm` -4. Will automatically open browser to http://localhost:6006/ Enjoy! - -On Windows: - -1. Select "Run Task" - -```bash -cd packages/vue-components -``` - -2. run storybook - -```bash -pnpm storybook +npm install effect ``` -3. Will automatically open browser to http://localhost:6006/ Enjoy! +## Requirements + +- **TypeScript 5.9 or newer.** TypeScript 7 is recommended for the best performance and compatibility with [Effect's TypeScript tooling](https://github.com/Effect-TS/tsgo#installation). +- **Node.js 18 or newer** is the general minimum for running Effect on Node.js. Some integration packages require newer runtimes; for example, `@effect/sql-sqlite-node` requires Node.js 22.16 or newer. +- **Strict type-checking:** the `strict` flag must be enabled in your `tsconfig.json`. + +## Links + +- [Website](https://effect.website): documentation, guides, and news +- [Discord](https://discord.gg/effect-ts): ask questions, share what you're building, and talk to the core team +- [Community](https://effect.website/community-hub): meetups and events, or bring Effect to your own +- [Issues](https://github.com/Effect-TS/effect/issues): bug reports and feature requests +- [Jobs](https://effect.website/effect-jobs): companies hiring Effect developers +- Follow us on [X](https://x.com/EffectTS_), [Bluesky](https://bsky.app/profile/effect-ts.bsky.social), and [LinkedIn](https://www.linkedin.com/company/effect-ts) + +## Let's talk + +Whether your team is considering Effect, rolling it out, or already running it in production, we'd love to hear from you: what you're building, what works, and what you need from Effect next. + +- **Talk to the maintainers.** Introduce your team on [Discord](https://discord.gg/effect-ts) or email [contact@effectful.co](mailto:contact@effectful.co). We're happy to connect privately on Slack or Discord for feedback and help with adoption. +- **Production support.** We're exploring how to better support teams running Effect in production. If your organization has specific support needs, let's discuss them. +- **Adoption help.** Our [adoption partners](https://effect.website/adoption-partners) offer implementation, consulting, team extension, training, and commercial support. + +Feedback from teams using and evaluating Effect directly shapes what we stabilize and build next. + +## Long-term support + +Teams depend on Effect for systems they expect to run for years. Effect 4.x is a long-term support (LTS) release with the following guarantees: + +- At least three years of support, including bug and security fixes. +- Bug fixes for one year after the next major version is released. +- Security fixes for two years after the next major version is released. + +Stable APIs reserve breaking changes for major releases. APIs marked unstable may change in minor releases, and experimental APIs may change in patch releases. + +## Effect v3 + +The Effect v3 source code is available on the [`v3`](https://github.com/Effect-TS/effect/tree/v3) branch, which is also where issues and pull requests meant for Effect v3 should be targeted. To upgrade, see the [migration guide](MIGRATION.md). + +## Packages + +This monorepo contains the core `effect` package alongside integration packages that extend it. All packages listed below are released together with synchronized versions. + +| Package | Description | API Reference | +| --------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ | +| [`effect`](packages/effect) | The core package | [docs](https://effect.website/docs/v4/api/effect) | +| [`@effect/platform-browser`](packages/platform/browser) | Platform services for the browser | [docs](https://effect.website/docs/v4/api/platform-browser) | +| [`@effect/platform-bun`](packages/platform/bun) | Platform services for [Bun](https://bun.sh) | [docs](https://effect.website/docs/v4/api/platform-bun) | +| [`@effect/platform-deno`](packages/platform/deno) | Platform services for [Deno](https://deno.com) | [docs](https://effect.website/docs/v4/api/platform-deno) | +| [`@effect/platform-node`](packages/platform/node) | Platform services for [Node.js](https://nodejs.org) | [docs](https://effect.website/docs/v4/api/platform-node) | +| [`@effect/platform-node-shared`](packages/platform/node-shared) | Shared services for Node.js-compatible runtimes | [docs](https://effect.website/docs/v4/api/platform-node-shared) | +| [`@effect/sql-clickhouse`](packages/sql/clickhouse) | SQL client for [ClickHouse](https://clickhouse.com) | [docs](https://effect.website/docs/v4/api/sql-clickhouse) | +| [`@effect/sql-d1`](packages/sql/d1) | SQL client for Cloudflare D1 | [docs](https://effect.website/docs/v4/api/sql-d1) | +| [`@effect/sql-libsql`](packages/sql/libsql) | SQL client for libSQL | [docs](https://effect.website/docs/v4/api/sql-libsql) | +| [`@effect/sql-mssql`](packages/sql/mssql) | SQL client for Microsoft SQL Server | [docs](https://effect.website/docs/v4/api/sql-mssql) | +| [`@effect/sql-mysql2`](packages/sql/mysql2) | SQL client for MySQL | [docs](https://effect.website/docs/v4/api/sql-mysql2) | +| [`@effect/sql-pg`](packages/sql/pg) | SQL client for PostgreSQL | [docs](https://effect.website/docs/v4/api/sql-pg) | +| [`@effect/sql-pglite`](packages/sql/pglite) | SQL client for [PGlite](https://pglite.dev) | [docs](https://effect.website/docs/v4/api/sql-pglite) | +| [`@effect/sql-sqlite-bun`](packages/sql/sqlite-bun) | SQL client for SQLite via `bun:sqlite` | [docs](https://effect.website/docs/v4/api/sql-sqlite-bun) | +| [`@effect/sql-sqlite-do`](packages/sql/sqlite-do) | SQL client for Cloudflare Durable Objects SQLite | [docs](https://effect.website/docs/v4/api/sql-sqlite-do) | +| [`@effect/sql-sqlite-node`](packages/sql/sqlite-node) | SQL client for SQLite via `node:sqlite` | [docs](https://effect.website/docs/v4/api/sql-sqlite-node) | +| [`@effect/sql-sqlite-react-native`](packages/sql/sqlite-react-native) | SQL client for SQLite in React Native | [docs](https://effect.website/docs/v4/api/sql-sqlite-react-native) | +| [`@effect/sql-sqlite-wasm`](packages/sql/sqlite-wasm) | SQL client for SQLite compiled to WebAssembly | [docs](https://effect.website/docs/v4/api/sql-sqlite-wasm) | +| [`@effect/ai-anthropic`](packages/ai/anthropic) | Anthropic provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-anthropic) | +| [`@effect/ai-openai`](packages/ai/openai) | OpenAI provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-openai) | +| [`@effect/ai-cloudflare`](packages/ai/cloudflare) | Cloudflare decision provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-cloudflare) | +| [`@effect/ai-typesafe`](packages/ai/typesafe) | TypeSafe decision provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-typesafe) | +| [`@effect/ai-openai-compat`](packages/ai/openai-compat) | OpenAI-compatible API provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-openai-compat) | +| [`@effect/ai-openrouter`](packages/ai/openrouter) | OpenRouter provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-openrouter) | +| [`@effect/atom-react`](packages/atom/react) | React bindings for Effect Atom | [docs](https://effect.website/docs/v4/api/atom-react) | +| [`@effect/atom-solid`](packages/atom/solid) | SolidJS bindings for Effect Atom | [docs](https://effect.website/docs/v4/api/atom-solid) | +| [`@effect/atom-vue`](packages/atom/vue) | Vue bindings for Effect Atom | [docs](https://effect.website/docs/v4/api/atom-vue) | +| [`@effect/opentelemetry`](packages/opentelemetry) | [OpenTelemetry](https://opentelemetry.io) integration | [docs](https://effect.website/docs/v4/api/opentelemetry) | +| [`@effect/vitest`](packages/vitest) | Helpers for testing with [Vitest](https://vitest.dev) | [docs](https://effect.website/docs/v4/api/vitest) | +| [`@effect/docgen`](packages/tools/docgen) | Documentation generator for Effect projects | [docs](https://effect.website/docs/v4/api/docgen) | +| [`@effect/doctest`](packages/tools/doctest) | Runs JSDoc examples as Vitest tests | [docs](https://effect.website/docs/v4/api/doctest) | +| [`@effect/openapi-generator`](packages/tools/openapi-generator) | Generate Effect code from OpenAPI specifications | [docs](https://effect.website/docs/v4/api/openapi-generator) | +| [`@effect/graphql-generator`](packages/tools/graphql-generator) | Generate Effect code from GraphQL documents | [docs](https://effect.website/docs/v4/api/graphql-generator) | + +## License + +MIT diff --git a/repos/effect/ai-docs/README.md b/ai-docs/README.md similarity index 100% rename from repos/effect/ai-docs/README.md rename to ai-docs/README.md diff --git a/repos/effect/ai-docs/package.json b/ai-docs/package.json similarity index 97% rename from repos/effect/ai-docs/package.json rename to ai-docs/package.json index 13ef4319f..31975a9fb 100644 --- a/repos/effect/ai-docs/package.json +++ b/ai-docs/package.json @@ -28,7 +28,7 @@ "@effect/vitest": "workspace:*", "effect": "workspace:*", "hono": "^4.13.13", - "nodemailer": "^10.1.0" + "nodemailer": "^10.1.2" }, "devDependencies": { "@types/nodemailer": "^8.0.2" diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts b/ai-docs/src/01_effect/01_basics/01_effect-gen.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts rename to ai-docs/src/01_effect/01_basics/01_effect-gen.ts diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts b/ai-docs/src/01_effect/01_basics/02_effect-fn.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts rename to ai-docs/src/01_effect/01_basics/02_effect-fn.ts diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts b/ai-docs/src/01_effect/01_basics/10_creating-effects.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts rename to ai-docs/src/01_effect/01_basics/10_creating-effects.ts diff --git a/repos/effect/ai-docs/src/01_effect/01_basics/index.md b/ai-docs/src/01_effect/01_basics/index.md similarity index 100% rename from repos/effect/ai-docs/src/01_effect/01_basics/index.md rename to ai-docs/src/01_effect/01_basics/index.md diff --git a/repos/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts b/ai-docs/src/01_effect/02_schema/10_schema-basics.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts rename to ai-docs/src/01_effect/02_schema/10_schema-basics.ts diff --git a/repos/effect/ai-docs/src/01_effect/02_schema/index.md b/ai-docs/src/01_effect/02_schema/index.md similarity index 100% rename from repos/effect/ai-docs/src/01_effect/02_schema/index.md rename to ai-docs/src/01_effect/02_schema/index.md diff --git a/repos/effect/ai-docs/src/01_effect/03_services/01_service.ts b/ai-docs/src/01_effect/03_services/01_service.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/03_services/01_service.ts rename to ai-docs/src/01_effect/03_services/01_service.ts diff --git a/repos/effect/ai-docs/src/01_effect/03_services/10_reference.ts b/ai-docs/src/01_effect/03_services/10_reference.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/03_services/10_reference.ts rename to ai-docs/src/01_effect/03_services/10_reference.ts diff --git a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts b/ai-docs/src/01_effect/03_services/20_layer-composition.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts rename to ai-docs/src/01_effect/03_services/20_layer-composition.ts diff --git a/repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts b/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts rename to ai-docs/src/01_effect/03_services/20_layer-unwrap.ts diff --git a/repos/effect/ai-docs/src/01_effect/03_services/index.md b/ai-docs/src/01_effect/03_services/index.md similarity index 100% rename from repos/effect/ai-docs/src/01_effect/03_services/index.md rename to ai-docs/src/01_effect/03_services/index.md diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts b/ai-docs/src/01_effect/04_errors/01_error-handling.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts rename to ai-docs/src/01_effect/04_errors/01_error-handling.ts diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts b/ai-docs/src/01_effect/04_errors/10_catch-tags.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts rename to ai-docs/src/01_effect/04_errors/10_catch-tags.ts diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts b/ai-docs/src/01_effect/04_errors/20_reason-errors.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts rename to ai-docs/src/01_effect/04_errors/20_reason-errors.ts diff --git a/repos/effect/ai-docs/src/01_effect/04_errors/index.md b/ai-docs/src/01_effect/04_errors/index.md similarity index 100% rename from repos/effect/ai-docs/src/01_effect/04_errors/index.md rename to ai-docs/src/01_effect/04_errors/index.md diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts b/ai-docs/src/01_effect/05_resources/10_acquire-release.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts rename to ai-docs/src/01_effect/05_resources/10_acquire-release.ts diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts b/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts rename to ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts b/ai-docs/src/01_effect/05_resources/30_layer-map.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts rename to ai-docs/src/01_effect/05_resources/30_layer-map.ts diff --git a/repos/effect/ai-docs/src/01_effect/05_resources/index.md b/ai-docs/src/01_effect/05_resources/index.md similarity index 100% rename from repos/effect/ai-docs/src/01_effect/05_resources/index.md rename to ai-docs/src/01_effect/05_resources/index.md diff --git a/repos/effect/ai-docs/src/01_effect/06_running/10_run-main.ts b/ai-docs/src/01_effect/06_running/10_run-main.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/06_running/10_run-main.ts rename to ai-docs/src/01_effect/06_running/10_run-main.ts diff --git a/repos/effect/ai-docs/src/01_effect/06_running/20_layer-launch.ts b/ai-docs/src/01_effect/06_running/20_layer-launch.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/06_running/20_layer-launch.ts rename to ai-docs/src/01_effect/06_running/20_layer-launch.ts diff --git a/repos/effect/ai-docs/src/01_effect/06_running/index.md b/ai-docs/src/01_effect/06_running/index.md similarity index 100% rename from repos/effect/ai-docs/src/01_effect/06_running/index.md rename to ai-docs/src/01_effect/06_running/index.md diff --git a/repos/effect/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts b/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts similarity index 100% rename from repos/effect/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts rename to ai-docs/src/01_effect/07_pubsub/10_pubsub.ts diff --git a/repos/effect/ai-docs/src/01_effect/07_pubsub/index.md b/ai-docs/src/01_effect/07_pubsub/index.md similarity index 100% rename from repos/effect/ai-docs/src/01_effect/07_pubsub/index.md rename to ai-docs/src/01_effect/07_pubsub/index.md diff --git a/repos/effect/ai-docs/src/03_stream/10_creating-streams.ts b/ai-docs/src/03_stream/10_creating-streams.ts similarity index 100% rename from repos/effect/ai-docs/src/03_stream/10_creating-streams.ts rename to ai-docs/src/03_stream/10_creating-streams.ts diff --git a/repos/effect/ai-docs/src/03_stream/20_consuming-streams.ts b/ai-docs/src/03_stream/20_consuming-streams.ts similarity index 100% rename from repos/effect/ai-docs/src/03_stream/20_consuming-streams.ts rename to ai-docs/src/03_stream/20_consuming-streams.ts diff --git a/repos/effect/ai-docs/src/03_stream/30_encoding.ts b/ai-docs/src/03_stream/30_encoding.ts similarity index 100% rename from repos/effect/ai-docs/src/03_stream/30_encoding.ts rename to ai-docs/src/03_stream/30_encoding.ts diff --git a/repos/effect/ai-docs/src/03_stream/index.md b/ai-docs/src/03_stream/index.md similarity index 100% rename from repos/effect/ai-docs/src/03_stream/index.md rename to ai-docs/src/03_stream/index.md diff --git a/repos/effect/ai-docs/src/04_integration/10_managed-runtime.ts b/ai-docs/src/04_integration/10_managed-runtime.ts similarity index 100% rename from repos/effect/ai-docs/src/04_integration/10_managed-runtime.ts rename to ai-docs/src/04_integration/10_managed-runtime.ts diff --git a/repos/effect/ai-docs/src/04_integration/index.md b/ai-docs/src/04_integration/index.md similarity index 100% rename from repos/effect/ai-docs/src/04_integration/index.md rename to ai-docs/src/04_integration/index.md diff --git a/repos/effect/ai-docs/src/05_batching/10_request-resolver.ts b/ai-docs/src/05_batching/10_request-resolver.ts similarity index 100% rename from repos/effect/ai-docs/src/05_batching/10_request-resolver.ts rename to ai-docs/src/05_batching/10_request-resolver.ts diff --git a/repos/effect/ai-docs/src/05_batching/index.md b/ai-docs/src/05_batching/index.md similarity index 100% rename from repos/effect/ai-docs/src/05_batching/index.md rename to ai-docs/src/05_batching/index.md diff --git a/repos/effect/ai-docs/src/06_schedule/10_schedules.ts b/ai-docs/src/06_schedule/10_schedules.ts similarity index 92% rename from repos/effect/ai-docs/src/06_schedule/10_schedules.ts rename to ai-docs/src/06_schedule/10_schedules.ts index d42072add..dfbe74e55 100644 --- a/repos/effect/ai-docs/src/06_schedule/10_schedules.ts +++ b/ai-docs/src/06_schedule/10_schedules.ts @@ -5,14 +5,15 @@ */ import { Duration, Effect, Random, Schedule, Schema } from "effect" -// Production pattern: capped exponential backoff with jitter and max attempts. -// Delays start at 250ms, grow exponentially with jitter, and are capped at 10s. +// Production pattern: capped exponential backoff with jitter and at most 5 retries (6 attempts). +// Base delays start at 250ms and grow exponentially, capped at 10s before jitter. export const productionRetrySchedule = Schedule.min([ Schedule.exponential("250 millis"), // Cap the delay at 10 seconds to avoid excessively long waits. Schedule.spaced("10 seconds") ]).pipe( Schedule.jittered, + Schedule.upTo({ times: 5 }), Schedule.setInputType(), Schedule.while(({ input }) => input.retryable) ) @@ -44,8 +45,8 @@ export const fetchUserProfile = Effect.fn("fetchUserProfile")( // Use the schedule with `Effect.retry` to retry failures. export const loadUserWithRetry = fetchUserProfile("user-123").pipe( Effect.retry(productionRetrySchedule), - // If the effect still fails after exhausting the schedule, turn the error - // into a fatal one. + // Turn the final error into a defect, either after exhausting the retries + // or immediately for a non-retryable error. Effect.orDie ) diff --git a/repos/effect/ai-docs/src/06_schedule/index.md b/ai-docs/src/06_schedule/index.md similarity index 100% rename from repos/effect/ai-docs/src/06_schedule/index.md rename to ai-docs/src/06_schedule/index.md diff --git a/repos/effect/ai-docs/src/07_datetime/10_creating-and-formatting.ts b/ai-docs/src/07_datetime/10_creating-and-formatting.ts similarity index 100% rename from repos/effect/ai-docs/src/07_datetime/10_creating-and-formatting.ts rename to ai-docs/src/07_datetime/10_creating-and-formatting.ts diff --git a/repos/effect/ai-docs/src/07_datetime/20_time-zones.ts b/ai-docs/src/07_datetime/20_time-zones.ts similarity index 100% rename from repos/effect/ai-docs/src/07_datetime/20_time-zones.ts rename to ai-docs/src/07_datetime/20_time-zones.ts diff --git a/repos/effect/ai-docs/src/07_datetime/index.md b/ai-docs/src/07_datetime/index.md similarity index 100% rename from repos/effect/ai-docs/src/07_datetime/index.md rename to ai-docs/src/07_datetime/index.md diff --git a/repos/effect/ai-docs/src/08_observability/10_logging.ts b/ai-docs/src/08_observability/10_logging.ts similarity index 100% rename from repos/effect/ai-docs/src/08_observability/10_logging.ts rename to ai-docs/src/08_observability/10_logging.ts diff --git a/repos/effect/ai-docs/src/08_observability/20_otlp-tracing.ts b/ai-docs/src/08_observability/20_otlp-tracing.ts similarity index 100% rename from repos/effect/ai-docs/src/08_observability/20_otlp-tracing.ts rename to ai-docs/src/08_observability/20_otlp-tracing.ts diff --git a/repos/effect/ai-docs/src/08_observability/index.md b/ai-docs/src/08_observability/index.md similarity index 100% rename from repos/effect/ai-docs/src/08_observability/index.md rename to ai-docs/src/08_observability/index.md diff --git a/repos/effect/ai-docs/src/09_testing/10_effect-tests.ts b/ai-docs/src/09_testing/10_effect-tests.ts similarity index 100% rename from repos/effect/ai-docs/src/09_testing/10_effect-tests.ts rename to ai-docs/src/09_testing/10_effect-tests.ts diff --git a/repos/effect/ai-docs/src/09_testing/20_layer-tests.ts b/ai-docs/src/09_testing/20_layer-tests.ts similarity index 100% rename from repos/effect/ai-docs/src/09_testing/20_layer-tests.ts rename to ai-docs/src/09_testing/20_layer-tests.ts diff --git a/repos/effect/ai-docs/src/09_testing/index.md b/ai-docs/src/09_testing/index.md similarity index 100% rename from repos/effect/ai-docs/src/09_testing/index.md rename to ai-docs/src/09_testing/index.md diff --git a/repos/effect/ai-docs/src/10_predicate/01_basics.ts b/ai-docs/src/10_predicate/01_basics.ts similarity index 100% rename from repos/effect/ai-docs/src/10_predicate/01_basics.ts rename to ai-docs/src/10_predicate/01_basics.ts diff --git a/repos/effect/ai-docs/src/10_predicate/index.md b/ai-docs/src/10_predicate/index.md similarity index 100% rename from repos/effect/ai-docs/src/10_predicate/index.md rename to ai-docs/src/10_predicate/index.md diff --git a/repos/effect/ai-docs/src/40_sql/10_basics.ts b/ai-docs/src/40_sql/10_basics.ts similarity index 100% rename from repos/effect/ai-docs/src/40_sql/10_basics.ts rename to ai-docs/src/40_sql/10_basics.ts diff --git a/repos/effect/ai-docs/src/40_sql/index.md b/ai-docs/src/40_sql/index.md similarity index 100% rename from repos/effect/ai-docs/src/40_sql/index.md rename to ai-docs/src/40_sql/index.md diff --git a/repos/effect/ai-docs/src/50_http-client/10_basics.ts b/ai-docs/src/50_http-client/10_basics.ts similarity index 100% rename from repos/effect/ai-docs/src/50_http-client/10_basics.ts rename to ai-docs/src/50_http-client/10_basics.ts diff --git a/repos/effect/ai-docs/src/50_http-client/index.md b/ai-docs/src/50_http-client/index.md similarity index 100% rename from repos/effect/ai-docs/src/50_http-client/index.md rename to ai-docs/src/50_http-client/index.md diff --git a/repos/effect/ai-docs/src/51_http-server/10_basics.ts b/ai-docs/src/51_http-server/10_basics.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/10_basics.ts rename to ai-docs/src/51_http-server/10_basics.ts diff --git a/repos/effect/ai-docs/src/51_http-server/20_testing.ts b/ai-docs/src/51_http-server/20_testing.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/20_testing.ts rename to ai-docs/src/51_http-server/20_testing.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/Api.ts b/ai-docs/src/51_http-server/fixtures/api/Api.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/api/Api.ts rename to ai-docs/src/51_http-server/fixtures/api/Api.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts b/ai-docs/src/51_http-server/fixtures/api/Authorization.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts rename to ai-docs/src/51_http-server/fixtures/api/Authorization.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/System.ts b/ai-docs/src/51_http-server/fixtures/api/System.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/api/System.ts rename to ai-docs/src/51_http-server/fixtures/api/System.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/api/Users.ts b/ai-docs/src/51_http-server/fixtures/api/Users.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/api/Users.ts rename to ai-docs/src/51_http-server/fixtures/api/Users.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/domain/User.ts b/ai-docs/src/51_http-server/fixtures/domain/User.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/domain/User.ts rename to ai-docs/src/51_http-server/fixtures/domain/User.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts b/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts rename to ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/server/Authorization.ts b/ai-docs/src/51_http-server/fixtures/server/Authorization.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/server/Authorization.ts rename to ai-docs/src/51_http-server/fixtures/server/Authorization.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users.ts b/ai-docs/src/51_http-server/fixtures/server/Users.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/server/Users.ts rename to ai-docs/src/51_http-server/fixtures/server/Users.ts diff --git a/repos/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts b/ai-docs/src/51_http-server/fixtures/server/Users/http.ts similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts rename to ai-docs/src/51_http-server/fixtures/server/Users/http.ts diff --git a/repos/effect/ai-docs/src/51_http-server/index.md b/ai-docs/src/51_http-server/index.md similarity index 100% rename from repos/effect/ai-docs/src/51_http-server/index.md rename to ai-docs/src/51_http-server/index.md diff --git a/repos/effect/ai-docs/src/52_graphql/10_basics.ts b/ai-docs/src/52_graphql/10_basics.ts similarity index 100% rename from repos/effect/ai-docs/src/52_graphql/10_basics.ts rename to ai-docs/src/52_graphql/10_basics.ts diff --git a/repos/effect/ai-docs/src/52_graphql/20_testing.ts b/ai-docs/src/52_graphql/20_testing.ts similarity index 100% rename from repos/effect/ai-docs/src/52_graphql/20_testing.ts rename to ai-docs/src/52_graphql/20_testing.ts diff --git a/repos/effect/ai-docs/src/52_graphql/30_batching.ts b/ai-docs/src/52_graphql/30_batching.ts similarity index 100% rename from repos/effect/ai-docs/src/52_graphql/30_batching.ts rename to ai-docs/src/52_graphql/30_batching.ts diff --git a/repos/effect/ai-docs/src/52_graphql/40_aliased-batching.ts b/ai-docs/src/52_graphql/40_aliased-batching.ts similarity index 100% rename from repos/effect/ai-docs/src/52_graphql/40_aliased-batching.ts rename to ai-docs/src/52_graphql/40_aliased-batching.ts diff --git a/repos/effect/ai-docs/src/52_graphql/50_errors-and-retries.ts b/ai-docs/src/52_graphql/50_errors-and-retries.ts similarity index 100% rename from repos/effect/ai-docs/src/52_graphql/50_errors-and-retries.ts rename to ai-docs/src/52_graphql/50_errors-and-retries.ts diff --git a/repos/effect/ai-docs/src/52_graphql/index.md b/ai-docs/src/52_graphql/index.md similarity index 100% rename from repos/effect/ai-docs/src/52_graphql/index.md rename to ai-docs/src/52_graphql/index.md diff --git a/repos/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts b/ai-docs/src/60_child-process/10_working-with-child-processes.ts similarity index 100% rename from repos/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts rename to ai-docs/src/60_child-process/10_working-with-child-processes.ts diff --git a/repos/effect/ai-docs/src/60_child-process/index.md b/ai-docs/src/60_child-process/index.md similarity index 100% rename from repos/effect/ai-docs/src/60_child-process/index.md rename to ai-docs/src/60_child-process/index.md diff --git a/repos/effect/ai-docs/src/70_cli/10_basics.ts b/ai-docs/src/70_cli/10_basics.ts similarity index 100% rename from repos/effect/ai-docs/src/70_cli/10_basics.ts rename to ai-docs/src/70_cli/10_basics.ts diff --git a/repos/effect/ai-docs/src/70_cli/index.md b/ai-docs/src/70_cli/index.md similarity index 100% rename from repos/effect/ai-docs/src/70_cli/index.md rename to ai-docs/src/70_cli/index.md diff --git a/repos/effect/ai-docs/src/71_ai/10_language-model.ts b/ai-docs/src/71_ai/10_language-model.ts similarity index 100% rename from repos/effect/ai-docs/src/71_ai/10_language-model.ts rename to ai-docs/src/71_ai/10_language-model.ts diff --git a/repos/effect/ai-docs/src/71_ai/20_tools.ts b/ai-docs/src/71_ai/20_tools.ts similarity index 100% rename from repos/effect/ai-docs/src/71_ai/20_tools.ts rename to ai-docs/src/71_ai/20_tools.ts diff --git a/repos/effect/ai-docs/src/71_ai/30_chat.ts b/ai-docs/src/71_ai/30_chat.ts similarity index 100% rename from repos/effect/ai-docs/src/71_ai/30_chat.ts rename to ai-docs/src/71_ai/30_chat.ts diff --git a/repos/effect/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts b/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts similarity index 100% rename from repos/effect/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts rename to ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts diff --git a/repos/effect/ai-docs/src/71_ai/index.md b/ai-docs/src/71_ai/index.md similarity index 100% rename from repos/effect/ai-docs/src/71_ai/index.md rename to ai-docs/src/71_ai/index.md diff --git a/repos/effect/ai-docs/src/80_cluster/10_entities.ts b/ai-docs/src/80_cluster/10_entities.ts similarity index 100% rename from repos/effect/ai-docs/src/80_cluster/10_entities.ts rename to ai-docs/src/80_cluster/10_entities.ts diff --git a/repos/effect/ai-docs/src/80_cluster/index.md b/ai-docs/src/80_cluster/index.md similarity index 100% rename from repos/effect/ai-docs/src/80_cluster/index.md rename to ai-docs/src/80_cluster/index.md diff --git a/repos/effect/ai-docs/src/index.md b/ai-docs/src/index.md similarity index 100% rename from repos/effect/ai-docs/src/index.md rename to ai-docs/src/index.md diff --git a/repos/effect/ai-docs/tsconfig.json b/ai-docs/tsconfig.json similarity index 83% rename from repos/effect/ai-docs/tsconfig.json rename to ai-docs/tsconfig.json index 8aad2d936..d6ec9acb9 100644 --- a/repos/effect/ai-docs/tsconfig.json +++ b/ai-docs/tsconfig.json @@ -13,11 +13,13 @@ "plugins": [ { "name": "@effect/language-service", + "effectFn": ["untraced"], "namespaceImportPackages": [], "diagnosticSeverity": { "floatingEffect": "off", "preferSchemaOverJson": "off", - "unstableApiUsage": "off" + "unstableApiUsage": "off", + "effectFnOpportunity": "error" } } ] diff --git a/assets/dev/effect-app-apple-touch-180.png b/assets/dev/effect-app-apple-touch-180.png deleted file mode 100644 index 906d1621a05e8b5d53409bf02c9dd50b315b4681..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11603 zcmYkicQl*t|37ZV3c8Hg9cmL|)GB?^s#TQQBZ^Rp5_=^ms#di|lp3}7-a=42R_(ni z_TKv&@6Y+2^E)}oegAQt=kxJg*L~f2JVG_!C{a@~Q4$akP%FPye2c$U+ARyo%P*!}Q%UGIf=6B8fG;G?$lo021A_@CiDTC=AIM`sjFU@Jh)sWZI>)1gyAl->MiKU5sSM|z`@B3y{oUdTq(9wTc(UY~>W(-;U;SJAw65Pjb3YCaCK^f$NY;vs%CzQo)q6?(3Yr9jHh-U3 zQ}_(JyZMzuUPDj78}s5;=Ba`3X~?V zbLObcnbMQv8SgD`hw0tEH$R_HlX=~J@4|MnF5TuTq^a_YN@O;Vkz8}b`2#(cz^zU3 z4nDeuBtQ$xdXO!F%aS=DZTS6TBH3Rfn_iqB`}wV*VB+&`4tWi0TJn0NQOw3{jos0x zwBx@do24-mCcxqEAErevE^&fdE$=Wazw-D#>KHBpK z8icLba@@KE$dr_Ie;~l$hGkc$b5&_TsZ|=KN7qizD)TiqvIyV0r!ugjb1SFb#@BZ0 z-A!QIreN+M%O3>=jM$)p!2bs1CCFg@;Nb0oS8ihTuxJn9ukINR6lvPk|z`)f$#&o%@w6)y2k=MDm2yUPG9-D;qZS$xl*vUeeR%qzaQATNx~m_fHad zmTc&p3aUu4aZDDr`88~1=0`QjzepMKP{oi!UOUv{$_r z626SloL51f6^)!ZeR{Q^8CK>z4o!g42fHX%6Pke|dcEYTDEUGT!k7vqUv*3_`H-fS z|D1+}4P}5a#u)3N0t*9K!$bO0;AYIk-~5QLs<{f}+0C z2Ww+kD!CG>&%nf~F+QsSlJ3dM!5K(Xm%}VwEn{trnu&Q|nXHvYMG_INLcO$MJ;a*b zLqYXFWu{d@ZC|Z#2De@x)4_y#lD>-_|NZ>?*??Ey1iB1aOAdMU4C|$^ccjtV*mQ4% zp197+dn5bq@lHw$5zfC^EhCf?6#^`eTNA4j{AvqCnto=SB`&`qMz3l-HOg!`J)D+-@9NV+#pX;2__i|SGxgKnXO8n=?e z6$N)CfXL#cl19?Z=Z1J|?=VSel?brXR?mnQ846tpzvPi2PW{J}jg)?9W=;qvgg+YV z^ap+0gXJEf2}X;HqI3Birkr0Du795aoPk$(HmG3v@7F2A5^~e@$hBXdu)}U%LZnKy z4zP+pU(29%8}P%q*<|ySHsV#Y@&4?1On&+t*m^3l(kgy)2;mwbV^8o*%u}x%Ix6SA zRsd*bacsjiC-G)0gSk$`sBq_=xR2fkRko1_4g{_v(h@t?!?uY$vqY06U<=eN5mSRr z))b=Jky(G+w2{P6cAC5j5AB#9;9jut0YsH6jDpG5efE^@{afiTDV zUd^I<4pkFE`1COmF8toB2Uw0AaaGMBN))6Uv<2omY&tUseER_S>eNaEFKWKcd0+s@ z-zoS@c#rP?YNypg9t4taen!lgs5>r?;_RQl9mnR@P5Vel&XPL;73%)+18~Qo1S1St zriEdtMk6KDKMxW@IA*<8iEu0%cy#x%1ns-cN&I!lTBIZoaUcl84ecvyM0$$00CP{A zMTv2bu-s4wAodRN?UDqYiwx&OW?Y6S8?2fA0X_J>SOZd0se?QOg!#g%d)raj_=4PK zOGC;Y3=U*)egL-K;ej4ky`sRoGNQ)uPiY@3_V7U8OvMbyy@dh>4+d^ya|ECs-czuK zdEdV+7gDm)v7!|i`k}rWfHiN&gJn&B0OLri;)4o8*|u)&Hi$uKsD+??gTmeBfIXOG zz}&5wbOzW%CItrY4};>*TM5hIQ2azLuY3H!0H5RCSdQ|Ud&4C0g9LyP^awIb)|U-i zGfI%7!m5!lWo;9rj<6HKk;eT=_Kvf<$+Epj z5J~>zvLyqDCtyqnYjbQQf5qDCM`e>*-Q`Glr2X@fet@H}K3#f0e-ErMd!J7oS!yS)%4C_Tg75QyNf-nEop5nqTG z7Xim@{)qe*Ae0V#LR$W>T4Pt+Djs5qGEp=piFZf*lYf3Z;`s%$%_=*MXf=%42oTZa z#=N}Pd@bo3XdtqGy2kt1u?&U7@FVdPtsPlI@R_~7Cr~m-3)enx?)D7#Z_RjrOF@fG zn&EP$*<~|t|Bnf?Ox6Qla`}cIcAa2VoUY*$Cn@VOxyF29u!3mt8+siK3xk$?0ZbF7(SkZ2p!q%mN)1YVx0K9kPO5u$v|8ZCkI^2ak_K>$D#u7{7Wdsilgv z|Euk&P9vcfySCDATi-Z*1~ODAUj0e}-jTCr(YuofpRH(M+ z^}^Wr(W{VSY&W?frtIh^DBCbh!n_!))KF2=&~WbhM++OiwwqemQ4rRR33JvT`vuwUu?NbH5EKDs$vf*?tzGB)3D zpC)T_?zhHomee-V`j2J1pM4FEF}hMGt415YU3SeU=sB1Q{Je1biQWjI!f60Ej!i_l zwV>gg5A%6~Od~_29oF{UoTWdPuIRZKF@woPLuH3E?@oP zd*Qj_TgvS9;``k+EsI|N2Y#`s0M?qs%S8$&zu(L`ZiV$oY@HPgCG7~exFIdv^o9nF zhCVIA6m8KI7jC@2>ZZ|^Lx&xARF_-p(NYro{(9hui5dI^0$)G?l!NhFhx{$-O~zl% zTM`)h)Z>USWP(Yd+^0wGlfrjmM9}B;3$c%~1ZdzJjEOx>2of?u#uNiTN0F;kC!cd^ zrM;oz=Px_9*?@ORm^r_%OWtc*GUDAgyNRD|T*|Uy%=)x%)=S0sjzFb)B9uLmlydvL zUyNgZ5wMj^L#puWC;)_!@tcgN&&(guM^VcKzws!rdHwL&muH)D*|AZ^;(-S@FhQjh znS=zY4zQDLsB^x*IZsSli7H@;D}Ed?%Shun_B?4fZA~pncj1V7W^-a9Gfa=Nb+k9J zK_o+ZxSVTo@rbV3>g@Z`xNbt#W3CRF3U;%8Ia{K_rH{$J9{CK~e88z;uY3nZqX#gC ze9PThga>m7yBz0X`_vO0X1nGnI86WwO-d!{`*=BnR4Y(W#_{`R$Ve07b%3E zR})Tg3!jv9O+J-71Y?mO7*KKQ$uO`NIsd&q!pcH1KWltEF|}1a$^kD#VdBw zbPLyBr2V%M8`I`w)W-GtFlB#ZV}JAeQA{DFz0#XEgj)(G4g#f@3j)Y}!RC40*5xuS z6t#BEsUyu&&5!Jk1y+uMM_|u$#;59z%e5gA=>?y#caEw-=-5?0-w4X}VjrQcfB=S+ zsW0|13EtzDvd<3v#R)P|zN;kN0^&k4rENlefs@tEI|ms+{M**S=?6382T#SguwVfb z9%>gBSX%ob$l|m`u5V@6eLGNO$h1T#^~(Nk8{t6*q+cIW@>%N_AtvJot6zAQmMlWzW!3K;~J#617z3SS{>nC@Hqf z#EmaJD%1+c4oOhwJ1JR-5KQ`c3Q^-RO0O=zQgwKS(N1pto84NjQP?V|r|&WR@4)~O zR?_ED=2!KLp<@3>EKfqstV7BLe|CUUekKxma?6Kd{0~l=b9qfY7sQhCHdr@|l3gEh zU5G4dU0KBLyYa=`Gc~RvoZ_|1QE!=`RoF6bO9&2YUa$gBS<5ylSha;@V9wowV?ha2Tu?o=WW+~^$52E^(z9Qx z*Q52BGaRWe=ToaS7AhlHKbqD*=6-Td(`r<5)_azRy*4AL`mAzBh<^rYAD3`b#lvLV zO2vadXCJ7d#XBA;v-cxJ(>ypR=;~(p=y~&5UV;Kfu0SKZ?*7Rngx2KQkJhLuF4B*( z{`3sI7WT$^`ZtSiTk0__DL+*uVyXq6lPm_Y-w9JDjgu{nEHg1*5Y~PxUr&RzIRL0= zG6kN9SkN)a-tiR9&}vK1V|dGCzJmN5nvr+*k$ z0K}MAL|b)W&vr&#nX{jMHt)Wh^4Wf901>4udakFcHVingS#0FS@bb&JsZiRC1%aqe z%$q(~oUH~O*nlj8zKP$lxvLrcdkeMJ&~Pc{X1}w``(ul92;4EnNiG*~H#C> z%>Q%w_?YyriTfa8vE{IqX+1uJqnrlD z)u7CT6>kA?h5F?m^Mi+sZst!knP5$(P8x-mS6`7;nNeQCN77jt$aa5Hc)K=lqSI?; zUzFmF9cCO1IR`I^Ft!grcYXMcK$YD-0FNr0FkmE|5`IX?%SD2CqMbC8dXBt*2t|UM8jb{-H2)bE;r1_B1^n=c}ly7!Ze31~dD+F#6N`$CYx@=z~MwY1n6F8%CI< zeLspiG2NGnJsg(mJ=_i#L^umny@MS;)W)0)|NeByYj~VkNgHQh?$$P%+S%a$K4T4H ze=rmtvqv2)X)Km~Bkemn4qW>A_(xXMi$85gDX%Fucc@@yzmwI6tfA)I%nAwNd{Gwd ze<&E=y-HG(v$>b;w{4>~HGK83Xw3J&Iq=2Ve;S3wIsg>|#_kp!@vO4iBL)qp9mh%6 z*Y`m#r$$O>YnmOIr8|d{tf8x+Q-kcNhi4J7fY!~WO$C8P5lJ|(Eg`J!9&blu3p^dA({wz*0Dh|6d`|o8=|1Z54^HAe1{4K zVsR>E4Rj7m=$m^|YQ(?!HOOjwGXH&QOrz^}aMDK+M$$Yz0eRSDZ;1ySxCY z6c_4#t^Vb5C)j-N(3ekGbFbM}&Yb?yPKbjHw#(NcGP;-UZm37LzXxvD!}&OZKKC7~ z&}M)K)gc4yOAtFZ&}w~`N;Tz4_b%JBFRCt~w!N;eWv%51P%ldISVsUMpg~zCHdPWO zTNiiTwQZLY75dhPBYnKGzhrBITXYsZvpdPX<-uVq(JaQf`}rwCnSb-1EubGysaDHX z`ja{MP*!JwWA9hI(4CKtE8t9>U9w;NJ7h4zW|-%*`}^^;?cv&C+k5q5#jKPv@Ht+# zdOOLiGR;-4=y!45K%?I}Oud1UT9NS$S4Vv^`OJdeRqoMwdaN?#1S8vQMK=rD{NB#+ zD_~K1!{sz=`?BrvaNeHQ1_p2H>`nbD`RrAiXRRB*CzLa4`fR21E-@A4_lH-y?ZK@q zSV~O0-mxX)42$R0$c$5zF&FCUcx`pfQgy|_IPor5apS&J_4Ve9I_dfbk9Q|s1JFq0 z(7Tw0iZGhxKT*A^z5EyZf6)1-LjoE^!R}U!GT3jz_G2EX);Y>H)vbCFpkw4m<4RVF zr#>505X_mGT;xeg6LON|)6l|FN!LcAoAZzuZ5e#U0cZtHwV1ZyFR+>iD~(m9xa~DJ zoFtvqF3TQu#7-j)?$7F^G&ZLy7FYKDO}vinvlgYpy)J9|Yk!tfJuvjcgR-9D?UtLz zSx{lLvP3XJd9uM6mH1(KNJ`E6>o+$Q+iF?^Ba{xwiMGEt$E)IRuCG%(?58$RD*fBs zJI$EYA<4iHgJOHRqkyx9o9VMJ_o!_aWjq(R-(Bvtcc9l~sY*1j(}n(wjpF;|Tc?EX zPa?R7dzZv@!wGPPHoaf6U|aVe}}~@ zt&)y?>)H?{DL1!+L_0Rip5Q`4Y%tkdkL>%w0C)tg>9dp*-%F0etKrwoUhdNDO?GsM zngwBvrb1cKED8JZlq2^uwrM2H3r4}z7c!~&^kk-vrlVJ+On6aBP~&F7s+f>E#?FEq{Jiu0gG1Hd$);J@oI4!P3|q;+x*&m<7}u5$6v1Lyq$; z&%6PrT;~SigAe3rh4A?+?dZ47G;LGkE?=tnHUpu+j(XgK>j~TU&@!(PdbT<{-{7=} z=**Mi<&nxn;TTX$0bnfw?LJuhc4<>c2pa^NpPj2XC`KTp<*q}N*0zh5arxE5UMvo$ z%?btjRRHbS_i4fY26`wB$vG;>`@PKT{>RnDi;*`rEZA>H5p+yckS{05yyNie!oNlc z^!LhqcNn9w*+iKeSJdp~Z;3rez@7!}J&F)>S=z(kJeqk1o}i2*>m;(V{#1 zi~X3!K3u-*at%sE>_6#YGg@mda8%t8n?DLXtDs_V?&iXUT-?82Xg1Pyni68Bf>^3e ziLt=srtkRHO2h!XIwH}R7w++;2S-J~){V!k^>sBH=hhSD)sMfhKCHvYy?$I_H53f@+TQqaJt7Zlqtfi^cS|yA~ff-dyraSC6r; z&rC_5T{Q_>^|X&Q^oc!o2P{!ba-f8aIt@~~Le93$st)Gle1NL4!m$yV-TE=YI-ygJ z#x2xxzEzEM5uLsg4H(}hv&9j;5T=LNg&~O#AOdWPF&;&RIsHxwnS){5PO%*g_D({T z9WVA&l_p}ovB}gyfAK7H<1D*ardw5LyECVA-f~G*`n0W@&Ac~^S(w{%cH*lC&qMui z2m$J5{eVjH;JXiM=hRa=tmyz8$7{~yHlIdK>!2N5vvAH;vr?VEuB&Ipc78?aV0MYh z?mvc#KLW1^W$g%I*=70z4JZaLhW&C+W^2AC`i;t<2(cE~etueT^SDkUd9-`cnS?|G zk=yoU4}{jfxeN&FOF_O90aRj|bm`d8_S6z47ov_IrHqjq{*+_uh_d1YP}@F!QS0>^j;+%qzmMDf;oG*Q#v# zJqX_u4OAA5ql<64`72iFwFh%{5?Wp;8T{lU*?pjb)1129X>XIvU!7Z=S$LcSDzy{q zWRQ6Q-V;+&oZObYk4kZVeWG=;et%hrX}VQ-?{I>}Tj&bm%*;Z`zeegyoOl23s)>;1 zmYXn-7VkSSkU2N^kCi(TjL3?B$cYIZd|694pEr2SvKqA8>U+s`FIo%c zujkNMN5px?^XQ-C5c2i1)_4_&IRH%qO3g0MI|$t<DM?ah0siVxC@r(2nex0-c04UIZ} z$0%10g&N-tCO??4Pr8iugx>f2*-B7~5uJ}}GHL4_6KV#3wEg9vZ4S_kXFU%8XPqyH z4bZe5%y-38&ZdO-e1`rYB8vFLjPss3^cn@|w<4NrJsh{f^QgB^J;R2nv7gk7M6~o{ zIx14ZIc~aSWCv~bNlwiT;}W{R<>yLker;Uy(3JbX%&epfdf{%Zk->Zv#$>|6lIw+- z*_7}lLR|bffxw&p5E7!-GFg3YR1uavSzFC&Dw&ld#C5Mspx64$I~OHIP3Cu#{wz*- z?Cj_|wQ8`Z|COTGo;Y-oQV#;%8*=MKN}7h7Q9JVXC#HzUgt~t=LJ$xDFK@K~=K72= z;CG;&oX?27fDbXHX#1X=Rr~bt+)EiN{&W^B|LPjA94yNWY&UfEqhleRp~~sp6AdIl zL{pdVXOlhF`@G93HY+1TpH&8a7g|`n{coL*9oiE-Q(O0a|2*IOG`?8&PM|>u<5NK3 z9(G}t02fuU*I15gYkfFi9)dwPjetXl2XJ06v+&K2y`Lpx=FL^DQvU#%I&`YYa)Kw` z3g}nZq{gPq-V2<__}5!IX?~e=X|C@G_P$nbd1t{ci6G2I{ANHgRKsW7*}8%aav8CH zmw7vJObZlh&omkqmd8%7_1G+x+hNoQMD$pPROg`VbA~1g#wbJTyvFKJ*e?B-L=S&Iv4!-beaLIh6X(wdQD9)EW z^@p~|=Nr0ypB=QbnX|W)&z9k9;NlxBcgoC<9h7My>$6=#TU#Omi06~%3jYNo5I%|+ zV8D5cx$1ji7YJ0aa-4i26|DQFfP*5E_BrrD5^xVkuIqZ{@C5KQtR1i$IA@S4c|ALp zJ*ZhXyZ*k<$Vac>?)L0*wOelQRF)5YW-nB*-%iUNsA5N<=|7vqrhMdoEdi{qP>+=O zGP1-tgERk9iS(A#soxuBRf z{P$W9|J+F4g^q4ObUty4gsdi{yPmYsjy3Q-rHAcaM}GK>O zKO3a)u3Tbf$GVzVD~hEY)F|^1I@NtoqAD<$pL{h0RN#3~2icPc>@}*@ zYnAqh@e49d4MXEL*XRMde^N;BsmE0{K=>sK?B(deByr#bqDZq}YVFP&Z=2@BeUBRN z9q=GkFNe?HCnh(V7E4e-$twmdX!$k2dtK6tAdsoM$1G^4I^Wn=`NWy$#`3K>TR@>d zhK25if=Rn6SBD%B*>s-lCo-vH<+yA86dJis+rwUC9f|Gmu;=hdmaBp(Tv!LxqbH6u z;%Dmx0)s$`AuOa=Wmc&Q%|-KkpZ<^SJ$IK2w<6;p9;hpgY@UWL=dnPhegq#yRckfN z2s zpZN5>$3jSSj=i%iC66?K3_eWLq!XG+K?1gJTlbAkw(ik14~A-@G;f{;OWK+h(I{{C z220MQDFjVu?4|;QUeRhKb2J{EkampR$MI8`)7*dzzzRS-1uKeOYj8rrKXwlNijE7G zOqOm)C%@+-E#2VtxwfouyC<^I{LS#I^S*dJujEKC{1J66=)c||$>9XOt#*uY-gvHW zV4&pGY(wE&Ri2?_AJouM=l0=jHm+O1>i5=k7>)(Rg3Ww>c$_TmfqK#KK{|K{V$Ce& zEDNtfdm4EjAF{*bI6;%c8(GT9qO*`OW*A99a^E@_Q*ggd+WX$b1}&ZhKhUm|_5x7A z*}X&#K7ZIVn!`&}xPP&YC7OiIzF=m-HqI7deJ1E>Sz(0X<|kP7V9D5YW$=o@z%x|& z5ECtYu1b^=)_LBpLI_tw5FV`hgTBw{f1t!p(D6e1%DI{@@p=cbiCZERFYovd^>8CJ z_El@rDqdP5w)sN>#;GE#nZu~d3XxgA(aSs9sArF8vIW}ml9QeWQkMGj^IamS66F+X_ZiS)v0dH zB^BP1)rCmUQ*-)DM?4PejV|nuH+)bw5SW$P7K!uxb}8~LdyQ3p|W z0PG##L97t~m+=66@(y35XaI!d{|N1Lb{N4@A@Tq6mVsyWl?1VOu+ElN+_wz23I4Ax z@+T>Qzmbx^bTKmf=72hCs0JbYEhTD*=P?@$IRoh2UX8qs+NC=O2@)OHqyggdb@~o7 zt(Sb42=cokWEM}h-=eev6My+At~lKNf2+9zL4=k;Y0#H${-oHwULsthGqn&F`93Hu z#xPniYY*%V+E+`*bNRK^J`2S*hy=cH!@{a3D)=FAb|-|J)xILZ&ND@xg}d&@&)UnB zop!m6#ReV#3nj4WCjzN~8;8}CM)ExZRx?Vzx&;n+-30Ey+V6NNju1MIYwZoF)whYs zmsDcfN!Xx(S6TOcKgaj&WP9s8`p41tD!E^z>v5LWqxWIHIoEg+gs>Mul`}eZS&fV{ z>JdgyRIZ{SWW?py^VSD*MC%({>#OTSvtAe=(bpkIgNq4iYUHhg?T#CaUDpf+cNJyC zMQYZAprCT zQqFYe%B~@_7ji_Y3~9zOKGpavdOm0l56DkXsg966lEq8aBujFUl1&8-#-!NZ5YYI5 z$@-<0=tIEat%6NAXNLiNV(LeD@C6C_RNQEHL^m_|;Bi9RWcdacQL!Zxz7lk_2Gf`ddc>>c~g-s&RsVPrUQnLpqY%Zf8p2`|Dq_!0n`)r>q^4kt#M zB261`|6B#PN%y`};q@vZbTF;28gpf9-;Y3KglGcl5|5D!N2Pz0YT8tI)yv6Gp>ba5 zw%+q!i_5yk@Se&3nItDo4m1%p*!0N9a)N-=^ky++6Mji2lUqty7x9&v zTd_}wvC!S&sP3hwjW?05@#ttLXv%K@TvJCd$^9h6IDW(5*JtSR=3n!soakh%hmB~0 z(3PDHxPx8So(;Pf@-C4K&gr3F38}vXG#h`ge}OEDj5Iyq-taTSNx z>*es9`mPHUJyZH!Q@M)`vZg^$r<%6(=D}FSbx>(EY*M@_GOLI_&Bg22&^M0Ycp{Ai zL;ja35fBnoWk_6S?8Bx4ua*MYia+a9KonndYzbdn@gUgR3{QK6^}JvFF@(%O4}QiL`kF4mLV4{CbfU;G!&<|3i!@osF5FYlIqU&6b&?drqAa6C)J~J!KyC^cB(xqxiX0KG*cu zHZ5g691s-oH9-{=*doJv6rR9WcrK9otLufiP{%Pbjtxp%(n{yX$7vK(@)r0(2}>7j q@f?0UCRp)5{cjg{M%w-jVO-tjJ-f<7AYLU+p#1WUV$pxlfd2 error)" note: "Use Cause.map with a constant function; only Fail errors change and Die/Interrupt reasons remain." effect/Cause#Cause: + include: true replacement: "Cause.Cause" - note: "The name remains, but v4 Cause is a wrapper with readonly reasons: ReadonlyArray>, not the v3 Empty/Fail/Die/Interrupt/Sequential/Parallel tree." + note: "V4 Cause wraps readonly reasons: ReadonlyArray>, replacing the v3 cause tree. Finalizer and concurrent failure aggregation can convert typed errors to defects when a defect is present; Cause.combine itself does not perform this conversion." effect/Cause#Cause.Variance: replacement: "none" note: "The public variance helper was removed. Cause.Cause is directly branded by Cause.TypeId; application code should not reproduce the old variance member." diff --git a/repos/effect/migration/annotations/effect__Channel.yaml b/migration/annotations/effect__Channel.yaml similarity index 92% rename from repos/effect/migration/annotations/effect__Channel.yaml rename to migration/annotations/effect__Channel.yaml index 06cbc912b..cebd43127 100644 --- a/repos/effect/migration/annotations/effect__Channel.yaml +++ b/migration/annotations/effect__Channel.yaml @@ -12,7 +12,7 @@ note: "The inferred Channel.fromChunk match is not equivalent. Rebuild the buffered upstream-pull transform with Channel.fromTransform and Channel.toTransform." "effect/Channel#catchAll": replacement: "Channel.catch" - note: "Renamed to catch for typed-error recovery." + note: "Rename to Channel.catch. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. Use Channel.catchCause for full-cause handling." "effect/Channel#catchAllCause": replacement: "Channel.catchCause" note: "Renamed to catchCause for full-cause recovery." @@ -146,11 +146,11 @@ replacement: "Channel.merge" note: "Use Channel.merge with haltStrategy left, right, both, or either for standard policies. Custom MergeDecision effects require a Pull-level redesign." "effect/Channel#orDieWith": - replacement: "Channel.catch" - note: "Use Channel.catch(self, (error) => Channel.die(f(error))); v4 Channel.orDie has no mapping callback." + replacement: "Channel.mapError + Channel.orDie" + note: "Use self.pipe(Channel.mapError(f), Channel.orDie). Mapping transforms every typed error while preserving existing defects and interruptions; Channel.catch skips the mapper for causes containing defects." "effect/Channel#orElse": replacement: "Channel.catch" - note: "Use Channel.catch(self, () => that()) and keep the fallback lazy." + note: "Use Channel.catch(self, () => that()) and keep the fallback lazy. Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery." "effect/Channel#provideLayer": replacement: "Channel.provide" note: "Both collapse into provide. V4 removes services supplied by the layer and retains remaining requirements; use options.local when a fresh layer instance is needed." @@ -217,3 +217,12 @@ "effect/Channel#zipRight": replacement: "Channel.concat" note: "Use concat for the sequential form; it preserves the right done value. Concurrent mode has no exact replacement." +effect/Channel#mapError: + include: true + replacement: "Channel.mapError" + note: "Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." + +effect/Channel#orDie: + include: true + replacement: "Channel.orDie" + note: "Converts typed errors to defects. If a defect is already present, every typed error is converted to a defect, preserving reason order and interruptions." diff --git a/repos/effect/migration/annotations/effect__ChildExecutorDecision.yaml b/migration/annotations/effect__ChildExecutorDecision.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ChildExecutorDecision.yaml rename to migration/annotations/effect__ChildExecutorDecision.yaml diff --git a/repos/effect/migration/annotations/effect__Chunk.yaml b/migration/annotations/effect__Chunk.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Chunk.yaml rename to migration/annotations/effect__Chunk.yaml diff --git a/repos/effect/migration/annotations/effect__Clock.yaml b/migration/annotations/effect__Clock.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Clock.yaml rename to migration/annotations/effect__Clock.yaml diff --git a/repos/effect/migration/annotations/effect__Config.yaml b/migration/annotations/effect__Config.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Config.yaml rename to migration/annotations/effect__Config.yaml diff --git a/repos/effect/migration/annotations/effect__ConfigError.yaml b/migration/annotations/effect__ConfigError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ConfigError.yaml rename to migration/annotations/effect__ConfigError.yaml diff --git a/repos/effect/migration/annotations/effect__ConfigProvider.yaml b/migration/annotations/effect__ConfigProvider.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ConfigProvider.yaml rename to migration/annotations/effect__ConfigProvider.yaml diff --git a/repos/effect/migration/annotations/effect__ConfigProviderPathPatch.yaml b/migration/annotations/effect__ConfigProviderPathPatch.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ConfigProviderPathPatch.yaml rename to migration/annotations/effect__ConfigProviderPathPatch.yaml diff --git a/repos/effect/migration/annotations/effect__Console.yaml b/migration/annotations/effect__Console.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Console.yaml rename to migration/annotations/effect__Console.yaml diff --git a/repos/effect/migration/annotations/effect__Context.yaml b/migration/annotations/effect__Context.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Context.yaml rename to migration/annotations/effect__Context.yaml diff --git a/repos/effect/migration/annotations/effect__Cron.yaml b/migration/annotations/effect__Cron.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Cron.yaml rename to migration/annotations/effect__Cron.yaml diff --git a/repos/effect/migration/annotations/effect__Data.yaml b/migration/annotations/effect__Data.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Data.yaml rename to migration/annotations/effect__Data.yaml diff --git a/repos/effect/migration/annotations/effect__DateTime.yaml b/migration/annotations/effect__DateTime.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__DateTime.yaml rename to migration/annotations/effect__DateTime.yaml diff --git a/repos/effect/migration/annotations/effect__DefaultServices.yaml b/migration/annotations/effect__DefaultServices.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__DefaultServices.yaml rename to migration/annotations/effect__DefaultServices.yaml diff --git a/repos/effect/migration/annotations/effect__Deferred.yaml b/migration/annotations/effect__Deferred.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Deferred.yaml rename to migration/annotations/effect__Deferred.yaml diff --git a/repos/effect/migration/annotations/effect__Differ.yaml b/migration/annotations/effect__Differ.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Differ.yaml rename to migration/annotations/effect__Differ.yaml diff --git a/repos/effect/migration/annotations/effect__Duration.yaml b/migration/annotations/effect__Duration.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Duration.yaml rename to migration/annotations/effect__Duration.yaml diff --git a/repos/effect/migration/annotations/effect__Effect.yaml b/migration/annotations/effect__Effect.yaml similarity index 89% rename from repos/effect/migration/annotations/effect__Effect.yaml rename to migration/annotations/effect__Effect.yaml index 019969d24..c59624991 100644 --- a/repos/effect/migration/annotations/effect__Effect.yaml +++ b/migration/annotations/effect__Effect.yaml @@ -51,11 +51,12 @@ effect/Effect#cacheRequestResult: replacement: "none" note: "Direct request-cache mutation was removed; configure request resolution through `RequestResolver`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." effect/Effect#catch: + include: true replacement: "Effect.catch" - note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." + note: "Typed recovery skips causes containing defects and converts their typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery; use Effect.catchCause for full-cause handling." effect/Effect#catchAll: replacement: "Effect.catch" - note: "Use the shortened v4 error-handler name. Adapt arguments and imports to the v4 API." + note: "Rename to Effect.catch. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. Use Effect.catchCause for full-cause handling." effect/Effect#catchAllCause: replacement: "Effect.catchCause" note: "Use the shortened v4 cause-handler name. Adapt arguments and imports to the v4 API." @@ -64,7 +65,7 @@ effect/Effect#catchAllDefect: note: "Use the shortened v4 defect-handler name. Adapt arguments and imports to the v4 API." effect/Effect#catchSome: replacement: "Effect.catchFilter" - note: "Replace the Option-returning partial function with a `Filter` and handler. Adapt arguments and imports to the v4 API." + note: "Replace the Option-returning partial function with a Filter and handler. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery." effect/Effect#catchSomeCause: replacement: "Effect.catchCauseFilter" note: "Replace the Option-returning partial function with a cause `Filter` and handler. Adapt arguments and imports to the v4 API." @@ -151,7 +152,7 @@ effect/Effect#EffectUnifyIgnore: note: "The internal unification-ignore helper is no longer public; rely on v4 Effect inference. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." effect/Effect#either: replacement: "Effect.result" - note: "V4 represents typed success/failure as `Result` instead of `Either`. Adapt arguments and imports to the v4 API." + note: "V4 represents typed success/failure as `Result` instead of `Either`. Adapt arguments and imports to the v4 API. Causes containing defects are not captured as Result failures and propagate with typed errors converted to defects; interruptions alongside typed errors do not block typed recovery." effect/Effect#ensureErrorType: replacement: "Effect.satisfiesErrorType" note: "Use the renamed compile-time channel constraint. Adapt arguments and imports to the v4 API." @@ -244,7 +245,7 @@ effect/Effect#if: note: "Select the branch lazily with a JavaScript conditional inside `Effect.suspend`. Adapt arguments and imports to the v4 API." effect/Effect#ignoreLogged: replacement: "Effect.ignore" - note: "Pass `{ log: true }` to the consolidated ignore combinator. Adapt arguments and imports to the v4 API." + note: "Use Effect.ignore(self, { log: true }). Causes containing defects are not ignored and propagate with typed errors converted to defects; interruptions alongside typed errors do not block typed recovery." effect/Effect#inheritFiberRefs: replacement: "none" note: "Bulk FiberRef inheritance was removed; propagate required context references explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." @@ -337,7 +338,7 @@ effect/Effect#orDieWith: note: "Map the typed error to the desired defect, then convert failures to defects. Adapt arguments and imports to the v4 API." effect/Effect#orElse: replacement: "Effect.catch" - note: "Ignore the caught error and evaluate the fallback Effect. Adapt arguments and imports to the v4 API." + note: "Use Effect.catch(self, () => that()). Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery." effect/Effect#orElseFail: replacement: "Effect.mapError" note: "Replace every typed error with the lazily produced failure value. Adapt arguments and imports to the v4 API." @@ -662,8 +663,78 @@ 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": + include: true 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." + 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. Causes containing defects from either the source or policy skip the fallback and convert typed errors to defects. Interruptions alongside typed errors do not block the fallback." "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." +effect/Effect#catchTag: + include: true + replacement: "Effect.catchTag" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Effect#catchTags: + include: true + replacement: "Effect.catchTags" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Effect#catchIf: + include: true + replacement: "Effect.catchIf" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Effect#match: + include: true + replacement: "Effect.match" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Effect#matchEffect: + include: true + replacement: "Effect.matchEffect" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Effect#orElseSucceed: + include: true + replacement: "Effect.orElseSucceed" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Effect#option: + include: true + replacement: "Effect.option" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Effect#ignore: + include: true + replacement: "Effect.ignore" + note: "Ignores recoverable typed errors only. Causes containing defects propagate with typed errors converted to defects; interruptions alongside typed errors do not prevent ignoring them." + +effect/Effect#mapError: + include: true + replacement: "Effect.mapError" + note: "Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." + +effect/Effect#mapBoth: + include: true + replacement: "Effect.mapBoth" + note: "Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." + +effect/Effect#orDie: + include: true + replacement: "Effect.orDie" + note: "Converts typed errors to defects. If a defect is already present, every typed error is converted to a defect, preserving reason order and interruptions." + +effect/Effect#retry: + include: true + replacement: "Effect.retry" + note: "Retries typed source errors, including those alongside interruptions. A source cause containing a defect is not retried and propagates with typed errors converted to defects." + +effect/Effect#retryOrElse: + include: true + replacement: "Effect.retryOrElse" + note: "A source cause containing a defect skips retries and the fallback, converting typed errors to defects. Interruptions alongside typed errors do not block retries. Policy failures propagate unchanged." + +effect/Effect#repeat: + include: true + replacement: "Effect.repeat" + note: "If the source or policy fails with a cause containing defects, typed errors are converted to defects. An interruption alongside a typed error does not block the typed failure path." diff --git a/repos/effect/migration/annotations/effect__Effectable.yaml b/migration/annotations/effect__Effectable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Effectable.yaml rename to migration/annotations/effect__Effectable.yaml diff --git a/repos/effect/migration/annotations/effect__Either.yaml b/migration/annotations/effect__Either.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Either.yaml rename to migration/annotations/effect__Either.yaml diff --git a/repos/effect/migration/annotations/effect__Encoding.yaml b/migration/annotations/effect__Encoding.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Encoding.yaml rename to migration/annotations/effect__Encoding.yaml diff --git a/repos/effect/migration/annotations/effect__Equal.yaml b/migration/annotations/effect__Equal.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Equal.yaml rename to migration/annotations/effect__Equal.yaml diff --git a/repos/effect/migration/annotations/effect__Equivalence.yaml b/migration/annotations/effect__Equivalence.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Equivalence.yaml rename to migration/annotations/effect__Equivalence.yaml diff --git a/repos/effect/migration/annotations/effect__ExecutionPlan.yaml b/migration/annotations/effect__ExecutionPlan.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ExecutionPlan.yaml rename to migration/annotations/effect__ExecutionPlan.yaml diff --git a/repos/effect/migration/annotations/effect__ExecutionStrategy.yaml b/migration/annotations/effect__ExecutionStrategy.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ExecutionStrategy.yaml rename to migration/annotations/effect__ExecutionStrategy.yaml diff --git a/repos/effect/migration/annotations/effect__Exit.yaml b/migration/annotations/effect__Exit.yaml similarity index 93% rename from repos/effect/migration/annotations/effect__Exit.yaml rename to migration/annotations/effect__Exit.yaml index 5644d4368..4d352b33f 100644 --- a/repos/effect/migration/annotations/effect__Exit.yaml +++ b/migration/annotations/effect__Exit.yaml @@ -70,3 +70,12 @@ effect/Exit#zipRight: effect/Exit#zipWith: replacement: "Exit.match" note: "No direct v4 equivalent. Nested-match both Exits: preserve a lone failure cause, call options.onFailure and Exit.failCause only when both fail, and call Exit.succeed(options.onSuccess(a, b)) when both succeed." +effect/Exit#mapError: + include: true + replacement: "Exit.mapError" + note: "Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." + +effect/Exit#mapBoth: + include: true + replacement: "Exit.mapBoth" + note: "Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." diff --git a/repos/effect/migration/annotations/effect__FastCheck.yaml b/migration/annotations/effect__FastCheck.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FastCheck.yaml rename to migration/annotations/effect__FastCheck.yaml diff --git a/repos/effect/migration/annotations/effect__Fiber.yaml b/migration/annotations/effect__Fiber.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Fiber.yaml rename to migration/annotations/effect__Fiber.yaml diff --git a/repos/effect/migration/annotations/effect__FiberHandle.yaml b/migration/annotations/effect__FiberHandle.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberHandle.yaml rename to migration/annotations/effect__FiberHandle.yaml diff --git a/repos/effect/migration/annotations/effect__FiberId.yaml b/migration/annotations/effect__FiberId.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberId.yaml rename to migration/annotations/effect__FiberId.yaml diff --git a/repos/effect/migration/annotations/effect__FiberMap.yaml b/migration/annotations/effect__FiberMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberMap.yaml rename to migration/annotations/effect__FiberMap.yaml diff --git a/repos/effect/migration/annotations/effect__FiberRef.yaml b/migration/annotations/effect__FiberRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberRef.yaml rename to migration/annotations/effect__FiberRef.yaml diff --git a/repos/effect/migration/annotations/effect__FiberRefs.yaml b/migration/annotations/effect__FiberRefs.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberRefs.yaml rename to migration/annotations/effect__FiberRefs.yaml diff --git a/repos/effect/migration/annotations/effect__FiberRefsPatch.yaml b/migration/annotations/effect__FiberRefsPatch.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberRefsPatch.yaml rename to migration/annotations/effect__FiberRefsPatch.yaml diff --git a/repos/effect/migration/annotations/effect__FiberSet.yaml b/migration/annotations/effect__FiberSet.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberSet.yaml rename to migration/annotations/effect__FiberSet.yaml diff --git a/repos/effect/migration/annotations/effect__FiberStatus.yaml b/migration/annotations/effect__FiberStatus.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__FiberStatus.yaml rename to migration/annotations/effect__FiberStatus.yaml diff --git a/repos/effect/migration/annotations/effect__Function.yaml b/migration/annotations/effect__Function.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Function.yaml rename to migration/annotations/effect__Function.yaml diff --git a/repos/effect/migration/annotations/effect__GlobalValue.yaml b/migration/annotations/effect__GlobalValue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__GlobalValue.yaml rename to migration/annotations/effect__GlobalValue.yaml diff --git a/repos/effect/migration/annotations/effect__Graph.yaml b/migration/annotations/effect__Graph.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Graph.yaml rename to migration/annotations/effect__Graph.yaml diff --git a/repos/effect/migration/annotations/effect__GroupBy.yaml b/migration/annotations/effect__GroupBy.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__GroupBy.yaml rename to migration/annotations/effect__GroupBy.yaml diff --git a/repos/effect/migration/annotations/effect__Hash.yaml b/migration/annotations/effect__Hash.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Hash.yaml rename to migration/annotations/effect__Hash.yaml diff --git a/repos/effect/migration/annotations/effect__HashMap.yaml b/migration/annotations/effect__HashMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__HashMap.yaml rename to migration/annotations/effect__HashMap.yaml diff --git a/repos/effect/migration/annotations/effect__HashSet.yaml b/migration/annotations/effect__HashSet.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__HashSet.yaml rename to migration/annotations/effect__HashSet.yaml diff --git a/repos/effect/migration/annotations/effect__Inspectable.yaml b/migration/annotations/effect__Inspectable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Inspectable.yaml rename to migration/annotations/effect__Inspectable.yaml diff --git a/repos/effect/migration/annotations/effect__Iterable.yaml b/migration/annotations/effect__Iterable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Iterable.yaml rename to migration/annotations/effect__Iterable.yaml diff --git a/repos/effect/migration/annotations/effect__JSONSchema.yaml b/migration/annotations/effect__JSONSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__JSONSchema.yaml rename to migration/annotations/effect__JSONSchema.yaml diff --git a/repos/effect/migration/annotations/effect__KeyedPool.yaml b/migration/annotations/effect__KeyedPool.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__KeyedPool.yaml rename to migration/annotations/effect__KeyedPool.yaml diff --git a/repos/effect/migration/annotations/effect__Layer.yaml b/migration/annotations/effect__Layer.yaml similarity index 97% rename from repos/effect/migration/annotations/effect__Layer.yaml rename to migration/annotations/effect__Layer.yaml index 925c52e31..08314f063 100644 --- a/repos/effect/migration/annotations/effect__Layer.yaml +++ b/migration/annotations/effect__Layer.yaml @@ -95,8 +95,8 @@ replacement: "Layer.fromBuild with Effect.matchCauseEffect over Layer.buildWithMemoMap" note: "Fold the source acquisition cause, then build the selected failure or success layer." "effect/Layer#memoize": - replacement: "automatic shared memoization under Effect.provide" - note: "Reuse the same Layer value; use { local: true } or Layer.fresh to opt out, or MemoMap APIs for manual control." + replacement: "enclosing Effect.provide or explicit MemoMap APIs" + note: "A layer already built by an enclosing provide or runtime is reused; sibling provide calls build their own copies. Use { local: true } or Layer.fresh for fresh builds, or MemoMap APIs for manual sharing." "effect/Layer#MemoMap": replacement: "Layer.MemoMap" note: "The interface remains and now supports parent-child ambient maps." diff --git a/repos/effect/migration/annotations/effect__LayerMap.yaml b/migration/annotations/effect__LayerMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__LayerMap.yaml rename to migration/annotations/effect__LayerMap.yaml diff --git a/repos/effect/migration/annotations/effect__List.yaml b/migration/annotations/effect__List.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__List.yaml rename to migration/annotations/effect__List.yaml diff --git a/repos/effect/migration/annotations/effect__LogLevel.yaml b/migration/annotations/effect__LogLevel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__LogLevel.yaml rename to migration/annotations/effect__LogLevel.yaml diff --git a/repos/effect/migration/annotations/effect__LogSpan.yaml b/migration/annotations/effect__LogSpan.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__LogSpan.yaml rename to migration/annotations/effect__LogSpan.yaml diff --git a/repos/effect/migration/annotations/effect__Logger.yaml b/migration/annotations/effect__Logger.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Logger.yaml rename to migration/annotations/effect__Logger.yaml diff --git a/repos/effect/migration/annotations/effect__Mailbox.yaml b/migration/annotations/effect__Mailbox.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Mailbox.yaml rename to migration/annotations/effect__Mailbox.yaml diff --git a/repos/effect/migration/annotations/effect__ManagedRuntime.yaml b/migration/annotations/effect__ManagedRuntime.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ManagedRuntime.yaml rename to migration/annotations/effect__ManagedRuntime.yaml diff --git a/repos/effect/migration/annotations/effect__Match.yaml b/migration/annotations/effect__Match.yaml similarity index 67% rename from repos/effect/migration/annotations/effect__Match.yaml rename to migration/annotations/effect__Match.yaml index 2ed399401..da050e153 100644 --- a/repos/effect/migration/annotations/effect__Match.yaml +++ b/migration/annotations/effect__Match.yaml @@ -10,6 +10,10 @@ "effect/Match#Not": replacement: "Match.Not" note: "The case type is retained. Its evaluate method now receives any Match.fn selector arguments after the selected input; update custom case implementations that consume those arguments." +"effect/Match#not": + include: true + replacement: "Match.not" + note: "Object patterns now reject top-level keys absent from every variant of the remaining input type. Correct stale property names or narrow the input before matching; function, refinement, and array patterns are not subject to this object-key check." "effect/Match#SafeRefinementId": replacement: "none" note: "The public safe-refinement brand was internalized. Use Predicate.Refinement, Predicate.Predicate, or a built-in Match refinement instead of constructing the brand." @@ -21,7 +25,7 @@ note: "The public type-level namespace is retained with no call-site migration." "effect/Match#Types.ExtractAndNarrow": replacement: "Match.Types.ExtractAndNarrow" - note: "The type-only matching helper is retained unchanged." + note: "Array prefix patterns now narrow the required positions and preserve the unmatched tail of arrays and variadic tuples. Update explicit handler or helper result types that assumed a prefix pattern described the entire array; runtime prefix matching is unchanged." "effect/Match#Types.MaybeReplace": replacement: "Match.Types.MaybeReplace" note: "The type-only matching helper is retained unchanged." @@ -49,3 +53,15 @@ "effect/Match#When": replacement: "Match.When" note: "The case type is retained. Its evaluate method now receives any Match.fn selector arguments after the selected input; update custom case implementations that consume those arguments." +"effect/Match#when": + include: true + replacement: "Match.when" + note: "Object patterns now reject top-level keys absent from every variant of the remaining input type. Correct stale property names or narrow the input before matching. Array prefix patterns retain the unmatched tail in the handler type rather than treating the pattern as the entire array." +"effect/Match#whenAnd": + include: true + replacement: "Match.whenAnd" + note: "Each object pattern now rejects top-level keys absent from every variant of the remaining input type, including patterns spread from an array. Correct stale property names or narrow the input before matching." +"effect/Match#whenOr": + include: true + replacement: "Match.whenOr" + note: "Each object pattern now rejects top-level keys absent from every variant of the remaining input type, including patterns spread from an array. Correct stale property names or narrow the input before matching." diff --git a/repos/effect/migration/annotations/effect__MergeDecision.yaml b/migration/annotations/effect__MergeDecision.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MergeDecision.yaml rename to migration/annotations/effect__MergeDecision.yaml diff --git a/repos/effect/migration/annotations/effect__MergeState.yaml b/migration/annotations/effect__MergeState.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MergeState.yaml rename to migration/annotations/effect__MergeState.yaml diff --git a/repos/effect/migration/annotations/effect__MergeStrategy.yaml b/migration/annotations/effect__MergeStrategy.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MergeStrategy.yaml rename to migration/annotations/effect__MergeStrategy.yaml diff --git a/repos/effect/migration/annotations/effect__Metric.yaml b/migration/annotations/effect__Metric.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Metric.yaml rename to migration/annotations/effect__Metric.yaml diff --git a/repos/effect/migration/annotations/effect__MetricBoundaries.yaml b/migration/annotations/effect__MetricBoundaries.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricBoundaries.yaml rename to migration/annotations/effect__MetricBoundaries.yaml diff --git a/repos/effect/migration/annotations/effect__MetricHook.yaml b/migration/annotations/effect__MetricHook.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricHook.yaml rename to migration/annotations/effect__MetricHook.yaml diff --git a/repos/effect/migration/annotations/effect__MetricKey.yaml b/migration/annotations/effect__MetricKey.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricKey.yaml rename to migration/annotations/effect__MetricKey.yaml diff --git a/repos/effect/migration/annotations/effect__MetricKeyType.yaml b/migration/annotations/effect__MetricKeyType.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricKeyType.yaml rename to migration/annotations/effect__MetricKeyType.yaml diff --git a/repos/effect/migration/annotations/effect__MetricLabel.yaml b/migration/annotations/effect__MetricLabel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricLabel.yaml rename to migration/annotations/effect__MetricLabel.yaml diff --git a/repos/effect/migration/annotations/effect__MetricPair.yaml b/migration/annotations/effect__MetricPair.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricPair.yaml rename to migration/annotations/effect__MetricPair.yaml diff --git a/repos/effect/migration/annotations/effect__MetricPolling.yaml b/migration/annotations/effect__MetricPolling.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricPolling.yaml rename to migration/annotations/effect__MetricPolling.yaml diff --git a/repos/effect/migration/annotations/effect__MetricRegistry.yaml b/migration/annotations/effect__MetricRegistry.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricRegistry.yaml rename to migration/annotations/effect__MetricRegistry.yaml diff --git a/repos/effect/migration/annotations/effect__MetricState.yaml b/migration/annotations/effect__MetricState.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MetricState.yaml rename to migration/annotations/effect__MetricState.yaml diff --git a/repos/effect/migration/annotations/effect__Micro.yaml b/migration/annotations/effect__Micro.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Micro.yaml rename to migration/annotations/effect__Micro.yaml diff --git a/repos/effect/migration/annotations/effect__ModuleVersion.yaml b/migration/annotations/effect__ModuleVersion.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ModuleVersion.yaml rename to migration/annotations/effect__ModuleVersion.yaml diff --git a/repos/effect/migration/annotations/effect__MutableHashMap.yaml b/migration/annotations/effect__MutableHashMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MutableHashMap.yaml rename to migration/annotations/effect__MutableHashMap.yaml diff --git a/repos/effect/migration/annotations/effect__MutableHashSet.yaml b/migration/annotations/effect__MutableHashSet.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MutableHashSet.yaml rename to migration/annotations/effect__MutableHashSet.yaml diff --git a/repos/effect/migration/annotations/effect__MutableList.yaml b/migration/annotations/effect__MutableList.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MutableList.yaml rename to migration/annotations/effect__MutableList.yaml diff --git a/repos/effect/migration/annotations/effect__MutableQueue.yaml b/migration/annotations/effect__MutableQueue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MutableQueue.yaml rename to migration/annotations/effect__MutableQueue.yaml diff --git a/repos/effect/migration/annotations/effect__MutableRef.yaml b/migration/annotations/effect__MutableRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__MutableRef.yaml rename to migration/annotations/effect__MutableRef.yaml diff --git a/repos/effect/migration/annotations/effect__Number.yaml b/migration/annotations/effect__Number.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Number.yaml rename to migration/annotations/effect__Number.yaml diff --git a/repos/effect/migration/annotations/effect__Option.yaml b/migration/annotations/effect__Option.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Option.yaml rename to migration/annotations/effect__Option.yaml diff --git a/repos/effect/migration/annotations/effect__Order.yaml b/migration/annotations/effect__Order.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Order.yaml rename to migration/annotations/effect__Order.yaml diff --git a/repos/effect/migration/annotations/effect__Ordering.yaml b/migration/annotations/effect__Ordering.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Ordering.yaml rename to migration/annotations/effect__Ordering.yaml diff --git a/repos/effect/migration/annotations/effect__ParseResult.yaml b/migration/annotations/effect__ParseResult.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ParseResult.yaml rename to migration/annotations/effect__ParseResult.yaml diff --git a/repos/effect/migration/annotations/effect__PartitionedSemaphore.yaml b/migration/annotations/effect__PartitionedSemaphore.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__PartitionedSemaphore.yaml rename to migration/annotations/effect__PartitionedSemaphore.yaml diff --git a/repos/effect/migration/annotations/effect__Pipeable.yaml b/migration/annotations/effect__Pipeable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Pipeable.yaml rename to migration/annotations/effect__Pipeable.yaml diff --git a/repos/effect/migration/annotations/effect__Pool.yaml b/migration/annotations/effect__Pool.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Pool.yaml rename to migration/annotations/effect__Pool.yaml diff --git a/repos/effect/migration/annotations/effect__Predicate.yaml b/migration/annotations/effect__Predicate.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Predicate.yaml rename to migration/annotations/effect__Predicate.yaml diff --git a/repos/effect/migration/annotations/effect__Pretty.yaml b/migration/annotations/effect__Pretty.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Pretty.yaml rename to migration/annotations/effect__Pretty.yaml diff --git a/repos/effect/migration/annotations/effect__PubSub.yaml b/migration/annotations/effect__PubSub.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__PubSub.yaml rename to migration/annotations/effect__PubSub.yaml diff --git a/repos/effect/migration/annotations/effect__Queue.yaml b/migration/annotations/effect__Queue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Queue.yaml rename to migration/annotations/effect__Queue.yaml diff --git a/repos/effect/migration/annotations/effect__Random.yaml b/migration/annotations/effect__Random.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Random.yaml rename to migration/annotations/effect__Random.yaml diff --git a/repos/effect/migration/annotations/effect__RateLimiter.yaml b/migration/annotations/effect__RateLimiter.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RateLimiter.yaml rename to migration/annotations/effect__RateLimiter.yaml diff --git a/repos/effect/migration/annotations/effect__RcMap.yaml b/migration/annotations/effect__RcMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RcMap.yaml rename to migration/annotations/effect__RcMap.yaml diff --git a/repos/effect/migration/annotations/effect__RcRef.yaml b/migration/annotations/effect__RcRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RcRef.yaml rename to migration/annotations/effect__RcRef.yaml diff --git a/repos/effect/migration/annotations/effect__Readable.yaml b/migration/annotations/effect__Readable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Readable.yaml rename to migration/annotations/effect__Readable.yaml diff --git a/repos/effect/migration/annotations/effect__Record.yaml b/migration/annotations/effect__Record.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Record.yaml rename to migration/annotations/effect__Record.yaml diff --git a/repos/effect/migration/annotations/effect__RedBlackTree.yaml b/migration/annotations/effect__RedBlackTree.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RedBlackTree.yaml rename to migration/annotations/effect__RedBlackTree.yaml diff --git a/repos/effect/migration/annotations/effect__Redacted.yaml b/migration/annotations/effect__Redacted.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Redacted.yaml rename to migration/annotations/effect__Redacted.yaml diff --git a/repos/effect/migration/annotations/effect__Ref.yaml b/migration/annotations/effect__Ref.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Ref.yaml rename to migration/annotations/effect__Ref.yaml diff --git a/repos/effect/migration/annotations/effect__Reloadable.yaml b/migration/annotations/effect__Reloadable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Reloadable.yaml rename to migration/annotations/effect__Reloadable.yaml diff --git a/repos/effect/migration/annotations/effect__Request.yaml b/migration/annotations/effect__Request.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Request.yaml rename to migration/annotations/effect__Request.yaml diff --git a/repos/effect/migration/annotations/effect__RequestBlock.yaml b/migration/annotations/effect__RequestBlock.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RequestBlock.yaml rename to migration/annotations/effect__RequestBlock.yaml diff --git a/repos/effect/migration/annotations/effect__RequestResolver.yaml b/migration/annotations/effect__RequestResolver.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RequestResolver.yaml rename to migration/annotations/effect__RequestResolver.yaml diff --git a/repos/effect/migration/annotations/effect__Resource.yaml b/migration/annotations/effect__Resource.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Resource.yaml rename to migration/annotations/effect__Resource.yaml diff --git a/repos/effect/migration/annotations/effect__Runtime.yaml b/migration/annotations/effect__Runtime.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Runtime.yaml rename to migration/annotations/effect__Runtime.yaml diff --git a/repos/effect/migration/annotations/effect__RuntimeFlags.yaml b/migration/annotations/effect__RuntimeFlags.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RuntimeFlags.yaml rename to migration/annotations/effect__RuntimeFlags.yaml diff --git a/repos/effect/migration/annotations/effect__RuntimeFlagsPatch.yaml b/migration/annotations/effect__RuntimeFlagsPatch.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__RuntimeFlagsPatch.yaml rename to migration/annotations/effect__RuntimeFlagsPatch.yaml diff --git a/repos/effect/migration/annotations/effect__STM.yaml b/migration/annotations/effect__STM.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__STM.yaml rename to migration/annotations/effect__STM.yaml diff --git a/repos/effect/migration/annotations/effect__Schedule.yaml b/migration/annotations/effect__Schedule.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Schedule.yaml rename to migration/annotations/effect__Schedule.yaml diff --git a/repos/effect/migration/annotations/effect__ScheduleDecision.yaml b/migration/annotations/effect__ScheduleDecision.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ScheduleDecision.yaml rename to migration/annotations/effect__ScheduleDecision.yaml diff --git a/repos/effect/migration/annotations/effect__ScheduleInterval.yaml b/migration/annotations/effect__ScheduleInterval.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ScheduleInterval.yaml rename to migration/annotations/effect__ScheduleInterval.yaml diff --git a/repos/effect/migration/annotations/effect__ScheduleIntervals.yaml b/migration/annotations/effect__ScheduleIntervals.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ScheduleIntervals.yaml rename to migration/annotations/effect__ScheduleIntervals.yaml diff --git a/repos/effect/migration/annotations/effect__Scheduler.yaml b/migration/annotations/effect__Scheduler.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Scheduler.yaml rename to migration/annotations/effect__Scheduler.yaml diff --git a/repos/effect/migration/annotations/effect__Schema.yaml b/migration/annotations/effect__Schema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Schema.yaml rename to migration/annotations/effect__Schema.yaml diff --git a/repos/effect/migration/annotations/effect__SchemaAST.yaml b/migration/annotations/effect__SchemaAST.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__SchemaAST.yaml rename to migration/annotations/effect__SchemaAST.yaml diff --git a/repos/effect/migration/annotations/effect__Scope.yaml b/migration/annotations/effect__Scope.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Scope.yaml rename to migration/annotations/effect__Scope.yaml diff --git a/repos/effect/migration/annotations/effect__ScopedCache.yaml b/migration/annotations/effect__ScopedCache.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ScopedCache.yaml rename to migration/annotations/effect__ScopedCache.yaml diff --git a/repos/effect/migration/annotations/effect__ScopedRef.yaml b/migration/annotations/effect__ScopedRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ScopedRef.yaml rename to migration/annotations/effect__ScopedRef.yaml diff --git a/repos/effect/migration/annotations/effect__Secret.yaml b/migration/annotations/effect__Secret.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Secret.yaml rename to migration/annotations/effect__Secret.yaml diff --git a/repos/effect/migration/annotations/effect__SingleProducerAsyncInput.yaml b/migration/annotations/effect__SingleProducerAsyncInput.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__SingleProducerAsyncInput.yaml rename to migration/annotations/effect__SingleProducerAsyncInput.yaml diff --git a/repos/effect/migration/annotations/effect__Sink.yaml b/migration/annotations/effect__Sink.yaml similarity index 92% rename from repos/effect/migration/annotations/effect__Sink.yaml rename to migration/annotations/effect__Sink.yaml index 636836d09..89b981b17 100644 --- a/repos/effect/migration/annotations/effect__Sink.yaml +++ b/migration/annotations/effect__Sink.yaml @@ -171,16 +171,16 @@ effect/Sink#race: note: "Sink racing (race/raceBoth/raceWith) was removed from v4; broadcast the stream (Stream.broadcast) into two consumers and race the resulting run effects with Effect.race, or write a custom Channel." effect/Sink#raceBoth: replacement: "none" - note: "Removed with the race family; broadcast the stream and use Effect.raceBoth (or Effect.race) on the two Stream.run effects to learn which side won." + note: "Removed with the race family; broadcast the stream and tag each Stream.run effect's result before combining them with Effect.race to learn which side succeeded first." effect/Sink#raceWith: replacement: "none" - note: "Removed, along with the MergeDecision type it depended on; the closest is broadcasting the stream and combining the two run effects manually (Effect.raceWith on the run effects)." + note: "Removed, along with the MergeDecision type it depended on; broadcast the stream and combine the two run effects manually. Use Effect.race for the first success or Effect.raceFirst for the first completion; custom completion and interruption handling requires explicit fibers." effect/Sink#refineOrDie: replacement: "Sink.catch" - note: "Removed; rebuild with the typed-error handler: Sink.catch(self, (e) => Option.match(pf(e), { onSome: Effect.fail, onNone: () => Effect.die(e) })) — note Sink.catch replaces the result on recovery, so refined errors must be re-failed as shown." + note: "For recoverable typed errors, use Sink.catch(self, (e) => Option.match(pf(e), { onSome: Effect.fail, onNone: () => Effect.die(e) })). Causes containing defects skip pf and convert typed errors to defects; use Sink.catchCause for full-cause handling." effect/Sink#refineOrDieWith: replacement: "Sink.catch" - note: "Removed; same pattern as refineOrDie but die with the mapped defect: Sink.catch(self, (e) => Option.match(pf(e), { onSome: Effect.fail, onNone: () => Effect.die(f(e)) }))." + note: "Use the refineOrDie recipe with Effect.die(f(e)) for unrefined errors. Causes containing defects skip pf and f and convert typed errors to defects; use Sink.catchCause for full-cause handling." effect/Sink#splitWhere: replacement: "none" note: "Removed; it re-chunked input so the sink stopped before the first later element matching the predicate — closest v4 options are pre-splitting the stream (Stream.split / Stream.takeWhile) or a custom Sink.fromTransform that cuts pulled arrays at the predicate boundary and returns the rest as leftovers." @@ -202,3 +202,12 @@ effect/Sink#zipRight: effect/Sink#zipWith: replacement: "Sink.flatMap" note: "Removed; use self.pipe(Sink.flatMap((a) => Sink.map(that, (a2) => f(a, a2)))) — sequential only, the { concurrent: true } option has no v4 equivalent." +effect/Sink#mapError: + include: true + replacement: "Sink.mapError" + note: "Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." + +effect/Sink#orElse: + include: true + replacement: "Sink.orElse" + note: "Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block typed recovery." diff --git a/repos/effect/migration/annotations/effect__SortedMap.yaml b/migration/annotations/effect__SortedMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__SortedMap.yaml rename to migration/annotations/effect__SortedMap.yaml diff --git a/repos/effect/migration/annotations/effect__SortedSet.yaml b/migration/annotations/effect__SortedSet.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__SortedSet.yaml rename to migration/annotations/effect__SortedSet.yaml diff --git a/repos/effect/migration/annotations/effect__Stream.yaml b/migration/annotations/effect__Stream.yaml similarity index 90% rename from repos/effect/migration/annotations/effect__Stream.yaml rename to migration/annotations/effect__Stream.yaml index e4396a591..7ee4c80be 100644 --- a/repos/effect/migration/annotations/effect__Stream.yaml +++ b/migration/annotations/effect__Stream.yaml @@ -9,11 +9,11 @@ effect/Stream#bindEffect: effect/Stream#catchTags: include: true replacement: "Stream.catchTags" - note: "Only keys present in the tagged error union are accepted; remove handlers for tags the stream cannot fail with. Untagged errors remain in the error channel." + note: "Only keys in the tagged error union are accepted; untagged errors remain in the error channel. Causes containing defects skip handlers and convert typed errors to defects; interruptions alongside typed errors do not block recovery." effect/Stream#mapBoth: include: true replacement: "Stream.mapBoth" - note: "Rename the options onSuccess to onElement and onFailure to onError. The callbacks still transform emitted elements and typed errors respectively." + note: "Rename the options onSuccess to onElement and onFailure to onError. The callbacks still transform emitted elements and typed errors respectively. Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." effect/Stream#partition: include: true replacement: "Stream.partition" @@ -73,7 +73,7 @@ effect/Stream#async: note: "Stream.callback((queue) => Effect | void, { bufferSize?, strategy? }) replaces the Emit-based async; push with Queue.offer/offerAll, end with Queue.end, fail with Queue.fail." effect/Stream#asyncEffect: replacement: "Stream.callback" - note: "The register function of Stream.callback may return an Effect (run before the stream starts pulling), covering asyncEffect; signal end/failure through the provided Queue." + note: "The register function of Stream.callback may return an Effect, covering asyncEffect. Unlike v3, it is started before the stream pulls but not awaited: it runs synchronously until its first async boundary, so asynchronous setup may still be in progress when pulling begins. Signal end/failure through the provided Queue." effect/Stream#asyncPush: replacement: "Stream.callback" note: "Stream.callback's register effect can use Scope for acquire/release of the external subscription, replacing asyncPush; the Emit ops helpers become plain Queue operations." @@ -98,13 +98,13 @@ effect/Stream#bufferChunks: note: "Chunk->Array rename; buffers whole arrays (chunks) up to capacity with the same strategy options." effect/Stream#catchAll: replacement: "Stream.catch" - note: "Renamed to Stream.catch (exported keyword-style); same (error) => Stream handler for all typed failures." + note: "Rename to Stream.catch with an (error) => Stream handler. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. Use Stream.catchCause for full-cause handling." effect/Stream#catchAllCause: replacement: "Stream.catchCause" note: "Renamed; handler receives the full Cause and returns a recovery stream, identical semantics." effect/Stream#catchSome: replacement: "Stream.catchFilter" - note: "Option-returning partial handler replaced by the Filter API: Stream.catchFilter(filter, f, orElse?) recovers matched errors, unmatched failures pass through (Stream.catchIf for refinement/predicate matching)." + note: "Use Stream.catchFilter(filter, f, orElse?) instead of an Option-returning partial handler. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery." effect/Stream#catchSomeCause: replacement: "Stream.catchCauseFilter" note: "Option-returning cause handler replaced by Stream.catchCauseFilter(filter, f, orElse?) using a Filter on the Cause (Stream.catchCauseIf for refinements)." @@ -149,7 +149,7 @@ effect/Stream#distributedWithDynamic: note: "Dynamic predicate-routed fan-out removed with distributedWith. Use Stream.broadcast/Stream.share for dynamic mirrors plus per-subscriber Stream.filter, or hand-roll routing into Queues via Stream.runForEach." effect/Stream#either: replacement: "Stream.result" - note: "Either is replaced by Result in v4: Stream.result yields Stream, never, R> (element -> Result.succeed, first error -> Result.fail and the stream ends, as before)." + note: "Either is replaced by Result in v4: Stream.result yields Stream, never, R> (element -> Result.succeed, first error -> Result.fail and the stream ends, as before). Causes containing defects are not emitted as Result failures and propagate with typed errors converted to defects; interruptions alongside typed errors do not block typed recovery." effect/Stream#ensuringWith: replacement: "Stream.onExit" note: "Renamed; Stream.onExit runs the finalizer with the Exit of the stream, identical shape." @@ -262,13 +262,13 @@ effect/Stream#orDieWith: note: "orDieWith removed; transform the error first, then convert failures to defects: `self.pipe(Stream.mapError(f), Stream.orDie)`." effect/Stream#orElse: replacement: "Stream.catch" - note: "v3 catchAll was renamed to Stream.catch in v4; orElse ignored the error, so write `Stream.catch(self, () => that())`." + note: "Use Stream.catch(self, () => that()). Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery." effect/Stream#orElseEither: replacement: "Stream.catch" - note: "Removed; Either is replaced by Result in v4. Emulate: `Stream.map(self, Result.succeed).pipe(Stream.catch(() => Stream.map(that(), Result.fail)))` (same encoding v4 Stream.mergeResult uses)." + note: "Replace Either with Result: Stream.map(self, Result.succeed).pipe(Stream.catch(() => Stream.map(that(), Result.fail))). Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery." effect/Stream#orElseFail: replacement: "Stream.mapError" - note: "Removed; it only replaced the failure value: `Stream.mapError(self, () => error())` or `Stream.catch(self, () => Stream.fail(error()))`." + note: "Use Stream.mapError(self, () => error()) to replace each typed error while preserving existing defects and interruptions. Stream.catch is not equivalent for causes containing defects because it skips typed recovery." effect/Stream#orElseIfEmptyChunk: replacement: "Stream.orElseIfEmpty" note: "Folded into Stream.orElseIfEmpty, which now takes a lazy fallback Stream: `Stream.orElseIfEmpty(self, () => Stream.fromArray(array))`; Chunk is replaced by plain arrays." @@ -301,10 +301,10 @@ effect/Stream#provideSomeLayer: note: "v4 Stream.provide accepts a Layer (or Context) and excludes only the provided services from R — same partial-provision semantics." effect/Stream#refineOrDie: replacement: "Stream.catch" - note: "Removed; emulate with `Stream.catch(self, (e) => { const r = pf(e); return Option.isSome(r) ? Stream.fail(r.value) : Stream.die(e) })` — refail refined errors, die on the rest." + note: "For recoverable typed errors, use Stream.catch(self, (e) => { const r = pf(e); return Option.isSome(r) ? Stream.fail(r.value) : Stream.die(e) }). Causes containing defects skip pf and convert typed errors to defects; use Stream.catchCause for full-cause handling." effect/Stream#refineOrDieWith: replacement: "Stream.catch" - note: "Removed; same as refineOrDie but die with the mapped defect: `Stream.die(f(e))` for unrefined errors." + note: "Use the refineOrDie recipe with Stream.die(f(e)) for unrefined errors. Causes containing defects skip pf and f and convert typed errors to defects; use Stream.catchCause for full-cause handling." effect/Stream#repeatEffect: replacement: "Stream.fromEffectRepeat" note: "Renamed; repeats the effect forever emitting each result." @@ -486,3 +486,27 @@ effect/Stream#zipAllWith: effect/Stream#zipWithChunks: replacement: "Stream.zipWithArray" note: "Renamed; the combiner now receives two NonEmptyReadonlyArrays and returns `[output: NonEmptyReadonlyArray, leftoverLeft: ReadonlyArray, leftoverRight: ReadonlyArray]` — the Either-wrapped leftover (ZipChunksResult) is replaced by the two explicit leftover arrays." +effect/Stream#mapError: + include: true + replacement: "Stream.mapError" + note: "Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects." + +effect/Stream#orDie: + include: true + replacement: "Stream.orDie" + note: "Converts typed errors to defects. If a defect is already present, every typed error is converted to a defect, preserving reason order and interruptions." + +effect/Stream#retry: + include: true + replacement: "Stream.retry" + note: "Retries typed source errors, including those alongside interruptions. A source cause containing a defect is not retried and propagates with typed errors converted to defects." + +effect/Stream#catchTag: + include: true + replacement: "Stream.catchTag" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." + +effect/Stream#orElseSucceed: + include: true + replacement: "Stream.orElseSucceed" + note: "Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery." diff --git a/repos/effect/migration/annotations/effect__StreamEmit.yaml b/migration/annotations/effect__StreamEmit.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__StreamEmit.yaml rename to migration/annotations/effect__StreamEmit.yaml diff --git a/repos/effect/migration/annotations/effect__StreamHaltStrategy.yaml b/migration/annotations/effect__StreamHaltStrategy.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__StreamHaltStrategy.yaml rename to migration/annotations/effect__StreamHaltStrategy.yaml diff --git a/repos/effect/migration/annotations/effect__Streamable.yaml b/migration/annotations/effect__Streamable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Streamable.yaml rename to migration/annotations/effect__Streamable.yaml diff --git a/repos/effect/migration/annotations/effect__Struct.yaml b/migration/annotations/effect__Struct.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Struct.yaml rename to migration/annotations/effect__Struct.yaml diff --git a/repos/effect/migration/annotations/effect__Subscribable.yaml b/migration/annotations/effect__Subscribable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Subscribable.yaml rename to migration/annotations/effect__Subscribable.yaml diff --git a/repos/effect/migration/annotations/effect__SubscriptionRef.yaml b/migration/annotations/effect__SubscriptionRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__SubscriptionRef.yaml rename to migration/annotations/effect__SubscriptionRef.yaml diff --git a/repos/effect/migration/annotations/effect__Supervisor.yaml b/migration/annotations/effect__Supervisor.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Supervisor.yaml rename to migration/annotations/effect__Supervisor.yaml diff --git a/repos/effect/migration/annotations/effect__Symbol.yaml b/migration/annotations/effect__Symbol.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Symbol.yaml rename to migration/annotations/effect__Symbol.yaml diff --git a/repos/effect/migration/annotations/effect__SynchronizedRef.yaml b/migration/annotations/effect__SynchronizedRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__SynchronizedRef.yaml rename to migration/annotations/effect__SynchronizedRef.yaml diff --git a/repos/effect/migration/annotations/effect__TArray.yaml b/migration/annotations/effect__TArray.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TArray.yaml rename to migration/annotations/effect__TArray.yaml diff --git a/repos/effect/migration/annotations/effect__TDeferred.yaml b/migration/annotations/effect__TDeferred.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TDeferred.yaml rename to migration/annotations/effect__TDeferred.yaml diff --git a/repos/effect/migration/annotations/effect__TMap.yaml b/migration/annotations/effect__TMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TMap.yaml rename to migration/annotations/effect__TMap.yaml diff --git a/repos/effect/migration/annotations/effect__TPriorityQueue.yaml b/migration/annotations/effect__TPriorityQueue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TPriorityQueue.yaml rename to migration/annotations/effect__TPriorityQueue.yaml diff --git a/repos/effect/migration/annotations/effect__TPubSub.yaml b/migration/annotations/effect__TPubSub.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TPubSub.yaml rename to migration/annotations/effect__TPubSub.yaml diff --git a/repos/effect/migration/annotations/effect__TQueue.yaml b/migration/annotations/effect__TQueue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TQueue.yaml rename to migration/annotations/effect__TQueue.yaml diff --git a/repos/effect/migration/annotations/effect__TRandom.yaml b/migration/annotations/effect__TRandom.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TRandom.yaml rename to migration/annotations/effect__TRandom.yaml diff --git a/repos/effect/migration/annotations/effect__TReentrantLock.yaml b/migration/annotations/effect__TReentrantLock.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TReentrantLock.yaml rename to migration/annotations/effect__TReentrantLock.yaml diff --git a/repos/effect/migration/annotations/effect__TRef.yaml b/migration/annotations/effect__TRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TRef.yaml rename to migration/annotations/effect__TRef.yaml diff --git a/repos/effect/migration/annotations/effect__TSemaphore.yaml b/migration/annotations/effect__TSemaphore.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TSemaphore.yaml rename to migration/annotations/effect__TSemaphore.yaml diff --git a/repos/effect/migration/annotations/effect__TSet.yaml b/migration/annotations/effect__TSet.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TSet.yaml rename to migration/annotations/effect__TSet.yaml diff --git a/repos/effect/migration/annotations/effect__TSubscriptionRef.yaml b/migration/annotations/effect__TSubscriptionRef.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TSubscriptionRef.yaml rename to migration/annotations/effect__TSubscriptionRef.yaml diff --git a/repos/effect/migration/annotations/effect__Take.yaml b/migration/annotations/effect__Take.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Take.yaml rename to migration/annotations/effect__Take.yaml diff --git a/repos/effect/migration/annotations/effect__TestAnnotation.yaml b/migration/annotations/effect__TestAnnotation.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestAnnotation.yaml rename to migration/annotations/effect__TestAnnotation.yaml diff --git a/repos/effect/migration/annotations/effect__TestAnnotationMap.yaml b/migration/annotations/effect__TestAnnotationMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestAnnotationMap.yaml rename to migration/annotations/effect__TestAnnotationMap.yaml diff --git a/repos/effect/migration/annotations/effect__TestAnnotations.yaml b/migration/annotations/effect__TestAnnotations.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestAnnotations.yaml rename to migration/annotations/effect__TestAnnotations.yaml diff --git a/repos/effect/migration/annotations/effect__TestClock.yaml b/migration/annotations/effect__TestClock.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestClock.yaml rename to migration/annotations/effect__TestClock.yaml diff --git a/repos/effect/migration/annotations/effect__TestConfig.yaml b/migration/annotations/effect__TestConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestConfig.yaml rename to migration/annotations/effect__TestConfig.yaml diff --git a/repos/effect/migration/annotations/effect__TestContext.yaml b/migration/annotations/effect__TestContext.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestContext.yaml rename to migration/annotations/effect__TestContext.yaml diff --git a/repos/effect/migration/annotations/effect__TestLive.yaml b/migration/annotations/effect__TestLive.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestLive.yaml rename to migration/annotations/effect__TestLive.yaml diff --git a/repos/effect/migration/annotations/effect__TestServices.yaml b/migration/annotations/effect__TestServices.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestServices.yaml rename to migration/annotations/effect__TestServices.yaml diff --git a/repos/effect/migration/annotations/effect__TestSized.yaml b/migration/annotations/effect__TestSized.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__TestSized.yaml rename to migration/annotations/effect__TestSized.yaml diff --git a/repos/effect/migration/annotations/effect__Tracer.yaml b/migration/annotations/effect__Tracer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Tracer.yaml rename to migration/annotations/effect__Tracer.yaml diff --git a/repos/effect/migration/annotations/effect__Trie.yaml b/migration/annotations/effect__Trie.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Trie.yaml rename to migration/annotations/effect__Trie.yaml diff --git a/repos/effect/migration/annotations/effect__Tuple.yaml b/migration/annotations/effect__Tuple.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Tuple.yaml rename to migration/annotations/effect__Tuple.yaml diff --git a/repos/effect/migration/annotations/effect__Types.yaml b/migration/annotations/effect__Types.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Types.yaml rename to migration/annotations/effect__Types.yaml diff --git a/repos/effect/migration/annotations/effect__UpstreamPullRequest.yaml b/migration/annotations/effect__UpstreamPullRequest.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__UpstreamPullRequest.yaml rename to migration/annotations/effect__UpstreamPullRequest.yaml diff --git a/repos/effect/migration/annotations/effect__UpstreamPullStrategy.yaml b/migration/annotations/effect__UpstreamPullStrategy.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__UpstreamPullStrategy.yaml rename to migration/annotations/effect__UpstreamPullStrategy.yaml diff --git a/repos/effect/migration/annotations/effect__Utils.yaml b/migration/annotations/effect__Utils.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__Utils.yaml rename to migration/annotations/effect__Utils.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock.yaml b/migration/annotations/effect__ai-amazon-bedrock.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock.yaml rename to migration/annotations/effect__ai-amazon-bedrock.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml b/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml rename to migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml b/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml rename to migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml b/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml rename to migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml b/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml rename to migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml b/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml rename to migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml b/migration/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml rename to migration/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml diff --git a/repos/effect/migration/annotations/effect__ai-amazon-bedrock__index.yaml b/migration/annotations/effect__ai-amazon-bedrock__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-amazon-bedrock__index.yaml rename to migration/annotations/effect__ai-amazon-bedrock__index.yaml diff --git a/repos/effect/migration/annotations/effect__ai-anthropic__AnthropicClient.yaml b/migration/annotations/effect__ai-anthropic__AnthropicClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-anthropic__AnthropicClient.yaml rename to migration/annotations/effect__ai-anthropic__AnthropicClient.yaml diff --git a/repos/effect/migration/annotations/effect__ai-anthropic__AnthropicConfig.yaml b/migration/annotations/effect__ai-anthropic__AnthropicConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-anthropic__AnthropicConfig.yaml rename to migration/annotations/effect__ai-anthropic__AnthropicConfig.yaml diff --git a/repos/effect/migration/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml b/migration/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml rename to migration/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml b/migration/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml rename to migration/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml diff --git a/repos/effect/migration/annotations/effect__ai-anthropic__AnthropicTool.yaml b/migration/annotations/effect__ai-anthropic__AnthropicTool.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-anthropic__AnthropicTool.yaml rename to migration/annotations/effect__ai-anthropic__AnthropicTool.yaml diff --git a/repos/effect/migration/annotations/effect__ai-anthropic__Generated.yaml b/migration/annotations/effect__ai-anthropic__Generated.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-anthropic__Generated.yaml rename to migration/annotations/effect__ai-anthropic__Generated.yaml diff --git a/repos/effect/migration/annotations/effect__ai-anthropic__index.yaml b/migration/annotations/effect__ai-anthropic__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-anthropic__index.yaml rename to migration/annotations/effect__ai-anthropic__index.yaml diff --git a/repos/effect/migration/annotations/effect__ai-google.yaml b/migration/annotations/effect__ai-google.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-google.yaml rename to migration/annotations/effect__ai-google.yaml diff --git a/repos/effect/migration/annotations/effect__ai-google__Generated.yaml b/migration/annotations/effect__ai-google__Generated.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-google__Generated.yaml rename to migration/annotations/effect__ai-google__Generated.yaml diff --git a/repos/effect/migration/annotations/effect__ai-google__GoogleClient.yaml b/migration/annotations/effect__ai-google__GoogleClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-google__GoogleClient.yaml rename to migration/annotations/effect__ai-google__GoogleClient.yaml diff --git a/repos/effect/migration/annotations/effect__ai-google__GoogleConfig.yaml b/migration/annotations/effect__ai-google__GoogleConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-google__GoogleConfig.yaml rename to migration/annotations/effect__ai-google__GoogleConfig.yaml diff --git a/repos/effect/migration/annotations/effect__ai-google__GoogleLanguageModel.yaml b/migration/annotations/effect__ai-google__GoogleLanguageModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-google__GoogleLanguageModel.yaml rename to migration/annotations/effect__ai-google__GoogleLanguageModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai-google__GoogleTool.yaml b/migration/annotations/effect__ai-google__GoogleTool.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-google__GoogleTool.yaml rename to migration/annotations/effect__ai-google__GoogleTool.yaml diff --git a/repos/effect/migration/annotations/effect__ai-google__index.yaml b/migration/annotations/effect__ai-google__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-google__index.yaml rename to migration/annotations/effect__ai-google__index.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__Generated.yaml b/migration/annotations/effect__ai-openai__Generated.yaml similarity index 94% rename from repos/effect/migration/annotations/effect__ai-openai__Generated.yaml rename to migration/annotations/effect__ai-openai__Generated.yaml index 5f96a15c4..6dc8673e8 100644 --- a/repos/effect/migration/annotations/effect__ai-openai__Generated.yaml +++ b/migration/annotations/effect__ai-openai__Generated.yaml @@ -5,8 +5,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#AddUploadPartRequest": - replacement: "Generated.AddUploadPartRequest" - note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." + replacement: "Generated.AddUploadPartRequestMultipart" + note: "Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape." "@effect/ai-openai/Generated#AdminApiKey": replacement: "Generated.AdminApiKey" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -29,17 +29,17 @@ replacement: "Generated.ApiKeyList" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#ApplyPatchCallOutputStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ApplyPatchCallOutputStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ApplyPatchCallOutputStatusParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ApplyPatchCallOutputStatusParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ApplyPatchCallStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ApplyPatchCallStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ApplyPatchCallStatusParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ApplyPatchCallStatusParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ApplyPatchCreateFileOperation": replacement: "Generated.ApplyPatchCreateFileOperation" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -65,8 +65,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ApplyPatchOperationParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ApplyPatchOperationParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ApplyPatchToolCall": replacement: "Generated.ApplyPatchToolCall" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -110,8 +110,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ApproximateLocation": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ApproximateLocation" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ApproximateLocationType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -179,14 +179,14 @@ replacement: "Generated.Attachment" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#AttachmentType": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.AttachmentType" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#AudioResponseFormat": replacement: "Generated.AudioResponseFormat" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#AudioTranscription": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.AudioTranscription" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#AudioTranscriptionModel": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -218,20 +218,20 @@ replacement: "Generated.AuditLogEventType" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#AutoChunkingStrategyRequestParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.AutoChunkingStrategyRequestParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#AutoChunkingStrategyRequestParamType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#AutomaticThreadTitlingParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.AutomaticThreadTitlingParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#Batch": replacement: "Generated.Batch" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#BatchError": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.BatchError" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#BatchFileExpirationAfter": replacement: "Generated.BatchFileExpirationAfter" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -242,8 +242,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#BatchRequestCounts": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.BatchRequestCounts" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#BatchStatus": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -428,32 +428,32 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ChatkitConfigurationParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatkitConfigurationParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatkitWorkflow": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatkitWorkflow" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatkitWorkflowTracing": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatkitWorkflowTracing" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatModel": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ChatSessionAutomaticThreadTitling": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatSessionAutomaticThreadTitling" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatSessionChatkitConfiguration": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatSessionChatkitConfiguration" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatSessionFileUpload": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatSessionFileUpload" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatSessionHistory": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatSessionHistory" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatSessionRateLimits": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatSessionRateLimits" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChatSessionResource": replacement: "Generated.ChatSessionResource" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -461,8 +461,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ChatSessionStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ChatSessionStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ChunkingStrategyRequestParam": replacement: "Generated.ChunkingStrategyRequestParam" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -470,8 +470,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ClickButtonType": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ClickButtonType" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ClickParam": replacement: "Generated.ClickParam" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -494,8 +494,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ClientToolCallStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ClientToolCallStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ClosedStatus": replacement: "Generated.ClosedStatus" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -569,8 +569,8 @@ replacement: "Generated.ComputerCallSafetyCheckParam" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#ComputerEnvironment": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ComputerEnvironment" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ComputerScreenshotContent": replacement: "Generated.ComputerScreenshotContent" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -623,8 +623,8 @@ replacement: "Generated.ContainerListResource" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#ContainerMemoryLimit": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ContainerMemoryLimit" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ContainerResource": replacement: "Generated.ContainerResource" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -662,8 +662,8 @@ replacement: "Generated.CreateAssistantRequest" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#CreateBatchRequest": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.CreateBatchRequest" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#CreateBatchRequestCompletionWindow": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -740,8 +740,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#CreateEvalCompletionsRunDataSource": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.CreateEvalCompletionsRunDataSource" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#CreateEvalCompletionsRunDataSourceInputMessagesEnumType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -749,8 +749,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#CreateEvalCustomDataSourceConfig": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.CreateEvalCustomDataSourceConfig" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#CreateEvalCustomDataSourceConfigType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -758,8 +758,8 @@ replacement: "Generated.CreateEvalItem" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#CreateEvalJsonlRunDataSource": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.CreateEvalJsonlRunDataSource" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#CreateEvalJsonlRunDataSourceType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -770,8 +770,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#CreateEvalLogsDataSourceConfig": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.CreateEvalLogsDataSourceConfig" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#CreateEvalLogsDataSourceConfigType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -779,8 +779,8 @@ replacement: "Generated.CreateEvalRequest" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#CreateEvalResponsesRunDataSource": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.CreateEvalResponsesRunDataSource" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#CreateEvalResponsesRunDataSourceInputMessagesEnumType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -791,14 +791,14 @@ replacement: "Generated.CreateEvalRunRequest" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#CreateEvalStoredCompletionsDataSourceConfig": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.CreateEvalStoredCompletionsDataSourceConfig" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#CreateEvalStoredCompletionsDataSourceConfigType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#CreateFileRequest": - replacement: "Generated.CreateFileRequest" - note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." + replacement: "Generated.CreateFileRequestMultipart" + note: "Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape." "@effect/ai-openai/Generated#CreateFineTuningCheckpointPermissionRequest": replacement: "Generated.CreateFineTuningCheckpointPermissionRequest" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -824,8 +824,8 @@ replacement: "Generated.CreateGroupUserBody" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#CreateImageEditRequest": - replacement: "Generated.CreateImageEditRequest" - note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." + replacement: "Generated.CreateImageEditRequestMultipart" + note: "Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape. JSON image edits instead use Generated.CreateImageEditRequestJson." "@effect/ai-openai/Generated#CreateImageEditRequestBackground": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -872,8 +872,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#CreateImageVariationRequest": - replacement: "Generated.CreateImageVariationRequest" - note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." + replacement: "Generated.CreateImageVariationRequestMultipart" + note: "Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape." "@effect/ai-openai/Generated#CreateImageVariationRequestModelEnum": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -962,8 +962,8 @@ replacement: "Generated.CreateTranscription200" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#CreateTranscriptionRequest": - replacement: "Generated.CreateTranscriptionRequest" - note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." + replacement: "Generated.CreateTranscriptionRequestMultipart" + note: "Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape." "@effect/ai-openai/Generated#CreateTranscriptionRequestModelEnum": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -983,8 +983,8 @@ replacement: "Generated.CreateTranslation200" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#CreateTranslationRequest": - replacement: "Generated.CreateTranslationRequest" - note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." + replacement: "Generated.CreateTranslationRequestMultipart" + note: "Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape." "@effect/ai-openai/Generated#CreateTranslationRequestModelEnum": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1136,8 +1136,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#DetailEnum": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.DetailEnum" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#DoubleClickAction": replacement: "Generated.DoubleClickAction" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -1145,8 +1145,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#DownloadFile200": - replacement: "Generated.DownloadFile200" - note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." + replacement: "none" + note: "The standalone response schema was removed. The generated client's downloadFile method returns Uint8Array, optionally paired with the HTTP response according to its config; downloadFileStream streams Uint8Array chunks." "@effect/ai-openai/Generated#Drag": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1187,8 +1187,8 @@ replacement: "Generated.EvalApiError" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#EvalCustomDataSourceConfig": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.EvalCustomDataSourceConfig" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#EvalCustomDataSourceConfigType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1259,8 +1259,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#EvalLogsDataSourceConfig": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.EvalLogsDataSourceConfig" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#EvalLogsDataSourceConfigType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1301,8 +1301,8 @@ replacement: "Generated.EvalRunOutputItemResult" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#EvalStoredCompletionsDataSourceConfig": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.EvalStoredCompletionsDataSourceConfig" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#EvalStoredCompletionsDataSourceConfigType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1313,8 +1313,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#ExpiresAfterParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ExpiresAfterParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ExpiresAfterParamAnchor": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1322,8 +1322,8 @@ replacement: "Generated.FileAnnotation" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#FileAnnotationSource": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.FileAnnotationSource" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#FileAnnotationSourceType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1373,11 +1373,11 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#FileUploadParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.FileUploadParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#Filters": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.Filters" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#FineTuneDPOHyperparameters": replacement: "Generated.FineTuneDPOHyperparameters" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -1496,8 +1496,8 @@ replacement: "Generated.FunctionAndCustomToolCallOutput" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#FunctionCallItemStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.FunctionCallItemStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#FunctionCallOutputItemParam": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1511,11 +1511,11 @@ replacement: "Generated.FunctionParameters" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#FunctionShellAction": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.FunctionShellAction" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#FunctionShellActionParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.FunctionShellActionParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#FunctionShellCall": replacement: "Generated.FunctionShellCall" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -1526,8 +1526,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#FunctionShellCallItemStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.FunctionShellCallItemStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#FunctionShellCallOutput": replacement: "Generated.FunctionShellCallOutput" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -1556,8 +1556,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#FunctionShellCallOutputOutcomeParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.FunctionShellCallOutputOutcomeParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#FunctionShellCallOutputTimeoutOutcome": replacement: "Generated.FunctionShellCallOutputTimeoutOutcome" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -1658,8 +1658,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#GraderMulti": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.GraderMulti" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#GraderMultiType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -1694,8 +1694,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#GrammarSyntax1": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.GrammarSyntax1" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#Group": replacement: "Generated.Group" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -1739,17 +1739,17 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#HistoryParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.HistoryParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#HybridSearchOptions": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.HybridSearchOptions" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#Image": replacement: "Generated.Image" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#ImageDetail": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ImageDetail" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ImageGenInputUsageDetails": replacement: "Generated.ImageGenInputUsageDetails" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -1808,8 +1808,8 @@ replacement: "Generated.IncludeEnum" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#InferenceOptions": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.InferenceOptions" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#InputAudio": replacement: "Generated.InputAudio" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -2303,8 +2303,8 @@ replacement: "Generated.MCPToolCall" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#MCPToolCallStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.MCPToolCallStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#MCPToolCallType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -2390,8 +2390,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#MessageRole": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.MessageRole" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#MessageStatus": replacement: "OpenAiSchema.MessageStatus" note: "Use the focused v4 OpenAiSchema definition; the old generated export was removed when the OpenAI specification client was regenerated." @@ -2468,8 +2468,8 @@ replacement: "Generated.OrderEnum" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#OtherChunkingStrategyResponseParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.OtherChunkingStrategyResponseParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#OtherChunkingStrategyResponseParamType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -2675,14 +2675,14 @@ replacement: "Generated.PublicUpdateOrganizationRoleBody" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#RankerVersionType": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.RankerVersionType" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#RankingOptions": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.RankingOptions" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#RateLimitsParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.RateLimitsParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#RealtimeAudioFormats": replacement: "Generated.RealtimeAudioFormats" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -2975,8 +2975,8 @@ replacement: "Generated.RunStepCompletionUsage" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#RunStepDetailsMessageCreationObject": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.RunStepDetailsMessageCreationObject" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#RunStepDetailsMessageCreationObjectType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -2990,14 +2990,14 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputImageObject": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.RunStepDetailsToolCallsCodeOutputImageObject" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputImageObjectType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputLogsObject": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.RunStepDetailsToolCallsCodeOutputLogsObject" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputLogsObjectType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -3020,8 +3020,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#RunStepDetailsToolCallsObject": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.RunStepDetailsToolCallsObject" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#RunStepDetailsToolCallsObjectType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -3059,14 +3059,14 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#SearchContextSize": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.SearchContextSize" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#ServiceTier": replacement: "Generated.ServiceTier" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#ServiceTierEnum": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.ServiceTierEnum" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#SpecificApplyPatchParam": replacement: "Generated.SpecificApplyPatchParam" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -3083,14 +3083,14 @@ replacement: "Generated.StaticChunkingStrategy" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#StaticChunkingStrategyRequestParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.StaticChunkingStrategyRequestParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#StaticChunkingStrategyRequestParamType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#StaticChunkingStrategyResponseParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.StaticChunkingStrategyResponseParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#StaticChunkingStrategyResponseParamType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -3131,8 +3131,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#TaskType": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.TaskType" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#TextAnnotation": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -3269,8 +3269,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#TruncationEnum": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.TruncationEnum" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#Type": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -3335,8 +3335,8 @@ replacement: "Generated.UrlAnnotation" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#UrlAnnotationSource": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.UrlAnnotationSource" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#UrlAnnotationSourceType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -3614,8 +3614,8 @@ replacement: "Generated.VideoListResource" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." "@effect/ai-openai/Generated#VideoModel": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.VideoModel" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#VideoResource": replacement: "Generated.VideoResource" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -3623,14 +3623,14 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#VideoSeconds": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.VideoSeconds" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#VideoSize": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.VideoSize" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#VideoStatus": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.VideoStatus" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#VoiceIdsShared": replacement: "Generated.VoiceIdsShared" note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." @@ -3644,20 +3644,20 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#WebSearchActionFind": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.WebSearchActionFind" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#WebSearchActionFindType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#WebSearchActionOpenPage": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.WebSearchActionOpenPage" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#WebSearchActionOpenPageType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#WebSearchActionSearch": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.WebSearchActionSearch" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#WebSearchActionSearchType": replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." @@ -3707,8 +3707,8 @@ replacement: "none" note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." "@effect/ai-openai/Generated#WorkflowParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.WorkflowParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." "@effect/ai-openai/Generated#WorkflowTracingParam": - replacement: "none" - note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." + replacement: "Generated.WorkflowTracingParam" + note: "Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification." diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiClient.yaml b/migration/annotations/effect__ai-openai__OpenAiClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__OpenAiClient.yaml rename to migration/annotations/effect__ai-openai__OpenAiClient.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiConfig.yaml b/migration/annotations/effect__ai-openai__OpenAiConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__OpenAiConfig.yaml rename to migration/annotations/effect__ai-openai__OpenAiConfig.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml b/migration/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml rename to migration/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiLanguageModel.yaml b/migration/annotations/effect__ai-openai__OpenAiLanguageModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__OpenAiLanguageModel.yaml rename to migration/annotations/effect__ai-openai__OpenAiLanguageModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiTelemetry.yaml b/migration/annotations/effect__ai-openai__OpenAiTelemetry.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__OpenAiTelemetry.yaml rename to migration/annotations/effect__ai-openai__OpenAiTelemetry.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiTokenizer.yaml b/migration/annotations/effect__ai-openai__OpenAiTokenizer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__OpenAiTokenizer.yaml rename to migration/annotations/effect__ai-openai__OpenAiTokenizer.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__OpenAiTool.yaml b/migration/annotations/effect__ai-openai__OpenAiTool.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__OpenAiTool.yaml rename to migration/annotations/effect__ai-openai__OpenAiTool.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openai__index.yaml b/migration/annotations/effect__ai-openai__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openai__index.yaml rename to migration/annotations/effect__ai-openai__index.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openrouter__Generated.yaml b/migration/annotations/effect__ai-openrouter__Generated.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openrouter__Generated.yaml rename to migration/annotations/effect__ai-openrouter__Generated.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml b/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml rename to migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterConfig.yaml b/migration/annotations/effect__ai-openrouter__OpenRouterConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterConfig.yaml rename to migration/annotations/effect__ai-openrouter__OpenRouterConfig.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml b/migration/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml rename to migration/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai-openrouter__index.yaml b/migration/annotations/effect__ai-openrouter__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai-openrouter__index.yaml rename to migration/annotations/effect__ai-openrouter__index.yaml diff --git a/repos/effect/migration/annotations/effect__ai.yaml b/migration/annotations/effect__ai.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai.yaml rename to migration/annotations/effect__ai.yaml diff --git a/repos/effect/migration/annotations/effect__ai__AiError.yaml b/migration/annotations/effect__ai__AiError.yaml similarity index 72% rename from repos/effect/migration/annotations/effect__ai__AiError.yaml rename to migration/annotations/effect__ai__AiError.yaml index b5c46028f..07736140e 100644 --- a/repos/effect/migration/annotations/effect__ai__AiError.yaml +++ b/migration/annotations/effect__ai__AiError.yaml @@ -5,14 +5,14 @@ replacement: "AiError.make + AiError.NetworkError" note: "Replace the old top-level request error with an AiError whose reason is NetworkError. NetworkError.fromRequestError converts a v4 HttpClientError.RequestError." "@effect/ai/AiError#HttpResponseError": - replacement: "AiError.make + AiError.reasonFromHttpStatus / AiError.InvalidOutputError" - note: "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." + replacement: "AiError.make + AiError.reasonFromHttpStatus / AiError.InvalidResponseError / AiError.InvalidOutputError" + note: "There is no single v4 response-error class. Wrap a semantic reason with AiError.make: use reasonFromHttpStatus for status failures. Provider response-schema and SSE decoding failures use non-retryable InvalidResponseError. Lower-level HTTP client DecodeError and EmptyBodyError still map to retryable InvalidOutputError in provider adapters." "@effect/ai/AiError#MalformedInput": replacement: "AiError.make + AiError.InvalidUserInputError" note: "Replace the old top-level input error with an AiError whose reason is InvalidUserInputError. Use InvalidRequestError when the provider request parameters themselves are malformed." "@effect/ai/AiError#MalformedOutput": - replacement: "AiError.make + AiError.InvalidOutputError" - note: "Replace the old top-level output error with an AiError whose reason is InvalidOutputError. The old fromParseError helper becomes InvalidOutputError.fromSchemaError." + replacement: "AiError.make + AiError.InvalidOutputError / AiError.InvalidResponseError" + note: "Replace the old top-level output error with an AiError wrapper. Use retryable InvalidOutputError for invalid model-generated output and non-retryable InvalidResponseError for incompatible provider response formats, including response-schema and SSE decoding failures. Replace fromParseError with InvalidOutputError.fromSchemaError or InvalidResponseError.fromSchemaError according to what was being decoded; pass module and method to AiError.make." "@effect/ai/AiError#TypeId": replacement: "AiError.isAiError" note: "The AiError brand is private in v4. Use isAiError for runtime narrowing, or isAiErrorReason for a reason, instead of inspecting or constructing the type id." diff --git a/repos/effect/migration/annotations/effect__ai__Chat.yaml b/migration/annotations/effect__ai__Chat.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__Chat.yaml rename to migration/annotations/effect__ai__Chat.yaml diff --git a/repos/effect/migration/annotations/effect__ai__EmbeddingModel.yaml b/migration/annotations/effect__ai__EmbeddingModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__EmbeddingModel.yaml rename to migration/annotations/effect__ai__EmbeddingModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai__IdGenerator.yaml b/migration/annotations/effect__ai__IdGenerator.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__IdGenerator.yaml rename to migration/annotations/effect__ai__IdGenerator.yaml diff --git a/repos/effect/migration/annotations/effect__ai__LanguageModel.yaml b/migration/annotations/effect__ai__LanguageModel.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__LanguageModel.yaml rename to migration/annotations/effect__ai__LanguageModel.yaml diff --git a/repos/effect/migration/annotations/effect__ai__McpSchema.yaml b/migration/annotations/effect__ai__McpSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__McpSchema.yaml rename to migration/annotations/effect__ai__McpSchema.yaml diff --git a/repos/effect/migration/annotations/effect__ai__McpServer.yaml b/migration/annotations/effect__ai__McpServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__McpServer.yaml rename to migration/annotations/effect__ai__McpServer.yaml diff --git a/repos/effect/migration/annotations/effect__ai__Model.yaml b/migration/annotations/effect__ai__Model.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__Model.yaml rename to migration/annotations/effect__ai__Model.yaml diff --git a/repos/effect/migration/annotations/effect__ai__Prompt.yaml b/migration/annotations/effect__ai__Prompt.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__Prompt.yaml rename to migration/annotations/effect__ai__Prompt.yaml diff --git a/repos/effect/migration/annotations/effect__ai__Response.yaml b/migration/annotations/effect__ai__Response.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__Response.yaml rename to migration/annotations/effect__ai__Response.yaml diff --git a/repos/effect/migration/annotations/effect__ai__Telemetry.yaml b/migration/annotations/effect__ai__Telemetry.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__Telemetry.yaml rename to migration/annotations/effect__ai__Telemetry.yaml diff --git a/repos/effect/migration/annotations/effect__ai__Tool.yaml b/migration/annotations/effect__ai__Tool.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__Tool.yaml rename to migration/annotations/effect__ai__Tool.yaml diff --git a/repos/effect/migration/annotations/effect__ai__Toolkit.yaml b/migration/annotations/effect__ai__Toolkit.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__Toolkit.yaml rename to migration/annotations/effect__ai__Toolkit.yaml diff --git a/repos/effect/migration/annotations/effect__ai__index.yaml b/migration/annotations/effect__ai__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__ai__index.yaml rename to migration/annotations/effect__ai__index.yaml diff --git a/repos/effect/migration/annotations/effect__cli.yaml b/migration/annotations/effect__cli.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli.yaml rename to migration/annotations/effect__cli.yaml diff --git a/repos/effect/migration/annotations/effect__cli__Args.yaml b/migration/annotations/effect__cli__Args.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__Args.yaml rename to migration/annotations/effect__cli__Args.yaml diff --git a/repos/effect/migration/annotations/effect__cli__AutoCorrect.yaml b/migration/annotations/effect__cli__AutoCorrect.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__AutoCorrect.yaml rename to migration/annotations/effect__cli__AutoCorrect.yaml diff --git a/repos/effect/migration/annotations/effect__cli__BuiltInOptions.yaml b/migration/annotations/effect__cli__BuiltInOptions.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__BuiltInOptions.yaml rename to migration/annotations/effect__cli__BuiltInOptions.yaml diff --git a/repos/effect/migration/annotations/effect__cli__CliApp.yaml b/migration/annotations/effect__cli__CliApp.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__CliApp.yaml rename to migration/annotations/effect__cli__CliApp.yaml diff --git a/repos/effect/migration/annotations/effect__cli__CliConfig.yaml b/migration/annotations/effect__cli__CliConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__CliConfig.yaml rename to migration/annotations/effect__cli__CliConfig.yaml diff --git a/repos/effect/migration/annotations/effect__cli__Command.yaml b/migration/annotations/effect__cli__Command.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__Command.yaml rename to migration/annotations/effect__cli__Command.yaml diff --git a/repos/effect/migration/annotations/effect__cli__CommandDescriptor.yaml b/migration/annotations/effect__cli__CommandDescriptor.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__CommandDescriptor.yaml rename to migration/annotations/effect__cli__CommandDescriptor.yaml diff --git a/repos/effect/migration/annotations/effect__cli__CommandDirective.yaml b/migration/annotations/effect__cli__CommandDirective.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__CommandDirective.yaml rename to migration/annotations/effect__cli__CommandDirective.yaml diff --git a/repos/effect/migration/annotations/effect__cli__ConfigFile.yaml b/migration/annotations/effect__cli__ConfigFile.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__ConfigFile.yaml rename to migration/annotations/effect__cli__ConfigFile.yaml diff --git a/repos/effect/migration/annotations/effect__cli__HelpDoc.yaml b/migration/annotations/effect__cli__HelpDoc.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__HelpDoc.yaml rename to migration/annotations/effect__cli__HelpDoc.yaml diff --git a/repos/effect/migration/annotations/effect__cli__HelpDoc__Span.yaml b/migration/annotations/effect__cli__HelpDoc__Span.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__HelpDoc__Span.yaml rename to migration/annotations/effect__cli__HelpDoc__Span.yaml diff --git a/repos/effect/migration/annotations/effect__cli__Options.yaml b/migration/annotations/effect__cli__Options.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__Options.yaml rename to migration/annotations/effect__cli__Options.yaml diff --git a/repos/effect/migration/annotations/effect__cli__Primitive.yaml b/migration/annotations/effect__cli__Primitive.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__Primitive.yaml rename to migration/annotations/effect__cli__Primitive.yaml diff --git a/repos/effect/migration/annotations/effect__cli__Prompt.yaml b/migration/annotations/effect__cli__Prompt.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__Prompt.yaml rename to migration/annotations/effect__cli__Prompt.yaml diff --git a/repos/effect/migration/annotations/effect__cli__Usage.yaml b/migration/annotations/effect__cli__Usage.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__Usage.yaml rename to migration/annotations/effect__cli__Usage.yaml diff --git a/repos/effect/migration/annotations/effect__cli__ValidationError.yaml b/migration/annotations/effect__cli__ValidationError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__ValidationError.yaml rename to migration/annotations/effect__cli__ValidationError.yaml diff --git a/repos/effect/migration/annotations/effect__cli__index.yaml b/migration/annotations/effect__cli__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cli__index.yaml rename to migration/annotations/effect__cli__index.yaml diff --git a/repos/effect/migration/annotations/effect__cluster.yaml b/migration/annotations/effect__cluster.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster.yaml rename to migration/annotations/effect__cluster.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__ClusterCron.yaml b/migration/annotations/effect__cluster__ClusterCron.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__ClusterCron.yaml rename to migration/annotations/effect__cluster__ClusterCron.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__ClusterError.yaml b/migration/annotations/effect__cluster__ClusterError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__ClusterError.yaml rename to migration/annotations/effect__cluster__ClusterError.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__ClusterSchema.yaml b/migration/annotations/effect__cluster__ClusterSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__ClusterSchema.yaml rename to migration/annotations/effect__cluster__ClusterSchema.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__ClusterWorkflowEngine.yaml b/migration/annotations/effect__cluster__ClusterWorkflowEngine.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__ClusterWorkflowEngine.yaml rename to migration/annotations/effect__cluster__ClusterWorkflowEngine.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__DeliverAt.yaml b/migration/annotations/effect__cluster__DeliverAt.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__DeliverAt.yaml rename to migration/annotations/effect__cluster__DeliverAt.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Entity.yaml b/migration/annotations/effect__cluster__Entity.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Entity.yaml rename to migration/annotations/effect__cluster__Entity.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__EntityAddress.yaml b/migration/annotations/effect__cluster__EntityAddress.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__EntityAddress.yaml rename to migration/annotations/effect__cluster__EntityAddress.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__EntityId.yaml b/migration/annotations/effect__cluster__EntityId.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__EntityId.yaml rename to migration/annotations/effect__cluster__EntityId.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__EntityProxy.yaml b/migration/annotations/effect__cluster__EntityProxy.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__EntityProxy.yaml rename to migration/annotations/effect__cluster__EntityProxy.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml b/migration/annotations/effect__cluster__EntityProxyServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml rename to migration/annotations/effect__cluster__EntityProxyServer.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__EntityResource.yaml b/migration/annotations/effect__cluster__EntityResource.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__EntityResource.yaml rename to migration/annotations/effect__cluster__EntityResource.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Envelope.yaml b/migration/annotations/effect__cluster__Envelope.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Envelope.yaml rename to migration/annotations/effect__cluster__Envelope.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__HttpRunner.yaml b/migration/annotations/effect__cluster__HttpRunner.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__HttpRunner.yaml rename to migration/annotations/effect__cluster__HttpRunner.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__K8sHttpClient.yaml b/migration/annotations/effect__cluster__K8sHttpClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__K8sHttpClient.yaml rename to migration/annotations/effect__cluster__K8sHttpClient.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__MachineId.yaml b/migration/annotations/effect__cluster__MachineId.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__MachineId.yaml rename to migration/annotations/effect__cluster__MachineId.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Message.yaml b/migration/annotations/effect__cluster__Message.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Message.yaml rename to migration/annotations/effect__cluster__Message.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__MessageStorage.yaml b/migration/annotations/effect__cluster__MessageStorage.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__MessageStorage.yaml rename to migration/annotations/effect__cluster__MessageStorage.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Reply.yaml b/migration/annotations/effect__cluster__Reply.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Reply.yaml rename to migration/annotations/effect__cluster__Reply.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Runner.yaml b/migration/annotations/effect__cluster__Runner.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Runner.yaml rename to migration/annotations/effect__cluster__Runner.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__RunnerAddress.yaml b/migration/annotations/effect__cluster__RunnerAddress.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__RunnerAddress.yaml rename to migration/annotations/effect__cluster__RunnerAddress.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__RunnerHealth.yaml b/migration/annotations/effect__cluster__RunnerHealth.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__RunnerHealth.yaml rename to migration/annotations/effect__cluster__RunnerHealth.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__RunnerServer.yaml b/migration/annotations/effect__cluster__RunnerServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__RunnerServer.yaml rename to migration/annotations/effect__cluster__RunnerServer.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__RunnerStorage.yaml b/migration/annotations/effect__cluster__RunnerStorage.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__RunnerStorage.yaml rename to migration/annotations/effect__cluster__RunnerStorage.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Runners.yaml b/migration/annotations/effect__cluster__Runners.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Runners.yaml rename to migration/annotations/effect__cluster__Runners.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__ShardId.yaml b/migration/annotations/effect__cluster__ShardId.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__ShardId.yaml rename to migration/annotations/effect__cluster__ShardId.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Sharding.yaml b/migration/annotations/effect__cluster__Sharding.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Sharding.yaml rename to migration/annotations/effect__cluster__Sharding.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__ShardingConfig.yaml b/migration/annotations/effect__cluster__ShardingConfig.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__ShardingConfig.yaml rename to migration/annotations/effect__cluster__ShardingConfig.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__ShardingRegistrationEvent.yaml b/migration/annotations/effect__cluster__ShardingRegistrationEvent.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__ShardingRegistrationEvent.yaml rename to migration/annotations/effect__cluster__ShardingRegistrationEvent.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__SingleRunner.yaml b/migration/annotations/effect__cluster__SingleRunner.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__SingleRunner.yaml rename to migration/annotations/effect__cluster__SingleRunner.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Singleton.yaml b/migration/annotations/effect__cluster__Singleton.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Singleton.yaml rename to migration/annotations/effect__cluster__Singleton.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__SingletonAddress.yaml b/migration/annotations/effect__cluster__SingletonAddress.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__SingletonAddress.yaml rename to migration/annotations/effect__cluster__SingletonAddress.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__Snowflake.yaml b/migration/annotations/effect__cluster__Snowflake.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__Snowflake.yaml rename to migration/annotations/effect__cluster__Snowflake.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__SocketRunner.yaml b/migration/annotations/effect__cluster__SocketRunner.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__SocketRunner.yaml rename to migration/annotations/effect__cluster__SocketRunner.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__SqlMessageStorage.yaml b/migration/annotations/effect__cluster__SqlMessageStorage.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__SqlMessageStorage.yaml rename to migration/annotations/effect__cluster__SqlMessageStorage.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__SqlRunnerStorage.yaml b/migration/annotations/effect__cluster__SqlRunnerStorage.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__SqlRunnerStorage.yaml rename to migration/annotations/effect__cluster__SqlRunnerStorage.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__TestRunner.yaml b/migration/annotations/effect__cluster__TestRunner.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__TestRunner.yaml rename to migration/annotations/effect__cluster__TestRunner.yaml diff --git a/repos/effect/migration/annotations/effect__cluster__index.yaml b/migration/annotations/effect__cluster__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__cluster__index.yaml rename to migration/annotations/effect__cluster__index.yaml diff --git a/repos/effect/migration/annotations/effect__experimental.yaml b/migration/annotations/effect__experimental.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental.yaml rename to migration/annotations/effect__experimental.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__DevTools.yaml b/migration/annotations/effect__experimental__DevTools.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__DevTools.yaml rename to migration/annotations/effect__experimental__DevTools.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__DevTools__Client.yaml b/migration/annotations/effect__experimental__DevTools__Client.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__DevTools__Client.yaml rename to migration/annotations/effect__experimental__DevTools__Client.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__DevTools__Domain.yaml b/migration/annotations/effect__experimental__DevTools__Domain.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__DevTools__Domain.yaml rename to migration/annotations/effect__experimental__DevTools__Domain.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__DevTools__Server.yaml b/migration/annotations/effect__experimental__DevTools__Server.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__DevTools__Server.yaml rename to migration/annotations/effect__experimental__DevTools__Server.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Event.yaml b/migration/annotations/effect__experimental__Event.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Event.yaml rename to migration/annotations/effect__experimental__Event.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__EventGroup.yaml b/migration/annotations/effect__experimental__EventGroup.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__EventGroup.yaml rename to migration/annotations/effect__experimental__EventGroup.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__EventJournal.yaml b/migration/annotations/effect__experimental__EventJournal.yaml similarity index 57% rename from repos/effect/migration/annotations/effect__experimental__EventJournal.yaml rename to migration/annotations/effect__experimental__EventJournal.yaml index a63c391dd..2a86c5ab4 100644 --- a/repos/effect/migration/annotations/effect__experimental__EventJournal.yaml +++ b/migration/annotations/effect__experimental__EventJournal.yaml @@ -5,8 +5,9 @@ replacement: none note: The v4 error marker is private; narrow with EventJournalError instead. "@effect/experimental/EventJournal#makeEntryId": + include: true replacement: effect/eventlog/EventJournal#makeEntryIdUnsafe - note: The unchecked EntryId constructor was renamed to makeEntryIdUnsafe. + note: The unchecked EntryId constructor was renamed to makeEntryIdUnsafe. Without explicit msecs, ids advance monotonically within the process using a logical millisecond and counter, so their timestamp can exceed wall-clock time when the clock stalls or moves backwards. Explicit msecs bypasses this sequence. "@effect/experimental/EventJournal#makeRemoteId": replacement: effect/eventlog/EventJournal#makeRemoteIdUnsafe note: The unchecked RemoteId constructor was renamed to makeRemoteIdUnsafe. @@ -16,3 +17,7 @@ "@effect/experimental/EventJournal#makeMemory": replacement: effect/eventlog/EventJournal#makeMemory note: The in-memory constructor moved into core Effect and now returns the Context.Service implementation through its Service projection. +"@effect/experimental/EventJournal#EventJournal": + include: true + replacement: effect/eventlog/EventJournal#EventJournal + note: Custom journal services must implement entriesById, writeMany, and writeRemoteAcks. writeFromRemote.effect now receives one array of entry/conflicts pairs per batch, not one pair per callback. Read conflicts before invoking it and commit the batch only after it succeeds. Replay entries in entry-id order; conflicts are stored entries with strictly greater ids, also in id order. Compaction replacements use the source entry as their ordering and conflict anchor. diff --git a/repos/effect/migration/annotations/effect__experimental__EventLog.yaml b/migration/annotations/effect__experimental__EventLog.yaml similarity index 53% rename from repos/effect/migration/annotations/effect__experimental__EventLog.yaml rename to migration/annotations/effect__experimental__EventLog.yaml index 9d8229a1e..dfb6f8891 100644 --- a/repos/effect/migration/annotations/effect__experimental__EventLog.yaml +++ b/migration/annotations/effect__experimental__EventLog.yaml @@ -2,8 +2,9 @@ replacement: effect/eventlog/EventLog#group note: Import group from the v4 EventLog module; it now requires the shared Registry service. "@effect/experimental/EventLog#Handlers": + include: true replacement: effect/eventlog/EventLog#Handlers - note: Import Handlers from the v4 EventLog module; handlers now also receive storeId. + note: Import Handlers from the v4 EventLog module; handlers now also receive storeId. Remote decode or handler failures leave the entire batch uncommitted and retry it after backoff instead of logging and committing past the failure. Replay handlers must tolerate repeated execution. Conflicts are stored entries with strictly greater ids, in id order; compacted replacements use their source entry as the conflict anchor. "@effect/experimental/EventLog#Handlers.Error": replacement: effect/eventlog/EventLog#Handlers.Error note: Import the retained Handlers.Error type from the v4 EventLog module. @@ -25,3 +26,11 @@ "@effect/experimental/EventLog#SchemaTypeId": replacement: effect/eventlog/EventLog#SchemaTypeId note: Import SchemaTypeId from the v4 EventLog module. +"@effect/experimental/EventLog#groupCompaction": + include: true + replacement: effect/eventlog/EventLog#groupCompaction + note: Use forward(entry) to retain an input entry unchanged rather than writing a replacement. Entries neither forwarded nor replaced are dropped; undecodable entries are preserved automatically. Replacements are anchored to the first decoded entry for the primary key and replayed in anchor-id order, then write order, with conflicts newer than that anchor. Collapsing entries across an intervening stored entry for the same event and primary key can break convergence. +"@effect/experimental/EventLog#makeClient": + include: true + replacement: effect/eventlog/EventLog#makeClient + note: The returned client now takes one { event, payload } object instead of separate event and payload arguments. The payload remains paired with its event tag, and each call delegates to EventLog.write; use EventLog.writeMany for an atomic batch of events. diff --git a/migration/annotations/effect__experimental__EventLogEncryption.yaml b/migration/annotations/effect__experimental__EventLogEncryption.yaml new file mode 100644 index 000000000..a6ac8b091 --- /dev/null +++ b/migration/annotations/effect__experimental__EventLogEncryption.yaml @@ -0,0 +1,12 @@ +"@effect/experimental/EventLogEncryption#EncryptedEntry": + include: true + replacement: effect/eventlog/EventLogEncryption#EncryptedBatch + note: Replace per-entry encrypted records with batches containing entryIds, iv, and encryptedBatch. Each batch encrypts multiple entries as one message and authenticates the store id and ordered entry ids; existing per-entry ciphertext is not compatible with this format. +"@effect/experimental/EventLogEncryption#EncryptedRemoteEntry": + include: true + replacement: effect/eventlog/EventLogEncryption#EncryptedRemoteBatch + note: Remote encrypted records are now batches with entryIds, iv, and encryptedBatch. sequence is the first entry sequence; subsequent entries have consecutive sequences. Existing per-entry ciphertext is not compatible with the batch format. +"@effect/experimental/EventLogEncryption#EventLogEncryption": + include: true + replacement: effect/eventlog/EventLogEncryption#EventLogEncryption + note: Use the Context.Service and its Service projection. encrypt and decrypt now take storeId after identity. encrypt returns EncryptedBatch records rather than an iv/encryptedEntries object; decrypt takes EncryptedRemoteBatch records and returns RemoteEntry records. Encryption authenticates the store id and ordered entry ids, so old per-entry ciphertext cannot be supplied as a batch. diff --git a/repos/effect/migration/annotations/effect__experimental__EventLogRemote.yaml b/migration/annotations/effect__experimental__EventLogRemote.yaml similarity index 79% rename from repos/effect/migration/annotations/effect__experimental__EventLogRemote.yaml rename to migration/annotations/effect__experimental__EventLogRemote.yaml index 8e632aa21..c4989a140 100644 --- a/repos/effect/migration/annotations/effect__experimental__EventLogRemote.yaml +++ b/migration/annotations/effect__experimental__EventLogRemote.yaml @@ -1,9 +1,11 @@ "@effect/experimental/EventLogRemote#Ack": - replacement: none - note: A write acknowledgement is now the void success of EventLogMessage.WriteSingleRpc or WriteChunkedRpc. + include: true + replacement: effect/eventlog/EventLogMessage#WriteAck + note: Write RPCs now return arrays of entryId/sequence acknowledgements in request order. WriteSingleRpc returns them directly; WriteChunkedRpc returns them on the completing chunk, with empty arrays for earlier chunks. "@effect/experimental/EventLogRemote#Changes": + include: true replacement: effect/eventlog/EventLogMessage#ChangesRpc - note: ChangesRpc replaces the separate request and response models with one streaming RPC. + note: ChangesRpc replaces the separate request and response models with one streaming RPC. Encrypted changes carry EncryptedRemoteBatch records. Built-in servers acknowledge writes from this connection instead of echoing them through changes. "@effect/experimental/EventLogRemote#decodeRequest": replacement: effect/eventlog/EventLogMessage#EventLogRemoteRpcs note: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific request decoder. @@ -17,8 +19,9 @@ replacement: effect/eventlog/EventLogMessage#EventLogRemoteRpcs note: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific response encoder. "@effect/experimental/EventLogRemote#EventLogRemote": + include: true replacement: effect/eventlog/EventLogRemote#EventLogRemote - note: Use the v4 Context.Service; methods now take storeId-aware options. + note: Use the v4 Context.Service; methods now take storeId-aware options. write may return entryId/sequence acknowledgement arrays; custom remotes can still return void when confirming uploads through changes instead. "@effect/experimental/EventLogRemote#fromSocket": replacement: effect/eventlog/EventLogRemote#makeEncrypted + effect/rpc/RpcClient#makeProtocolSocket note: Construct the encrypted remote separately from its generic RPC socket protocol. @@ -55,3 +58,7 @@ "@effect/experimental/EventLogRemote#StopChanges": replacement: none note: Interrupt the ChangesRpc stream instead of sending a StopChanges message. +"@effect/experimental/EventLogRemote#WriteEntries": + include: true + replacement: effect/eventlog/EventLogMessage#WriteEntries + note: Use the storeId-aware write payload with batches of EncryptedBatch instead of iv and encryptedEntries. Each batch contains entryIds, iv, and encryptedBatch; it is not compatible with the old per-entry ciphertext format. diff --git a/repos/effect/migration/annotations/effect__experimental__EventLogServer.yaml b/migration/annotations/effect__experimental__EventLogServer.yaml similarity index 58% rename from repos/effect/migration/annotations/effect__experimental__EventLogServer.yaml rename to migration/annotations/effect__experimental__EventLogServer.yaml index 4968aa18b..065a058b8 100644 --- a/repos/effect/migration/annotations/effect__experimental__EventLogServer.yaml +++ b/migration/annotations/effect__experimental__EventLogServer.yaml @@ -14,4 +14,8 @@ "@effect/experimental/EventLogServer#Storage": include: true replacement: effect/eventlog/EventLogServerEncrypted#Storage - note: Use the encrypted server Storage service, which is storeId- and session-aware. + note: Use the storeId- and session-aware encrypted server Storage service. write and changes now operate on encrypted batches. write returns newly stored batches and skips a batch only when all its entry ids are already known. Custom storage must implement entrySequences to look up acknowledgements for skipped batches; each batch owns consecutive sequences starting at its sequence. +"@effect/experimental/EventLogServer#PersistedEntry": + include: true + replacement: effect/eventlog/EventLogEncryption#EncryptedBatch + note: The per-entry class was removed; encrypted storage now accepts batch records containing entryIds, iv, and encryptedBatch. The class encoding helpers and entryIdString getter are not retained. Old per-entry ciphertext cannot be reused as an encrypted batch. diff --git a/repos/effect/migration/annotations/effect__experimental__EventLogServer__Cloudflare.yaml b/migration/annotations/effect__experimental__EventLogServer__Cloudflare.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__EventLogServer__Cloudflare.yaml rename to migration/annotations/effect__experimental__EventLogServer__Cloudflare.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Machine.yaml b/migration/annotations/effect__experimental__Machine.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Machine.yaml rename to migration/annotations/effect__experimental__Machine.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Machine__Procedure.yaml b/migration/annotations/effect__experimental__Machine__Procedure.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Machine__Procedure.yaml rename to migration/annotations/effect__experimental__Machine__Procedure.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Machine__ProcedureList.yaml b/migration/annotations/effect__experimental__Machine__ProcedureList.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Machine__ProcedureList.yaml rename to migration/annotations/effect__experimental__Machine__ProcedureList.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Machine__SerializableProcedureList.yaml b/migration/annotations/effect__experimental__Machine__SerializableProcedureList.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Machine__SerializableProcedureList.yaml rename to migration/annotations/effect__experimental__Machine__SerializableProcedureList.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__PersistedCache.yaml b/migration/annotations/effect__experimental__PersistedCache.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__PersistedCache.yaml rename to migration/annotations/effect__experimental__PersistedCache.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__PersistedQueue.yaml b/migration/annotations/effect__experimental__PersistedQueue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__PersistedQueue.yaml rename to migration/annotations/effect__experimental__PersistedQueue.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__PersistedQueue__Redis.yaml b/migration/annotations/effect__experimental__PersistedQueue__Redis.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__PersistedQueue__Redis.yaml rename to migration/annotations/effect__experimental__PersistedQueue__Redis.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Persistence.yaml b/migration/annotations/effect__experimental__Persistence.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Persistence.yaml rename to migration/annotations/effect__experimental__Persistence.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Persistence__Lmdb.yaml b/migration/annotations/effect__experimental__Persistence__Lmdb.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Persistence__Lmdb.yaml rename to migration/annotations/effect__experimental__Persistence__Lmdb.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Persistence__Redis.yaml b/migration/annotations/effect__experimental__Persistence__Redis.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Persistence__Redis.yaml rename to migration/annotations/effect__experimental__Persistence__Redis.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml b/migration/annotations/effect__experimental__RateLimiter.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__RateLimiter.yaml rename to migration/annotations/effect__experimental__RateLimiter.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__RateLimiter__Redis.yaml b/migration/annotations/effect__experimental__RateLimiter__Redis.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__RateLimiter__Redis.yaml rename to migration/annotations/effect__experimental__RateLimiter__Redis.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Reactivity.yaml b/migration/annotations/effect__experimental__Reactivity.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Reactivity.yaml rename to migration/annotations/effect__experimental__Reactivity.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__RequestResolver.yaml b/migration/annotations/effect__experimental__RequestResolver.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__RequestResolver.yaml rename to migration/annotations/effect__experimental__RequestResolver.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__Sse.yaml b/migration/annotations/effect__experimental__Sse.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__Sse.yaml rename to migration/annotations/effect__experimental__Sse.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__VariantSchema.yaml b/migration/annotations/effect__experimental__VariantSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__VariantSchema.yaml rename to migration/annotations/effect__experimental__VariantSchema.yaml diff --git a/repos/effect/migration/annotations/effect__experimental__index.yaml b/migration/annotations/effect__experimental__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__experimental__index.yaml rename to migration/annotations/effect__experimental__index.yaml diff --git a/repos/effect/migration/annotations/effect__index.yaml b/migration/annotations/effect__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__index.yaml rename to migration/annotations/effect__index.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__Logger.yaml b/migration/annotations/effect__opentelemetry__Logger.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__Logger.yaml rename to migration/annotations/effect__opentelemetry__Logger.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__Metrics.yaml b/migration/annotations/effect__opentelemetry__Metrics.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__Metrics.yaml rename to migration/annotations/effect__opentelemetry__Metrics.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__NodeSdk.yaml b/migration/annotations/effect__opentelemetry__NodeSdk.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__NodeSdk.yaml rename to migration/annotations/effect__opentelemetry__NodeSdk.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__Otlp.yaml b/migration/annotations/effect__opentelemetry__Otlp.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__Otlp.yaml rename to migration/annotations/effect__opentelemetry__Otlp.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__OtlpLogger.yaml b/migration/annotations/effect__opentelemetry__OtlpLogger.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__OtlpLogger.yaml rename to migration/annotations/effect__opentelemetry__OtlpLogger.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__OtlpMetrics.yaml b/migration/annotations/effect__opentelemetry__OtlpMetrics.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__OtlpMetrics.yaml rename to migration/annotations/effect__opentelemetry__OtlpMetrics.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__OtlpResource.yaml b/migration/annotations/effect__opentelemetry__OtlpResource.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__OtlpResource.yaml rename to migration/annotations/effect__opentelemetry__OtlpResource.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__OtlpTracer.yaml b/migration/annotations/effect__opentelemetry__OtlpTracer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__OtlpTracer.yaml rename to migration/annotations/effect__opentelemetry__OtlpTracer.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__Resource.yaml b/migration/annotations/effect__opentelemetry__Resource.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__Resource.yaml rename to migration/annotations/effect__opentelemetry__Resource.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__Tracer.yaml b/migration/annotations/effect__opentelemetry__Tracer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__Tracer.yaml rename to migration/annotations/effect__opentelemetry__Tracer.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__WebSdk.yaml b/migration/annotations/effect__opentelemetry__WebSdk.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__WebSdk.yaml rename to migration/annotations/effect__opentelemetry__WebSdk.yaml diff --git a/repos/effect/migration/annotations/effect__opentelemetry__index.yaml b/migration/annotations/effect__opentelemetry__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__opentelemetry__index.yaml rename to migration/annotations/effect__opentelemetry__index.yaml diff --git a/repos/effect/migration/annotations/effect__platform-browser__BrowserHttpClient.yaml b/migration/annotations/effect__platform-browser__BrowserHttpClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-browser__BrowserHttpClient.yaml rename to migration/annotations/effect__platform-browser__BrowserHttpClient.yaml diff --git a/repos/effect/migration/annotations/effect__platform-browser__BrowserWorker.yaml b/migration/annotations/effect__platform-browser__BrowserWorker.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-browser__BrowserWorker.yaml rename to migration/annotations/effect__platform-browser__BrowserWorker.yaml diff --git a/repos/effect/migration/annotations/effect__platform-browser__BrowserWorkerRunner.yaml b/migration/annotations/effect__platform-browser__BrowserWorkerRunner.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-browser__BrowserWorkerRunner.yaml rename to migration/annotations/effect__platform-browser__BrowserWorkerRunner.yaml diff --git a/repos/effect/migration/annotations/effect__platform-browser__Clipboard.yaml b/migration/annotations/effect__platform-browser__Clipboard.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-browser__Clipboard.yaml rename to migration/annotations/effect__platform-browser__Clipboard.yaml diff --git a/repos/effect/migration/annotations/effect__platform-browser__Geolocation.yaml b/migration/annotations/effect__platform-browser__Geolocation.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-browser__Geolocation.yaml rename to migration/annotations/effect__platform-browser__Geolocation.yaml diff --git a/repos/effect/migration/annotations/effect__platform-browser__Permissions.yaml b/migration/annotations/effect__platform-browser__Permissions.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-browser__Permissions.yaml rename to migration/annotations/effect__platform-browser__Permissions.yaml diff --git a/repos/effect/migration/annotations/effect__platform-browser__index.yaml b/migration/annotations/effect__platform-browser__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-browser__index.yaml rename to migration/annotations/effect__platform-browser__index.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunCommandExecutor.yaml b/migration/annotations/effect__platform-bun__BunCommandExecutor.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunCommandExecutor.yaml rename to migration/annotations/effect__platform-bun__BunCommandExecutor.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunContext.yaml b/migration/annotations/effect__platform-bun__BunContext.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunContext.yaml rename to migration/annotations/effect__platform-bun__BunContext.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml b/migration/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml rename to migration/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunHttpPlatform.yaml b/migration/annotations/effect__platform-bun__BunHttpPlatform.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunHttpPlatform.yaml rename to migration/annotations/effect__platform-bun__BunHttpPlatform.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunHttpServer.yaml b/migration/annotations/effect__platform-bun__BunHttpServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunHttpServer.yaml rename to migration/annotations/effect__platform-bun__BunHttpServer.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunHttpServerRequest.yaml b/migration/annotations/effect__platform-bun__BunHttpServerRequest.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunHttpServerRequest.yaml rename to migration/annotations/effect__platform-bun__BunHttpServerRequest.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunKeyValueStore.yaml b/migration/annotations/effect__platform-bun__BunKeyValueStore.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunKeyValueStore.yaml rename to migration/annotations/effect__platform-bun__BunKeyValueStore.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunSink.yaml b/migration/annotations/effect__platform-bun__BunSink.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunSink.yaml rename to migration/annotations/effect__platform-bun__BunSink.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunSocket.yaml b/migration/annotations/effect__platform-bun__BunSocket.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunSocket.yaml rename to migration/annotations/effect__platform-bun__BunSocket.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunSocketServer.yaml b/migration/annotations/effect__platform-bun__BunSocketServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunSocketServer.yaml rename to migration/annotations/effect__platform-bun__BunSocketServer.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml b/migration/annotations/effect__platform-bun__BunStream.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunStream.yaml rename to migration/annotations/effect__platform-bun__BunStream.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunTerminal.yaml b/migration/annotations/effect__platform-bun__BunTerminal.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunTerminal.yaml rename to migration/annotations/effect__platform-bun__BunTerminal.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__BunWorker.yaml b/migration/annotations/effect__platform-bun__BunWorker.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__BunWorker.yaml rename to migration/annotations/effect__platform-bun__BunWorker.yaml diff --git a/repos/effect/migration/annotations/effect__platform-bun__index.yaml b/migration/annotations/effect__platform-bun__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-bun__index.yaml rename to migration/annotations/effect__platform-bun__index.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml b/migration/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml rename to migration/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml b/migration/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml rename to migration/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml b/migration/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml rename to migration/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeMultipart.yaml b/migration/annotations/effect__platform-node-shared__NodeMultipart.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeMultipart.yaml rename to migration/annotations/effect__platform-node-shared__NodeMultipart.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeSink.yaml b/migration/annotations/effect__platform-node-shared__NodeSink.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeSink.yaml rename to migration/annotations/effect__platform-node-shared__NodeSink.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeSocket.yaml b/migration/annotations/effect__platform-node-shared__NodeSocket.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeSocket.yaml rename to migration/annotations/effect__platform-node-shared__NodeSocket.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeStream.yaml b/migration/annotations/effect__platform-node-shared__NodeStream.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeStream.yaml rename to migration/annotations/effect__platform-node-shared__NodeStream.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node-shared__NodeTerminal.yaml b/migration/annotations/effect__platform-node-shared__NodeTerminal.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node-shared__NodeTerminal.yaml rename to migration/annotations/effect__platform-node-shared__NodeTerminal.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeCommandExecutor.yaml b/migration/annotations/effect__platform-node__NodeCommandExecutor.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeCommandExecutor.yaml rename to migration/annotations/effect__platform-node__NodeCommandExecutor.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeContext.yaml b/migration/annotations/effect__platform-node__NodeContext.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeContext.yaml rename to migration/annotations/effect__platform-node__NodeContext.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml b/migration/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml rename to migration/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeHttpClient.yaml b/migration/annotations/effect__platform-node__NodeHttpClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeHttpClient.yaml rename to migration/annotations/effect__platform-node__NodeHttpClient.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeHttpServer.yaml b/migration/annotations/effect__platform-node__NodeHttpServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeHttpServer.yaml rename to migration/annotations/effect__platform-node__NodeHttpServer.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeKeyValueStore.yaml b/migration/annotations/effect__platform-node__NodeKeyValueStore.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeKeyValueStore.yaml rename to migration/annotations/effect__platform-node__NodeKeyValueStore.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeTerminal.yaml b/migration/annotations/effect__platform-node__NodeTerminal.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeTerminal.yaml rename to migration/annotations/effect__platform-node__NodeTerminal.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__NodeWorker.yaml b/migration/annotations/effect__platform-node__NodeWorker.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__NodeWorker.yaml rename to migration/annotations/effect__platform-node__NodeWorker.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__Undici.yaml b/migration/annotations/effect__platform-node__Undici.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__Undici.yaml rename to migration/annotations/effect__platform-node__Undici.yaml diff --git a/repos/effect/migration/annotations/effect__platform-node__index.yaml b/migration/annotations/effect__platform-node__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform-node__index.yaml rename to migration/annotations/effect__platform-node__index.yaml diff --git a/repos/effect/migration/annotations/effect__platform.yaml b/migration/annotations/effect__platform.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform.yaml rename to migration/annotations/effect__platform.yaml diff --git a/repos/effect/migration/annotations/effect__platform__ChannelSchema.yaml b/migration/annotations/effect__platform__ChannelSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__ChannelSchema.yaml rename to migration/annotations/effect__platform__ChannelSchema.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Command.yaml b/migration/annotations/effect__platform__Command.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Command.yaml rename to migration/annotations/effect__platform__Command.yaml diff --git a/repos/effect/migration/annotations/effect__platform__CommandExecutor.yaml b/migration/annotations/effect__platform__CommandExecutor.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__CommandExecutor.yaml rename to migration/annotations/effect__platform__CommandExecutor.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Cookies.yaml b/migration/annotations/effect__platform__Cookies.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Cookies.yaml rename to migration/annotations/effect__platform__Cookies.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Effectify.yaml b/migration/annotations/effect__platform__Effectify.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Effectify.yaml rename to migration/annotations/effect__platform__Effectify.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Error.yaml b/migration/annotations/effect__platform__Error.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Error.yaml rename to migration/annotations/effect__platform__Error.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Etag.yaml b/migration/annotations/effect__platform__Etag.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Etag.yaml rename to migration/annotations/effect__platform__Etag.yaml diff --git a/repos/effect/migration/annotations/effect__platform__FetchHttpClient.yaml b/migration/annotations/effect__platform__FetchHttpClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__FetchHttpClient.yaml rename to migration/annotations/effect__platform__FetchHttpClient.yaml diff --git a/repos/effect/migration/annotations/effect__platform__FileSystem.yaml b/migration/annotations/effect__platform__FileSystem.yaml similarity index 92% rename from repos/effect/migration/annotations/effect__platform__FileSystem.yaml rename to migration/annotations/effect__platform__FileSystem.yaml index a2b699595..364692141 100644 --- a/repos/effect/migration/annotations/effect__platform__FileSystem.yaml +++ b/migration/annotations/effect__platform__FileSystem.yaml @@ -7,6 +7,9 @@ "@effect/platform/FileSystem#File.Descriptor": replacement: "none" note: "Native file descriptors are no longer part of the portable File interface; use the File methods and keep any platform handle private in custom implementations." +"@effect/platform/FileSystem#File.Info": + replacement: "FileSystem.File.Info" + note: "The module moved to effect/FileSystem. dev is now bigint; ino, rdev, nlink, and blocks are Option. Update custom stat results, comparisons, arithmetic, and serialization rather than converting these values to number and losing precision. mode, uid, and gid remain numeric; size and blksize use ByteSize. Node and Bun preserve the 64-bit metadata exactly, while Deno may round large values before converting them to bigint." "@effect/platform/FileSystem#FileDescriptor": replacement: "none" note: "The descriptor branding constructor was removed with the public fd field; use File operations instead of exposing a native descriptor." diff --git a/repos/effect/migration/annotations/effect__platform__Headers.yaml b/migration/annotations/effect__platform__Headers.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Headers.yaml rename to migration/annotations/effect__platform__Headers.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApi.yaml b/migration/annotations/effect__platform__HttpApi.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApi.yaml rename to migration/annotations/effect__platform__HttpApi.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml b/migration/annotations/effect__platform__HttpApiBuilder.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml rename to migration/annotations/effect__platform__HttpApiBuilder.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiClient.yaml b/migration/annotations/effect__platform__HttpApiClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiClient.yaml rename to migration/annotations/effect__platform__HttpApiClient.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiEndpoint.yaml b/migration/annotations/effect__platform__HttpApiEndpoint.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiEndpoint.yaml rename to migration/annotations/effect__platform__HttpApiEndpoint.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiError.yaml b/migration/annotations/effect__platform__HttpApiError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiError.yaml rename to migration/annotations/effect__platform__HttpApiError.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiGroup.yaml b/migration/annotations/effect__platform__HttpApiGroup.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiGroup.yaml rename to migration/annotations/effect__platform__HttpApiGroup.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiMiddleware.yaml b/migration/annotations/effect__platform__HttpApiMiddleware.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiMiddleware.yaml rename to migration/annotations/effect__platform__HttpApiMiddleware.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiScalar.yaml b/migration/annotations/effect__platform__HttpApiScalar.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiScalar.yaml rename to migration/annotations/effect__platform__HttpApiScalar.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiSchema.yaml b/migration/annotations/effect__platform__HttpApiSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiSchema.yaml rename to migration/annotations/effect__platform__HttpApiSchema.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiSecurity.yaml b/migration/annotations/effect__platform__HttpApiSecurity.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiSecurity.yaml rename to migration/annotations/effect__platform__HttpApiSecurity.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApiSwagger.yaml b/migration/annotations/effect__platform__HttpApiSwagger.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApiSwagger.yaml rename to migration/annotations/effect__platform__HttpApiSwagger.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpApp.yaml b/migration/annotations/effect__platform__HttpApp.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpApp.yaml rename to migration/annotations/effect__platform__HttpApp.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpBody.yaml b/migration/annotations/effect__platform__HttpBody.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpBody.yaml rename to migration/annotations/effect__platform__HttpBody.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpClient.yaml b/migration/annotations/effect__platform__HttpClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpClient.yaml rename to migration/annotations/effect__platform__HttpClient.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpClientError.yaml b/migration/annotations/effect__platform__HttpClientError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpClientError.yaml rename to migration/annotations/effect__platform__HttpClientError.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpClientRequest.yaml b/migration/annotations/effect__platform__HttpClientRequest.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpClientRequest.yaml rename to migration/annotations/effect__platform__HttpClientRequest.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml b/migration/annotations/effect__platform__HttpClientResponse.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpClientResponse.yaml rename to migration/annotations/effect__platform__HttpClientResponse.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml b/migration/annotations/effect__platform__HttpIncomingMessage.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml rename to migration/annotations/effect__platform__HttpIncomingMessage.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpLayerRouter.yaml b/migration/annotations/effect__platform__HttpLayerRouter.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpLayerRouter.yaml rename to migration/annotations/effect__platform__HttpLayerRouter.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpMethod.yaml b/migration/annotations/effect__platform__HttpMethod.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpMethod.yaml rename to migration/annotations/effect__platform__HttpMethod.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpMiddleware.yaml b/migration/annotations/effect__platform__HttpMiddleware.yaml similarity index 84% rename from repos/effect/migration/annotations/effect__platform__HttpMiddleware.yaml rename to migration/annotations/effect__platform__HttpMiddleware.yaml index dd8f50df1..bc9b63307 100644 --- a/repos/effect/migration/annotations/effect__platform__HttpMiddleware.yaml +++ b/migration/annotations/effect__platform__HttpMiddleware.yaml @@ -25,3 +25,6 @@ "@effect/platform/HttpMiddleware#withTracerDisabledWhenEffect": replacement: "Effect.provideService(effect, HttpMiddleware.TracerDisabledWhen, predicate)" note: "Provide the TracerDisabledWhen reference locally to the effect." +"@effect/platform/HttpMiddleware#xForwardedHeaders": + replacement: "HttpMiddleware.xForwardedHeaders({ trustedProxies })" + note: "Now requires a trusted proxy policy; headers from untrusted peers are ignored and X-Forwarded-For is read from the right. HttpServerRequest.toURL no longer reads x-forwarded-proto directly." diff --git a/repos/effect/migration/annotations/effect__platform__HttpMultiplex.yaml b/migration/annotations/effect__platform__HttpMultiplex.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpMultiplex.yaml rename to migration/annotations/effect__platform__HttpMultiplex.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml b/migration/annotations/effect__platform__HttpPlatform.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpPlatform.yaml rename to migration/annotations/effect__platform__HttpPlatform.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpRouter.yaml b/migration/annotations/effect__platform__HttpRouter.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpRouter.yaml rename to migration/annotations/effect__platform__HttpRouter.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpServer.yaml b/migration/annotations/effect__platform__HttpServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpServer.yaml rename to migration/annotations/effect__platform__HttpServer.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpServerError.yaml b/migration/annotations/effect__platform__HttpServerError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpServerError.yaml rename to migration/annotations/effect__platform__HttpServerError.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpServerRequest.yaml b/migration/annotations/effect__platform__HttpServerRequest.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpServerRequest.yaml rename to migration/annotations/effect__platform__HttpServerRequest.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpServerRespondable.yaml b/migration/annotations/effect__platform__HttpServerRespondable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpServerRespondable.yaml rename to migration/annotations/effect__platform__HttpServerRespondable.yaml diff --git a/repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml b/migration/annotations/effect__platform__HttpServerResponse.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__HttpServerResponse.yaml rename to migration/annotations/effect__platform__HttpServerResponse.yaml diff --git a/repos/effect/migration/annotations/effect__platform__KeyValueStore.yaml b/migration/annotations/effect__platform__KeyValueStore.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__KeyValueStore.yaml rename to migration/annotations/effect__platform__KeyValueStore.yaml diff --git a/repos/effect/migration/annotations/effect__platform__MsgPack.yaml b/migration/annotations/effect__platform__MsgPack.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__MsgPack.yaml rename to migration/annotations/effect__platform__MsgPack.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Multipart.yaml b/migration/annotations/effect__platform__Multipart.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Multipart.yaml rename to migration/annotations/effect__platform__Multipart.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Ndjson.yaml b/migration/annotations/effect__platform__Ndjson.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Ndjson.yaml rename to migration/annotations/effect__platform__Ndjson.yaml diff --git a/repos/effect/migration/annotations/effect__platform__OpenApi.yaml b/migration/annotations/effect__platform__OpenApi.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__OpenApi.yaml rename to migration/annotations/effect__platform__OpenApi.yaml diff --git a/repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml b/migration/annotations/effect__platform__OpenApiJsonSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml rename to migration/annotations/effect__platform__OpenApiJsonSchema.yaml diff --git a/migration/annotations/effect__platform__Path.yaml b/migration/annotations/effect__platform__Path.yaml new file mode 100644 index 000000000..bbb536df2 --- /dev/null +++ b/migration/annotations/effect__platform__Path.yaml @@ -0,0 +1,6 @@ +"@effect/platform/Path#Path": + replacement: "Path.Path" + note: "The module moved to effect/Path. Custom service implementations must now provide resolveWithin as well. Prefer Path.make with the existing platform operations: it adds the type marker and derives resolveWithin, which returns an Option and checks lexical containment without following symlinks." +"@effect/platform/Path#TypeId": + replacement: "typeof Path.TypeId" + note: "The module moved to effect/Path; the runtime marker remains exported but the separate type alias was removed." diff --git a/repos/effect/migration/annotations/effect__platform__PlatformConfigProvider.yaml b/migration/annotations/effect__platform__PlatformConfigProvider.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__PlatformConfigProvider.yaml rename to migration/annotations/effect__platform__PlatformConfigProvider.yaml diff --git a/repos/effect/migration/annotations/effect__platform__PlatformLogger.yaml b/migration/annotations/effect__platform__PlatformLogger.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__PlatformLogger.yaml rename to migration/annotations/effect__platform__PlatformLogger.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Runtime.yaml b/migration/annotations/effect__platform__Runtime.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Runtime.yaml rename to migration/annotations/effect__platform__Runtime.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Socket.yaml b/migration/annotations/effect__platform__Socket.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Socket.yaml rename to migration/annotations/effect__platform__Socket.yaml diff --git a/repos/effect/migration/annotations/effect__platform__SocketServer.yaml b/migration/annotations/effect__platform__SocketServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__SocketServer.yaml rename to migration/annotations/effect__platform__SocketServer.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Template.yaml b/migration/annotations/effect__platform__Template.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Template.yaml rename to migration/annotations/effect__platform__Template.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Terminal.yaml b/migration/annotations/effect__platform__Terminal.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Terminal.yaml rename to migration/annotations/effect__platform__Terminal.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Transferable.yaml b/migration/annotations/effect__platform__Transferable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Transferable.yaml rename to migration/annotations/effect__platform__Transferable.yaml diff --git a/repos/effect/migration/annotations/effect__platform__Url.yaml b/migration/annotations/effect__platform__Url.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Url.yaml rename to migration/annotations/effect__platform__Url.yaml diff --git a/repos/effect/migration/annotations/effect__platform__UrlParams.yaml b/migration/annotations/effect__platform__UrlParams.yaml similarity index 92% rename from repos/effect/migration/annotations/effect__platform__UrlParams.yaml rename to migration/annotations/effect__platform__UrlParams.yaml index 83fa4c75e..8d5c9cb44 100644 --- a/repos/effect/migration/annotations/effect__platform__UrlParams.yaml +++ b/migration/annotations/effect__platform__UrlParams.yaml @@ -51,4 +51,4 @@ note: "Retained and broadened to accept any UrlParams.Input." "@effect/platform/UrlParams#UrlParams": replacement: "UrlParams.UrlParams" - note: "Import UrlParams from effect/http. It is now a branded iterable object with a params field rather than a ReadonlyArray; construct it with UrlParams.make or UrlParams.fromInput." + note: "Import UrlParams from effect/http. It is now a branded iterable object with a params field rather than a ReadonlyArray; construct it with UrlParams.make or UrlParams.fromInput. Iteration and params values are string | Redacted.Redacted, not just string. getAll, getFirst, getLast and toRecord unwrap values; toString also serializes unwrapped values and can expose secrets." diff --git a/repos/effect/migration/annotations/effect__platform__Worker.yaml b/migration/annotations/effect__platform__Worker.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__Worker.yaml rename to migration/annotations/effect__platform__Worker.yaml diff --git a/repos/effect/migration/annotations/effect__platform__WorkerError.yaml b/migration/annotations/effect__platform__WorkerError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__WorkerError.yaml rename to migration/annotations/effect__platform__WorkerError.yaml diff --git a/repos/effect/migration/annotations/effect__platform__WorkerRunner.yaml b/migration/annotations/effect__platform__WorkerRunner.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__WorkerRunner.yaml rename to migration/annotations/effect__platform__WorkerRunner.yaml diff --git a/repos/effect/migration/annotations/effect__platform__index.yaml b/migration/annotations/effect__platform__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__platform__index.yaml rename to migration/annotations/effect__platform__index.yaml diff --git a/repos/effect/migration/annotations/effect__printer-ansi.yaml b/migration/annotations/effect__printer-ansi.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer-ansi.yaml rename to migration/annotations/effect__printer-ansi.yaml diff --git a/repos/effect/migration/annotations/effect__printer-ansi__Ansi.yaml b/migration/annotations/effect__printer-ansi__Ansi.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer-ansi__Ansi.yaml rename to migration/annotations/effect__printer-ansi__Ansi.yaml diff --git a/repos/effect/migration/annotations/effect__printer-ansi__AnsiDoc.yaml b/migration/annotations/effect__printer-ansi__AnsiDoc.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer-ansi__AnsiDoc.yaml rename to migration/annotations/effect__printer-ansi__AnsiDoc.yaml diff --git a/repos/effect/migration/annotations/effect__printer-ansi__Color.yaml b/migration/annotations/effect__printer-ansi__Color.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer-ansi__Color.yaml rename to migration/annotations/effect__printer-ansi__Color.yaml diff --git a/repos/effect/migration/annotations/effect__printer-ansi__index.yaml b/migration/annotations/effect__printer-ansi__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer-ansi__index.yaml rename to migration/annotations/effect__printer-ansi__index.yaml diff --git a/repos/effect/migration/annotations/effect__printer.yaml b/migration/annotations/effect__printer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer.yaml rename to migration/annotations/effect__printer.yaml diff --git a/repos/effect/migration/annotations/effect__printer__Doc.yaml b/migration/annotations/effect__printer__Doc.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__Doc.yaml rename to migration/annotations/effect__printer__Doc.yaml diff --git a/repos/effect/migration/annotations/effect__printer__DocStream.yaml b/migration/annotations/effect__printer__DocStream.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__DocStream.yaml rename to migration/annotations/effect__printer__DocStream.yaml diff --git a/repos/effect/migration/annotations/effect__printer__DocTree.yaml b/migration/annotations/effect__printer__DocTree.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__DocTree.yaml rename to migration/annotations/effect__printer__DocTree.yaml diff --git a/repos/effect/migration/annotations/effect__printer__Flatten.yaml b/migration/annotations/effect__printer__Flatten.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__Flatten.yaml rename to migration/annotations/effect__printer__Flatten.yaml diff --git a/repos/effect/migration/annotations/effect__printer__Layout.yaml b/migration/annotations/effect__printer__Layout.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__Layout.yaml rename to migration/annotations/effect__printer__Layout.yaml diff --git a/repos/effect/migration/annotations/effect__printer__Optimize.yaml b/migration/annotations/effect__printer__Optimize.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__Optimize.yaml rename to migration/annotations/effect__printer__Optimize.yaml diff --git a/repos/effect/migration/annotations/effect__printer__PageWidth.yaml b/migration/annotations/effect__printer__PageWidth.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__PageWidth.yaml rename to migration/annotations/effect__printer__PageWidth.yaml diff --git a/repos/effect/migration/annotations/effect__printer__index.yaml b/migration/annotations/effect__printer__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__printer__index.yaml rename to migration/annotations/effect__printer__index.yaml diff --git a/repos/effect/migration/annotations/effect__rpc.yaml b/migration/annotations/effect__rpc.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc.yaml rename to migration/annotations/effect__rpc.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__Rpc.yaml b/migration/annotations/effect__rpc__Rpc.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__Rpc.yaml rename to migration/annotations/effect__rpc__Rpc.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcClient.yaml b/migration/annotations/effect__rpc__RpcClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcClient.yaml rename to migration/annotations/effect__rpc__RpcClient.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcClientError.yaml b/migration/annotations/effect__rpc__RpcClientError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcClientError.yaml rename to migration/annotations/effect__rpc__RpcClientError.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcGroup.yaml b/migration/annotations/effect__rpc__RpcGroup.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcGroup.yaml rename to migration/annotations/effect__rpc__RpcGroup.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcMessage.yaml b/migration/annotations/effect__rpc__RpcMessage.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcMessage.yaml rename to migration/annotations/effect__rpc__RpcMessage.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcMiddleware.yaml b/migration/annotations/effect__rpc__RpcMiddleware.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcMiddleware.yaml rename to migration/annotations/effect__rpc__RpcMiddleware.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcSchema.yaml b/migration/annotations/effect__rpc__RpcSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcSchema.yaml rename to migration/annotations/effect__rpc__RpcSchema.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcSerialization.yaml b/migration/annotations/effect__rpc__RpcSerialization.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcSerialization.yaml rename to migration/annotations/effect__rpc__RpcSerialization.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcServer.yaml b/migration/annotations/effect__rpc__RpcServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcServer.yaml rename to migration/annotations/effect__rpc__RpcServer.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__RpcTest.yaml b/migration/annotations/effect__rpc__RpcTest.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__RpcTest.yaml rename to migration/annotations/effect__rpc__RpcTest.yaml diff --git a/repos/effect/migration/annotations/effect__rpc__index.yaml b/migration/annotations/effect__rpc__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__rpc__index.yaml rename to migration/annotations/effect__rpc__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-clickhouse__ClickhouseClient.yaml b/migration/annotations/effect__sql-clickhouse__ClickhouseClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-clickhouse__ClickhouseClient.yaml rename to migration/annotations/effect__sql-clickhouse__ClickhouseClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml b/migration/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml rename to migration/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml diff --git a/repos/effect/migration/annotations/effect__sql-clickhouse__index.yaml b/migration/annotations/effect__sql-clickhouse__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-clickhouse__index.yaml rename to migration/annotations/effect__sql-clickhouse__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-d1__D1Client.yaml b/migration/annotations/effect__sql-d1__D1Client.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-d1__D1Client.yaml rename to migration/annotations/effect__sql-d1__D1Client.yaml diff --git a/repos/effect/migration/annotations/effect__sql-d1__index.yaml b/migration/annotations/effect__sql-d1__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-d1__index.yaml rename to migration/annotations/effect__sql-d1__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-drizzle__Mysql.yaml b/migration/annotations/effect__sql-drizzle__Mysql.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-drizzle__Mysql.yaml rename to migration/annotations/effect__sql-drizzle__Mysql.yaml diff --git a/repos/effect/migration/annotations/effect__sql-drizzle__Pg.yaml b/migration/annotations/effect__sql-drizzle__Pg.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-drizzle__Pg.yaml rename to migration/annotations/effect__sql-drizzle__Pg.yaml diff --git a/repos/effect/migration/annotations/effect__sql-drizzle__Sqlite.yaml b/migration/annotations/effect__sql-drizzle__Sqlite.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-drizzle__Sqlite.yaml rename to migration/annotations/effect__sql-drizzle__Sqlite.yaml diff --git a/repos/effect/migration/annotations/effect__sql-kysely__Kysely.yaml b/migration/annotations/effect__sql-kysely__Kysely.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-kysely__Kysely.yaml rename to migration/annotations/effect__sql-kysely__Kysely.yaml diff --git a/repos/effect/migration/annotations/effect__sql-kysely__Mssql.yaml b/migration/annotations/effect__sql-kysely__Mssql.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-kysely__Mssql.yaml rename to migration/annotations/effect__sql-kysely__Mssql.yaml diff --git a/repos/effect/migration/annotations/effect__sql-kysely__Mysql.yaml b/migration/annotations/effect__sql-kysely__Mysql.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-kysely__Mysql.yaml rename to migration/annotations/effect__sql-kysely__Mysql.yaml diff --git a/repos/effect/migration/annotations/effect__sql-kysely__Pg.yaml b/migration/annotations/effect__sql-kysely__Pg.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-kysely__Pg.yaml rename to migration/annotations/effect__sql-kysely__Pg.yaml diff --git a/repos/effect/migration/annotations/effect__sql-kysely__Sqlite.yaml b/migration/annotations/effect__sql-kysely__Sqlite.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-kysely__Sqlite.yaml rename to migration/annotations/effect__sql-kysely__Sqlite.yaml diff --git a/repos/effect/migration/annotations/effect__sql-kysely__patch.types.yaml b/migration/annotations/effect__sql-kysely__patch.types.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-kysely__patch.types.yaml rename to migration/annotations/effect__sql-kysely__patch.types.yaml diff --git a/repos/effect/migration/annotations/effect__sql-libsql__index.yaml b/migration/annotations/effect__sql-libsql__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-libsql__index.yaml rename to migration/annotations/effect__sql-libsql__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-mssql__MssqlClient.yaml b/migration/annotations/effect__sql-mssql__MssqlClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-mssql__MssqlClient.yaml rename to migration/annotations/effect__sql-mssql__MssqlClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-mssql__Parameter.yaml b/migration/annotations/effect__sql-mssql__Parameter.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-mssql__Parameter.yaml rename to migration/annotations/effect__sql-mssql__Parameter.yaml diff --git a/repos/effect/migration/annotations/effect__sql-mssql__Procedure.yaml b/migration/annotations/effect__sql-mssql__Procedure.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-mssql__Procedure.yaml rename to migration/annotations/effect__sql-mssql__Procedure.yaml diff --git a/repos/effect/migration/annotations/effect__sql-mssql__index.yaml b/migration/annotations/effect__sql-mssql__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-mssql__index.yaml rename to migration/annotations/effect__sql-mssql__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-mysql2__MysqlClient.yaml b/migration/annotations/effect__sql-mysql2__MysqlClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-mysql2__MysqlClient.yaml rename to migration/annotations/effect__sql-mysql2__MysqlClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-mysql2__index.yaml b/migration/annotations/effect__sql-mysql2__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-mysql2__index.yaml rename to migration/annotations/effect__sql-mysql2__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml b/migration/annotations/effect__sql-pg__PgClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-pg__PgClient.yaml rename to migration/annotations/effect__sql-pg__PgClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-pg__index.yaml b/migration/annotations/effect__sql-pg__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-pg__index.yaml rename to migration/annotations/effect__sql-pg__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-bun__SqliteClient.yaml b/migration/annotations/effect__sql-sqlite-bun__SqliteClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-bun__SqliteClient.yaml rename to migration/annotations/effect__sql-sqlite-bun__SqliteClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-bun__index.yaml b/migration/annotations/effect__sql-sqlite-bun__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-bun__index.yaml rename to migration/annotations/effect__sql-sqlite-bun__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-do__SqliteClient.yaml b/migration/annotations/effect__sql-sqlite-do__SqliteClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-do__SqliteClient.yaml rename to migration/annotations/effect__sql-sqlite-do__SqliteClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-do__index.yaml b/migration/annotations/effect__sql-sqlite-do__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-do__index.yaml rename to migration/annotations/effect__sql-sqlite-do__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-node__SqliteClient.yaml b/migration/annotations/effect__sql-sqlite-node__SqliteClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-node__SqliteClient.yaml rename to migration/annotations/effect__sql-sqlite-node__SqliteClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-node__index.yaml b/migration/annotations/effect__sql-sqlite-node__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-node__index.yaml rename to migration/annotations/effect__sql-sqlite-node__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml b/migration/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml rename to migration/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-react-native__index.yaml b/migration/annotations/effect__sql-sqlite-react-native__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-react-native__index.yaml rename to migration/annotations/effect__sql-sqlite-react-native__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml b/migration/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml rename to migration/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml diff --git a/repos/effect/migration/annotations/effect__sql-sqlite-wasm__index.yaml b/migration/annotations/effect__sql-sqlite-wasm__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql-sqlite-wasm__index.yaml rename to migration/annotations/effect__sql-sqlite-wasm__index.yaml diff --git a/repos/effect/migration/annotations/effect__sql.yaml b/migration/annotations/effect__sql.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql.yaml rename to migration/annotations/effect__sql.yaml diff --git a/repos/effect/migration/annotations/effect__sql__Migrator.yaml b/migration/annotations/effect__sql__Migrator.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__Migrator.yaml rename to migration/annotations/effect__sql__Migrator.yaml diff --git a/repos/effect/migration/annotations/effect__sql__Migrator__FileSystem.yaml b/migration/annotations/effect__sql__Migrator__FileSystem.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__Migrator__FileSystem.yaml rename to migration/annotations/effect__sql__Migrator__FileSystem.yaml diff --git a/repos/effect/migration/annotations/effect__sql__Model.yaml b/migration/annotations/effect__sql__Model.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__Model.yaml rename to migration/annotations/effect__sql__Model.yaml diff --git a/migration/annotations/effect__sql__SqlClient.yaml b/migration/annotations/effect__sql__SqlClient.yaml new file mode 100644 index 000000000..505aeeff7 --- /dev/null +++ b/migration/annotations/effect__sql__SqlClient.yaml @@ -0,0 +1,23 @@ +"@effect/sql/SqlClient#make": + replacement: "effect/sql/SqlClient#make" + note: "Moved; custom clients must supply MakeOptions.driver, rename reactiveMailbox to reactiveQueue, and may supply transactionService. Driver names conventionally omit the @effect/sql- prefix, for example pg or pglite." +"@effect/sql/SqlClient#SafeIntegers": + replacement: "effect/sql/SqlClient#SafeIntegers" + note: "Moved and changed from a Reference subclass to a Context.Reference value; provide it as a service." +"@effect/sql/SqlClient#SqlClient": + include: true + replacement: "effect/sql/SqlClient#SqlClient" + note: "Moved into core Effect. Custom service implementations must expose driver and afterCommit; SqlClient.make supplies both. driver identifies the driver independently of its SQL dialect. afterCommit registers effects to run after the outermost successful commit and connection release attempt, discarding rolled-back work. withTransaction now also defers contextual Reactivity.invalidate and mutation invalidations until that commit. Transactions bypassing makeWithTransaction, such as sqlite-do transactions, run callbacks and invalidations immediately." +"@effect/sql/SqlClient#makeWithTransaction": + include: true + replacement: "effect/sql/SqlClient#makeWithTransaction" + note: "Rename transactionTag to transactionService. acquireConnection is now a function receiving restore and returning Effect, rather than an effect returning a scope/connection pair. Register releases in the provided scope and wrap interruptible connection waits with restore; the wrapper closes the scope on transaction completion or failed acquisition." +"@effect/sql/SqlClient#SqlClient.MakeOptions": + replacement: "effect/sql/SqlClient#SqlClient.MakeOptions" + note: "Moved; driver is now required and conventionally names the driver package without @effect/sql-, for example pg or pglite. Rename reactiveMailbox to reactiveQueue; transactionService is optional." +"@effect/sql/SqlClient#TransactionConnection": + replacement: "effect/sql/SqlClient#TransactionConnection" + note: "Now a factory keyed by client id, not a singleton tag. Prefer the client's transactionService; the payload type is TransactionConnection.Service." +"@effect/sql/SqlClient#TypeId": + replacement: "none" + note: "The brand is private in v4; do not inspect or attach it, and obtain clients through SqlClient or SqlClient.make." diff --git a/repos/effect/migration/annotations/effect__sql__SqlConnection.yaml b/migration/annotations/effect__sql__SqlConnection.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__SqlConnection.yaml rename to migration/annotations/effect__sql__SqlConnection.yaml diff --git a/repos/effect/migration/annotations/effect__sql__SqlError.yaml b/migration/annotations/effect__sql__SqlError.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__SqlError.yaml rename to migration/annotations/effect__sql__SqlError.yaml diff --git a/repos/effect/migration/annotations/effect__sql__SqlEventJournal.yaml b/migration/annotations/effect__sql__SqlEventJournal.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__SqlEventJournal.yaml rename to migration/annotations/effect__sql__SqlEventJournal.yaml diff --git a/migration/annotations/effect__sql__SqlEventLogServer.yaml b/migration/annotations/effect__sql__SqlEventLogServer.yaml new file mode 100644 index 000000000..c1d5c7c27 --- /dev/null +++ b/migration/annotations/effect__sql__SqlEventLogServer.yaml @@ -0,0 +1,12 @@ +"@effect/sql/SqlEventLogServer#layerStorage": + include: true + replacement: "effect/eventlog/SqlEventLogServerEncrypted#layerStorage" + note: "Moved to the encrypted server module; remove insertBatchSize. entryTablePrefix and remoteIdTable remain, as does the EventLogEncryption requirement. Storage uses encrypted batch tables and entry-id lookup tables, not the old per-entry ciphertext schema; existing encrypted tables are not automatically converted." +"@effect/sql/SqlEventLogServer#makeStorage": + include: true + replacement: "effect/eventlog/SqlEventLogServerEncrypted#makeStorage" + note: "Moved to the encrypted server module; remove insertBatchSize. SQL, encryption, and scope requirements remain. Storage uses encrypted batch tables and entry-id lookup tables, not the old per-entry ciphertext schema; existing encrypted tables are not automatically converted." +"@effect/sql/SqlEventLogServer#layerStorageSubtle": + include: true + replacement: "effect/eventlog/SqlEventLogServerEncrypted#layerStorageSubtle" + note: "Moved to the encrypted server module; remove insertBatchSize. The layer still supplies Web Crypto encryption. Storage now uses encrypted batch tables and entry-id lookup tables; existing per-entry ciphertext tables are not automatically converted." diff --git a/repos/effect/migration/annotations/effect__sql__SqlPersistedQueue.yaml b/migration/annotations/effect__sql__SqlPersistedQueue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__SqlPersistedQueue.yaml rename to migration/annotations/effect__sql__SqlPersistedQueue.yaml diff --git a/repos/effect/migration/annotations/effect__sql__SqlResolver.yaml b/migration/annotations/effect__sql__SqlResolver.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__SqlResolver.yaml rename to migration/annotations/effect__sql__SqlResolver.yaml diff --git a/repos/effect/migration/annotations/effect__sql__SqlSchema.yaml b/migration/annotations/effect__sql__SqlSchema.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__SqlSchema.yaml rename to migration/annotations/effect__sql__SqlSchema.yaml diff --git a/repos/effect/migration/annotations/effect__sql__Statement.yaml b/migration/annotations/effect__sql__Statement.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__Statement.yaml rename to migration/annotations/effect__sql__Statement.yaml diff --git a/repos/effect/migration/annotations/effect__sql__index.yaml b/migration/annotations/effect__sql__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__sql__index.yaml rename to migration/annotations/effect__sql__index.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass.yaml b/migration/annotations/effect__typeclass.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass.yaml rename to migration/annotations/effect__typeclass.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Alternative.yaml b/migration/annotations/effect__typeclass__Alternative.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Alternative.yaml rename to migration/annotations/effect__typeclass__Alternative.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Applicative.yaml b/migration/annotations/effect__typeclass__Applicative.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Applicative.yaml rename to migration/annotations/effect__typeclass__Applicative.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Bicovariant.yaml b/migration/annotations/effect__typeclass__Bicovariant.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Bicovariant.yaml rename to migration/annotations/effect__typeclass__Bicovariant.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Bounded.yaml b/migration/annotations/effect__typeclass__Bounded.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Bounded.yaml rename to migration/annotations/effect__typeclass__Bounded.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Chainable.yaml b/migration/annotations/effect__typeclass__Chainable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Chainable.yaml rename to migration/annotations/effect__typeclass__Chainable.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Contravariant.yaml b/migration/annotations/effect__typeclass__Contravariant.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Contravariant.yaml rename to migration/annotations/effect__typeclass__Contravariant.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Coproduct.yaml b/migration/annotations/effect__typeclass__Coproduct.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Coproduct.yaml rename to migration/annotations/effect__typeclass__Coproduct.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Covariant.yaml b/migration/annotations/effect__typeclass__Covariant.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Covariant.yaml rename to migration/annotations/effect__typeclass__Covariant.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Filterable.yaml b/migration/annotations/effect__typeclass__Filterable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Filterable.yaml rename to migration/annotations/effect__typeclass__Filterable.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__FlatMap.yaml b/migration/annotations/effect__typeclass__FlatMap.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__FlatMap.yaml rename to migration/annotations/effect__typeclass__FlatMap.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Foldable.yaml b/migration/annotations/effect__typeclass__Foldable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Foldable.yaml rename to migration/annotations/effect__typeclass__Foldable.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Invariant.yaml b/migration/annotations/effect__typeclass__Invariant.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Invariant.yaml rename to migration/annotations/effect__typeclass__Invariant.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Monad.yaml b/migration/annotations/effect__typeclass__Monad.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Monad.yaml rename to migration/annotations/effect__typeclass__Monad.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Monoid.yaml b/migration/annotations/effect__typeclass__Monoid.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Monoid.yaml rename to migration/annotations/effect__typeclass__Monoid.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Of.yaml b/migration/annotations/effect__typeclass__Of.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Of.yaml rename to migration/annotations/effect__typeclass__Of.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Pointed.yaml b/migration/annotations/effect__typeclass__Pointed.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Pointed.yaml rename to migration/annotations/effect__typeclass__Pointed.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Product.yaml b/migration/annotations/effect__typeclass__Product.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Product.yaml rename to migration/annotations/effect__typeclass__Product.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__SemiAlternative.yaml b/migration/annotations/effect__typeclass__SemiAlternative.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__SemiAlternative.yaml rename to migration/annotations/effect__typeclass__SemiAlternative.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__SemiApplicative.yaml b/migration/annotations/effect__typeclass__SemiApplicative.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__SemiApplicative.yaml rename to migration/annotations/effect__typeclass__SemiApplicative.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__SemiCoproduct.yaml b/migration/annotations/effect__typeclass__SemiCoproduct.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__SemiCoproduct.yaml rename to migration/annotations/effect__typeclass__SemiCoproduct.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__SemiProduct.yaml b/migration/annotations/effect__typeclass__SemiProduct.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__SemiProduct.yaml rename to migration/annotations/effect__typeclass__SemiProduct.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Semigroup.yaml b/migration/annotations/effect__typeclass__Semigroup.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Semigroup.yaml rename to migration/annotations/effect__typeclass__Semigroup.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__Traversable.yaml b/migration/annotations/effect__typeclass__Traversable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__Traversable.yaml rename to migration/annotations/effect__typeclass__Traversable.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__TraversableFilterable.yaml b/migration/annotations/effect__typeclass__TraversableFilterable.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__TraversableFilterable.yaml rename to migration/annotations/effect__typeclass__TraversableFilterable.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Array.yaml b/migration/annotations/effect__typeclass__data__Array.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Array.yaml rename to migration/annotations/effect__typeclass__data__Array.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__BigInt.yaml b/migration/annotations/effect__typeclass__data__BigInt.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__BigInt.yaml rename to migration/annotations/effect__typeclass__data__BigInt.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Boolean.yaml b/migration/annotations/effect__typeclass__data__Boolean.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Boolean.yaml rename to migration/annotations/effect__typeclass__data__Boolean.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Duration.yaml b/migration/annotations/effect__typeclass__data__Duration.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Duration.yaml rename to migration/annotations/effect__typeclass__data__Duration.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Effect.yaml b/migration/annotations/effect__typeclass__data__Effect.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Effect.yaml rename to migration/annotations/effect__typeclass__data__Effect.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Either.yaml b/migration/annotations/effect__typeclass__data__Either.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Either.yaml rename to migration/annotations/effect__typeclass__data__Either.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Identity.yaml b/migration/annotations/effect__typeclass__data__Identity.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Identity.yaml rename to migration/annotations/effect__typeclass__data__Identity.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Micro.yaml b/migration/annotations/effect__typeclass__data__Micro.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Micro.yaml rename to migration/annotations/effect__typeclass__data__Micro.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Number.yaml b/migration/annotations/effect__typeclass__data__Number.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Number.yaml rename to migration/annotations/effect__typeclass__data__Number.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Option.yaml b/migration/annotations/effect__typeclass__data__Option.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Option.yaml rename to migration/annotations/effect__typeclass__data__Option.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Ordering.yaml b/migration/annotations/effect__typeclass__data__Ordering.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Ordering.yaml rename to migration/annotations/effect__typeclass__data__Ordering.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Predicate.yaml b/migration/annotations/effect__typeclass__data__Predicate.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Predicate.yaml rename to migration/annotations/effect__typeclass__data__Predicate.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Record.yaml b/migration/annotations/effect__typeclass__data__Record.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Record.yaml rename to migration/annotations/effect__typeclass__data__Record.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__String.yaml b/migration/annotations/effect__typeclass__data__String.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__String.yaml rename to migration/annotations/effect__typeclass__data__String.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__data__Tuple.yaml b/migration/annotations/effect__typeclass__data__Tuple.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__data__Tuple.yaml rename to migration/annotations/effect__typeclass__data__Tuple.yaml diff --git a/repos/effect/migration/annotations/effect__typeclass__index.yaml b/migration/annotations/effect__typeclass__index.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__typeclass__index.yaml rename to migration/annotations/effect__typeclass__index.yaml diff --git a/repos/effect/migration/annotations/effect__vitest__index.yaml b/migration/annotations/effect__vitest__index.yaml similarity index 96% rename from repos/effect/migration/annotations/effect__vitest__index.yaml rename to migration/annotations/effect__vitest__index.yaml index 515c9201a..5db5f2cc0 100644 --- a/repos/effect/migration/annotations/effect__vitest__index.yaml +++ b/migration/annotations/effect__vitest__index.yaml @@ -269,8 +269,13 @@ 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": + include: true 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." + note: "Vitest 5 removes it.sequential; pass { concurrent: false } to the native test or as the Effect helper's third argument. Synchronous it.prop callbacks must return boolean or void: true and undefined pass, false falsifies, and other values (including Effects and Promises) throw TypeError. Use it.effect.prop for Effect callbacks." +"@effect/vitest/index#prop": + include: true + replacement: "@effect/vitest#prop" + note: "Synchronous property callbacks must return boolean or void: true and undefined pass, false falsifies, and other values (including Effects and Promises) throw TypeError. Move Effect callbacks to it.effect.prop instead of returning an Effect from prop." "@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." diff --git a/repos/effect/migration/annotations/effect__vitest__utils.yaml b/migration/annotations/effect__vitest__utils.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__vitest__utils.yaml rename to migration/annotations/effect__vitest__utils.yaml diff --git a/repos/effect/migration/annotations/effect__workflow.yaml b/migration/annotations/effect__workflow.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow.yaml rename to migration/annotations/effect__workflow.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__Activity.yaml b/migration/annotations/effect__workflow__Activity.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__Activity.yaml rename to migration/annotations/effect__workflow__Activity.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__DurableClock.yaml b/migration/annotations/effect__workflow__DurableClock.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__DurableClock.yaml rename to migration/annotations/effect__workflow__DurableClock.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml b/migration/annotations/effect__workflow__DurableDeferred.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__DurableDeferred.yaml rename to migration/annotations/effect__workflow__DurableDeferred.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__DurableQueue.yaml b/migration/annotations/effect__workflow__DurableQueue.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__DurableQueue.yaml rename to migration/annotations/effect__workflow__DurableQueue.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__DurableRateLimiter.yaml b/migration/annotations/effect__workflow__DurableRateLimiter.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__DurableRateLimiter.yaml rename to migration/annotations/effect__workflow__DurableRateLimiter.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__Workflow.yaml b/migration/annotations/effect__workflow__Workflow.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__Workflow.yaml rename to migration/annotations/effect__workflow__Workflow.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__WorkflowEngine.yaml b/migration/annotations/effect__workflow__WorkflowEngine.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__WorkflowEngine.yaml rename to migration/annotations/effect__workflow__WorkflowEngine.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__WorkflowProxy.yaml b/migration/annotations/effect__workflow__WorkflowProxy.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__WorkflowProxy.yaml rename to migration/annotations/effect__workflow__WorkflowProxy.yaml diff --git a/repos/effect/migration/annotations/effect__workflow__WorkflowProxyServer.yaml b/migration/annotations/effect__workflow__WorkflowProxyServer.yaml similarity index 100% rename from repos/effect/migration/annotations/effect__workflow__WorkflowProxyServer.yaml rename to migration/annotations/effect__workflow__WorkflowProxyServer.yaml diff --git a/repos/effect/migration/cause.md b/migration/cause.md similarity index 100% rename from repos/effect/migration/cause.md rename to migration/cause.md diff --git a/repos/effect/migration/equality.md b/migration/equality.md similarity index 100% rename from repos/effect/migration/equality.md rename to migration/equality.md diff --git a/migration/error-handling.md b/migration/error-handling.md new file mode 100644 index 000000000..4638fc9a9 --- /dev/null +++ b/migration/error-handling.md @@ -0,0 +1,173 @@ +# Error Handling: `catch*` Renamings + +The `catch` combinators on `Effect` have been renamed in v4. The general +pattern: `catchAll*` is shortened to `catch*`, and the `catchSome*` family is +replaced by `catchFilter` / `catchCauseFilter`. + +## Renamings + +| v3 | v4 | +| ------------------------ | ------------------------------ | +| `Effect.catchAll` | `Effect.catch` | +| `Effect.catchAllCause` | `Effect.catchCause` | +| `Effect.catchAllDefect` | `Effect.catchDefect` | +| `Effect.catchTag` | `Effect.catchTag` (unchanged) | +| `Effect.catchTags` | `Effect.catchTags` (unchanged) | +| `Effect.catchIf` | `Effect.catchIf` (unchanged) | +| `Effect.catchSome` | `Effect.catchFilter` | +| `Effect.catchSomeCause` | `Effect.catchCauseFilter` | +| `Effect.catchSomeDefect` | Removed | + +## `Effect.catchAll` → `Effect.catch` + +**v3** + +```ts +import { Effect } from "effect" + +const program = Effect.fail("error").pipe( + Effect.catchAll((error) => Effect.succeed(`recovered: ${error}`)) +) +``` + +**v4** + +```ts +import { Effect } from "effect" + +const program = Effect.fail("error").pipe( + Effect.catch((error) => Effect.succeed(`recovered: ${error}`)) +) +``` + +## `Effect.catchAllCause` → `Effect.catchCause` + +**v3** + +```ts +import { Effect } from "effect" + +const program = Effect.die("defect").pipe( + Effect.catchAllCause((cause) => Effect.succeed("recovered")) +) +``` + +**v4** + +```ts +import { Cause, Effect } from "effect" + +const program = Effect.die("defect").pipe( + Effect.catchCause((cause) => Effect.succeed("recovered")) +) +``` + +## `Effect.catchSome` → `Effect.catchFilter` + +In v3, `catchSome` took a function returning `Option`. In v4, +`catchFilter` uses the `Filter` module instead. + +**v3** + +```ts +import { Effect, Option } from "effect" + +const program = Effect.fail(42).pipe( + Effect.catchSome((error) => + error === 42 + ? Option.some(Effect.succeed("caught")) + : Option.none() + ) +) +``` + +**v4** + +```ts +import { Effect, Filter } from "effect" + +const program = Effect.fail(42).pipe( + Effect.catchFilter( + Filter.fromPredicate((error: number) => error === 42), + (error) => Effect.succeed("caught") + ) +) +``` + +## Defects take precedence in mixed causes + +A failure cause can hold a typed error and a defect at the same time, for +example when a finalizer dies after the effect has already failed. In v4, +every operator that recovers from typed errors skips such causes. The handler +does not run, and the failure is propagated with each typed error converted to +a defect. + +In v3 only some operators behaved this way (`catchAll`, `matchEffect`, +`match`, `orElse`, `orElseSucceed`, `ignore`, `either`, `option` and +`Effect.retry`). `catchTag`, `catchTags`, `catchIf`, `catchSome` and the +`Stream` and `Channel` recovery operators ran the handler and dropped the +defect. In v4 the rule is the same for all of them: + +- `Effect`: `catch`, `catchTag`, `catchTags`, `catchIf`, `catchFilter`, + `catchReason`, `catchReasons`, `catchEager`, `matchEffect`, `match`, + `matchEager`, `orElseSucceed`, `ignore`, `option`, `result`, `orDie`, + `eventually`, `firstSuccessOf`, `retry`, `retryOrElse`, `repeat` and + `repeatOrElse` +- `Stream` and `Channel`: their typed recovery operators, including `catch`, + `catchTag`, `catchIf`, `catchFilter`, `catchReason`, `catchReasons`, + `ignore`, `orDie` and `retry` + +An interruption alongside a typed error does not prevent recovery. The first +typed error is handled; other typed errors and interrupts can be discarded. + +```ts +import { Effect } from "effect" + +const program = Effect.fail("error").pipe( + Effect.ensuring(Effect.die("finalizer defect")), + // The handler does not run: the cause contains a defect + Effect.catch(() => Effect.succeed("recovered")) +) +// fails with Die("error") + Die("finalizer defect") +``` + +Use `Effect.catchDefect` or `Effect.catchCause` to recover from causes that +contain defects. + +### Retry and repeat error paths + +`Effect.retry`, `retryOrElse`, `repeat` and `repeatOrElse` apply this rule to +source failures. `Fail(e) + Interrupt` now enters the typed error path instead +of propagating unchanged. `retry` retries it; when retries run out, or `repeat` +re-raises it, the final cause is `Fail(e)` without the interrupt. The `orElse` +handlers can now receive `e` from these causes. + +`repeat` and `repeatOrElse` also apply the rule to policy failures. A policy +failure containing `Fail(e) + Die(d)` propagates as `Die(e) + Die(d)` without +running `orElse`. In contrast, `retry` and `retryOrElse` do not recover policy +errors, so policy failures keep their original cause. + +## Error mapping preserves the whole cause + +Error mapping is not recovery. `Effect.mapError`, `mapErrorEager`, `mapBoth` +and `mapBothEager`, `Exit.mapError` and `mapBoth`, `Stream.mapError` and +`mapBoth`, and `Channel.mapError` now map every typed error in place while +preserving defects, interrupts and reason order. Eager and suspended inputs +follow the same rule. For example, `Fail(e) + Interrupt + Die(d)` becomes +`Fail(f(e)) + Interrupt + Die(d)`, not an elected defect cause. + +Previously these operators mapped only the first typed error and discarded +the remaining reasons. If a cause contains several typed errors, the mapper +now runs for each one. Unlike typed recovery, mapping neither skips errors +beside defects nor converts them to defects. + +## New in v4 + +- **`Effect.catchReason(errorTag, reasonTag, handler)`** — catches a specific + `reason` within a tagged error without removing the parent error from the + error channel. Useful for handling nested error causes (e.g. an `AiError` + with a `reason: RateLimitError | QuotaExceededError`). +- **`Effect.catchReasons(errorTag, cases)`** — like `catchReason` but handles + multiple reason tags at once via an object of handlers. +- **`Effect.catchEager(handler)`** — an optimization variant of `catch` that + evaluates synchronous recovery effects immediately. diff --git a/repos/effect/migration/fiber-keep-alive.md b/migration/fiber-keep-alive.md similarity index 100% rename from repos/effect/migration/fiber-keep-alive.md rename to migration/fiber-keep-alive.md diff --git a/repos/effect/migration/fiberref.md b/migration/fiberref.md similarity index 100% rename from repos/effect/migration/fiberref.md rename to migration/fiberref.md diff --git a/repos/effect/migration/forking.md b/migration/forking.md similarity index 100% rename from repos/effect/migration/forking.md rename to migration/forking.md diff --git a/repos/effect/migration/generators.md b/migration/generators.md similarity index 100% rename from repos/effect/migration/generators.md rename to migration/generators.md diff --git a/repos/effect/migration/layer-memoization.md b/migration/layer-memoization.md similarity index 58% rename from repos/effect/migration/layer-memoization.md rename to migration/layer-memoization.md index a67cc6e74..7455b46ae 100644 --- a/repos/effect/migration/layer-memoization.md +++ b/migration/layer-memoization.md @@ -5,10 +5,12 @@ were memoized / deduplicated within a single `Effect.provide` call, but would **not** be shared across separate calls — so two `Effect.provide` calls with overlapping layers would silently build those layers twice. -In v4, the underlying `MemoMap` data structure which facilitates memoization of -`Layer`s is shared between `Effect.provide` calls (unless explicitly disabled -via the `{ local: true }` option). Thus, layers are automatically memoized / -deduplicated across `Effect.provide` calls. +In v4, a layer already built by an enclosing `Effect.provide` or runtime is +reused by nested `Effect.provide` calls. Each call forks the ambient `MemoMap`: it +can reuse parent entries, but newly built layers are not written back to the +parent. Sibling `Effect.provide` calls therefore build their own copies, just as +in v3. Use `{ local: true }` to build a fresh layer subtree without reusing the +enclosing memo map. ## Example @@ -30,7 +32,7 @@ const program = Effect.gen(function*() { return a.value }) -// Same layer provided twice in separate provide calls +// Same layer provided twice in nested provide calls const main = program.pipe( Effect.provide(MyServiceLayer), Effect.provide(MyServiceLayer) @@ -41,10 +43,23 @@ const main = program.pipe( Effect.runPromise(main) ``` -## Prefer Layer Composition Over Multipl Provides +The calls above are nested, not siblings. In contrast, the following builds +`MyServiceLayer` twice in v4 when no enclosing provide or runtime has already +built it: -Even though v4 memoizes across `provide` calls, **composing layers before -providing is still the recommended pattern**. Layer composition makes your +```ts +const siblings = Effect.gen(function*() { + yield* program.pipe(Effect.provide(MyServiceLayer)) + yield* program.pipe(Effect.provide(MyServiceLayer)) +}) + +Effect.runPromise(siblings) // "Building MyService" is logged TWICE +``` + +## Prefer Layer Composition Over Multiple Provides + +Even though nested v4 `provide` calls reuse already-built layers, **composing +layers before providing is still the recommended pattern**. Layer composition makes your dependency graph explicit and lets you see the full structure in one place: ```ts @@ -52,9 +67,8 @@ dependency graph explicit and lets you see the full structure in one place: const main = program.pipe(Effect.provide(MyServiceLayer)) ``` -The auto-memoization feature is a safety net to avoid the footguns associated -with multiple `Effect.provide` calls present in v3. It is **NOT** a substitute -for proper layer composition. +Reusing enclosing layers is **not** a substitute for proper layer composition +and does not deduplicate independent sibling builds. ## Opting Out of Shared Memoization @@ -80,15 +94,16 @@ const main = program.pipe( ### `Effect.provide` with `{ local: true }` New in v4. Builds the provided layer with a **local memo map** instead of the -fiber's shared one. The layer and all its sublayers are built from scratch and -are not shared with other `provide` calls. +fiber's ambient one. The layer and all its sublayers are built from scratch, +without reusing layers built by an enclosing `provide` or runtime. Nested +`provide` calls can still reuse layers built in this local map. ```ts import { Effect } from "effect" const main = program.pipe( - Effect.provide(MyServiceLayer), - Effect.provide(MyServiceLayer, { local: true }) + Effect.provide(MyServiceLayer, { local: true }), + Effect.provide(MyServiceLayer) ) // "Building MyService" is logged TWICE — local creates its own memo map ``` diff --git a/repos/effect/migration/runtime.md b/migration/runtime.md similarity index 100% rename from repos/effect/migration/runtime.md rename to migration/runtime.md diff --git a/repos/effect/migration/schema.md b/migration/schema.md similarity index 99% rename from repos/effect/migration/schema.md rename to migration/schema.md index 8f7aec0e6..f1164138f 100644 --- a/repos/effect/migration/schema.md +++ b/migration/schema.md @@ -897,17 +897,16 @@ const NumberFromString = Schema.transformOrFail(Schema.String, Schema.Number, { v4 ```ts -import { Effect, Number, Schema, SchemaGetter, SchemaIssue } from "effect" +import { Effect, Number, Option, Schema, SchemaGetter, SchemaIssue } from "effect" const NumberFromString = Schema.String.pipe( Schema.decodeTo(Schema.Number, { - decode: SchemaGetter.transformEffect((s) => { - const n = Number.parse(s) - if (n === undefined) { - return Effect.fail(new SchemaIssue.InvalidValue()) - } - return Effect.succeed(n) - }), + decode: SchemaGetter.transformEffect((s) => + Option.match(Number.parse(s), { + onNone: () => Effect.fail(new SchemaIssue.InvalidValue()), + onSome: Effect.succeed + }) + ), encode: SchemaGetter.String() }) ) diff --git a/repos/effect/migration/scope.md b/migration/scope.md similarity index 100% rename from repos/effect/migration/scope.md rename to migration/scope.md diff --git a/repos/effect/migration/services.md b/migration/services.md similarity index 100% rename from repos/effect/migration/services.md rename to migration/services.md diff --git a/repos/effect/migration/v3-to-v4.md b/migration/v3-to-v4.md similarity index 97% rename from repos/effect/migration/v3-to-v4.md rename to migration/v3-to-v4.md index 0adea8d81..0993e3b03 100644 --- a/repos/effect/migration/v3-to-v4.md +++ b/migration/v3-to-v4.md @@ -2,9 +2,9 @@ # v3 to v4 Migration Reference -Base: `e730f59d29db5f1e312be6476a95d07bcad45788` (`e730f59d29db5f1e312be6476a95d07bcad45788`) +Base: `origin/v3` (`cab7f2e3493e4a3dceda27cb1b6d28c4599cb939`) -Head: `origin/main` (`7547a99f81fa6f7e651d52f3d42081f399bfbca8`) +Head: `origin/main` (`4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d`) This file is generated from the API diff and `migration/annotations/*.yaml`. @@ -1528,7 +1528,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ActiveStatusType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.AddUploadPartRequest` -> `Generated.AddUploadPartRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. +- `Generated.AddUploadPartRequest` -> `Generated.AddUploadPartRequestMultipart`: Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape. - `Generated.AdminApiKey` -> `Generated.AdminApiKey`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1544,13 +1544,13 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ApiKeyList` -> `Generated.ApiKeyList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.ApplyPatchCallOutputStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ApplyPatchCallOutputStatus` -> `Generated.ApplyPatchCallOutputStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ApplyPatchCallOutputStatusParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ApplyPatchCallOutputStatusParam` -> `Generated.ApplyPatchCallOutputStatusParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ApplyPatchCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ApplyPatchCallStatus` -> `Generated.ApplyPatchCallStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ApplyPatchCallStatusParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ApplyPatchCallStatusParam` -> `Generated.ApplyPatchCallStatusParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ApplyPatchCreateFileOperation` -> `Generated.ApplyPatchCreateFileOperation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1568,7 +1568,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ApplyPatchDeleteFileOperationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.ApplyPatchOperationParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ApplyPatchOperationParam` -> `Generated.ApplyPatchOperationParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ApplyPatchToolCall` -> `Generated.ApplyPatchToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1598,7 +1598,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ApplyPatchUpdateFileOperationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.ApproximateLocation` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ApproximateLocation` -> `Generated.ApproximateLocation`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ApproximateLocationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -1644,11 +1644,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.Attachment` -> `Generated.Attachment`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.AttachmentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.AttachmentType` -> `Generated.AttachmentType`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.AudioResponseFormat` -> `Generated.AudioResponseFormat`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.AudioTranscription` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.AudioTranscription` -> `Generated.AudioTranscription`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.AudioTranscriptionModel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -1670,15 +1670,15 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.AuditLogEventType` -> `Generated.AuditLogEventType`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.AutoChunkingStrategyRequestParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.AutoChunkingStrategyRequestParam` -> `Generated.AutoChunkingStrategyRequestParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.AutoChunkingStrategyRequestParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.AutomaticThreadTitlingParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.AutomaticThreadTitlingParam` -> `Generated.AutomaticThreadTitlingParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.Batch` -> `Generated.Batch`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.BatchError` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.BatchError` -> `Generated.BatchError`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.BatchFileExpirationAfter` -> `Generated.BatchFileExpirationAfter`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1686,7 +1686,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.BatchObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.BatchRequestCounts` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.BatchRequestCounts` -> `Generated.BatchRequestCounts`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.BatchStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -1812,33 +1812,33 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ChatModel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.ChatSessionAutomaticThreadTitling` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatSessionAutomaticThreadTitling` -> `Generated.ChatSessionAutomaticThreadTitling`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ChatSessionChatkitConfiguration` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatSessionChatkitConfiguration` -> `Generated.ChatSessionChatkitConfiguration`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ChatSessionFileUpload` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatSessionFileUpload` -> `Generated.ChatSessionFileUpload`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ChatSessionHistory` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatSessionHistory` -> `Generated.ChatSessionHistory`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ChatSessionRateLimits` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatSessionRateLimits` -> `Generated.ChatSessionRateLimits`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ChatSessionResource` -> `Generated.ChatSessionResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. - `Generated.ChatSessionResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.ChatSessionStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatSessionStatus` -> `Generated.ChatSessionStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ChatkitConfigurationParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatkitConfigurationParam` -> `Generated.ChatkitConfigurationParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ChatkitWorkflow` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatkitWorkflow` -> `Generated.ChatkitWorkflow`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.ChatkitWorkflowTracing` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ChatkitWorkflowTracing` -> `Generated.ChatkitWorkflowTracing`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ChunkingStrategyRequestParam` -> `Generated.ChunkingStrategyRequestParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. - `Generated.ChunkingStrategyResponse` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.ClickButtonType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ClickButtonType` -> `Generated.ClickButtonType`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ClickParam` -> `Generated.ClickParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1854,7 +1854,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ClientToolCallItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.ClientToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ClientToolCallStatus` -> `Generated.ClientToolCallStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ClosedStatus` -> `Generated.ClosedStatus`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1904,7 +1904,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ComputerCallSafetyCheckParam` -> `Generated.ComputerCallSafetyCheckParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.ComputerEnvironment` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ComputerEnvironment` -> `Generated.ComputerEnvironment`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ComputerScreenshotContent` -> `Generated.ComputerScreenshotContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1940,7 +1940,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ContainerListResource` -> `Generated.ContainerListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.ContainerMemoryLimit` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ContainerMemoryLimit` -> `Generated.ContainerMemoryLimit`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ContainerResource` -> `Generated.ContainerResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -1966,7 +1966,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateAssistantRequest` -> `Generated.CreateAssistantRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.CreateBatchRequest` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.CreateBatchRequest` -> `Generated.CreateBatchRequest`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.CreateBatchRequestCompletionWindow` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2018,19 +2018,19 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateEmbeddingResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.CreateEvalCompletionsRunDataSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.CreateEvalCompletionsRunDataSource` -> `Generated.CreateEvalCompletionsRunDataSource`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.CreateEvalCompletionsRunDataSourceInputMessagesEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. - `Generated.CreateEvalCompletionsRunDataSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.CreateEvalCustomDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.CreateEvalCustomDataSourceConfig` -> `Generated.CreateEvalCustomDataSourceConfig`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.CreateEvalCustomDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. - `Generated.CreateEvalItem` -> `Generated.CreateEvalItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.CreateEvalJsonlRunDataSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.CreateEvalJsonlRunDataSource` -> `Generated.CreateEvalJsonlRunDataSource`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.CreateEvalJsonlRunDataSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2038,13 +2038,13 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateEvalLabelModelGraderType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.CreateEvalLogsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.CreateEvalLogsDataSourceConfig` -> `Generated.CreateEvalLogsDataSourceConfig`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.CreateEvalLogsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. - `Generated.CreateEvalRequest` -> `Generated.CreateEvalRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.CreateEvalResponsesRunDataSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.CreateEvalResponsesRunDataSource` -> `Generated.CreateEvalResponsesRunDataSource`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.CreateEvalResponsesRunDataSourceInputMessagesEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2052,11 +2052,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateEvalRunRequest` -> `Generated.CreateEvalRunRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.CreateEvalStoredCompletionsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.CreateEvalStoredCompletionsDataSourceConfig` -> `Generated.CreateEvalStoredCompletionsDataSourceConfig`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.CreateEvalStoredCompletionsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.CreateFileRequest` -> `Generated.CreateFileRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. +- `Generated.CreateFileRequest` -> `Generated.CreateFileRequestMultipart`: Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape. - `Generated.CreateFineTuningCheckpointPermissionRequest` -> `Generated.CreateFineTuningCheckpointPermissionRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -2074,7 +2074,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateGroupUserBody` -> `Generated.CreateGroupUserBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.CreateImageEditRequest` -> `Generated.CreateImageEditRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. +- `Generated.CreateImageEditRequest` -> `Generated.CreateImageEditRequestMultipart`: Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape. JSON image edits instead use Generated.CreateImageEditRequestJson. - `Generated.CreateImageEditRequestBackground` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2106,7 +2106,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateImageRequestStyle` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.CreateImageVariationRequest` -> `Generated.CreateImageVariationRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. +- `Generated.CreateImageVariationRequest` -> `Generated.CreateImageVariationRequestMultipart`: Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape. - `Generated.CreateImageVariationRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2166,7 +2166,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateTranscription200` -> `Generated.CreateTranscription200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.CreateTranscriptionRequest` -> `Generated.CreateTranscriptionRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. +- `Generated.CreateTranscriptionRequest` -> `Generated.CreateTranscriptionRequestMultipart`: Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape. - `Generated.CreateTranscriptionRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2180,7 +2180,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.CreateTranslation200` -> `Generated.CreateTranslation200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.CreateTranslationRequest` -> `Generated.CreateTranslationRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. +- `Generated.CreateTranslationRequest` -> `Generated.CreateTranslationRequestMultipart`: Renamed for multipart requests; file fields now accept File or Blob. Re-check the current Type/Encoded shape. - `Generated.CreateTranslationRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2282,13 +2282,13 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.DeletedVideoResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.DetailEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.DetailEnum` -> `Generated.DetailEnum`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.DoubleClickAction` -> `Generated.DoubleClickAction`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. - `Generated.DoubleClickActionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.DownloadFile200` -> `Generated.DownloadFile200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. +- `Generated.DownloadFile200` -> `none`: The standalone response schema was removed. The generated client's downloadFile method returns Uint8Array, optionally paired with the HTTP response according to its config; downloadFileStream streams Uint8Array chunks. - `Generated.Drag` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2316,7 +2316,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.EvalApiError` -> `Generated.EvalApiError`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.EvalCustomDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.EvalCustomDataSourceConfig` -> `Generated.EvalCustomDataSourceConfig`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.EvalCustomDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2364,7 +2364,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.EvalListObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.EvalLogsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.EvalLogsDataSourceConfig` -> `Generated.EvalLogsDataSourceConfig`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.EvalLogsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2392,7 +2392,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.EvalRunOutputItemResult` -> `Generated.EvalRunOutputItemResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.EvalStoredCompletionsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.EvalStoredCompletionsDataSourceConfig` -> `Generated.EvalStoredCompletionsDataSourceConfig`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.EvalStoredCompletionsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2400,13 +2400,13 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.EvalStoredCompletionsSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.ExpiresAfterParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ExpiresAfterParam` -> `Generated.ExpiresAfterParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ExpiresAfterParamAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. - `Generated.FileAnnotation` -> `Generated.FileAnnotation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.FileAnnotationSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.FileAnnotationSource` -> `Generated.FileAnnotationSource`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.FileAnnotationSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2440,9 +2440,9 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.FileSearchToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.FileUploadParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.FileUploadParam` -> `Generated.FileUploadParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.Filters` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.Filters` -> `Generated.Filters`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.FineTuneDPOHyperparameters` -> `Generated.FineTuneDPOHyperparameters`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -2522,7 +2522,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.FunctionAndCustomToolCallOutput` -> `Generated.FunctionAndCustomToolCallOutput`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.FunctionCallItemStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.FunctionCallItemStatus` -> `Generated.FunctionCallItemStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.FunctionCallOutputItemParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2532,9 +2532,9 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.FunctionParameters` -> `Generated.FunctionParameters`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.FunctionShellAction` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.FunctionShellAction` -> `Generated.FunctionShellAction`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.FunctionShellActionParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.FunctionShellActionParam` -> `Generated.FunctionShellActionParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.FunctionShellCall` -> `Generated.FunctionShellCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -2542,7 +2542,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.FunctionShellCallItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.FunctionShellCallItemStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.FunctionShellCallItemStatus` -> `Generated.FunctionShellCallItemStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.FunctionShellCallOutput` -> `Generated.FunctionShellCallOutput`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -2562,7 +2562,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.FunctionShellCallOutputItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.FunctionShellCallOutputOutcomeParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.FunctionShellCallOutputOutcomeParam` -> `Generated.FunctionShellCallOutputOutcomeParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.FunctionShellCallOutputTimeoutOutcome` -> `Generated.FunctionShellCallOutputTimeoutOutcome`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -2630,7 +2630,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.GraderLabelModelType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.GraderMulti` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.GraderMulti` -> `Generated.GraderMulti`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.GraderMultiType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -2654,7 +2654,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.GraderTextSimilarityType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.GrammarSyntax1` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.GrammarSyntax1` -> `Generated.GrammarSyntax1`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.Group` -> `Generated.Group`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -2684,13 +2684,13 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.GroupUserDeletedResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.HistoryParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.HistoryParam` -> `Generated.HistoryParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.HybridSearchOptions` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.HybridSearchOptions` -> `Generated.HybridSearchOptions`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.Image` -> `Generated.Image`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.ImageDetail` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ImageDetail` -> `Generated.ImageDetail`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ImageGenInputUsageDetails` -> `Generated.ImageGenInputUsageDetails`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -2730,7 +2730,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.IncludeEnum` -> `Generated.IncludeEnum`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.InferenceOptions` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.InferenceOptions` -> `Generated.InferenceOptions`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.InputAudio` -> `Generated.InputAudio`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -3058,7 +3058,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.MCPToolCall` -> `Generated.MCPToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.MCPToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.MCPToolCallStatus` -> `Generated.MCPToolCallStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.MCPToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3116,7 +3116,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.MessageRequestContentTextObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.MessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.MessageRole` -> `Generated.MessageRole`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.MessageStatus` -> `OpenAiSchema.MessageStatus`: Use the focused v4 OpenAiSchema definition; the old generated export was removed when the OpenAI specification client was regenerated. @@ -3168,7 +3168,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.OrderEnum` -> `Generated.OrderEnum`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.OtherChunkingStrategyResponseParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.OtherChunkingStrategyResponseParam` -> `Generated.OtherChunkingStrategyResponseParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.OtherChunkingStrategyResponseParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3306,11 +3306,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.PublicUpdateOrganizationRoleBody` -> `Generated.PublicUpdateOrganizationRoleBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.RankerVersionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.RankerVersionType` -> `Generated.RankerVersionType`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.RankingOptions` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.RankingOptions` -> `Generated.RankingOptions`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.RateLimitsParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.RateLimitsParam` -> `Generated.RateLimitsParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.RealtimeAudioFormats` -> `Generated.RealtimeAudioFormats`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -3506,7 +3506,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.RunStepCompletionUsage` -> `Generated.RunStepCompletionUsage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.RunStepDetailsMessageCreationObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.RunStepDetailsMessageCreationObject` -> `Generated.RunStepDetailsMessageCreationObject`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.RunStepDetailsMessageCreationObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3516,11 +3516,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.RunStepDetailsToolCallsCodeObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.RunStepDetailsToolCallsCodeOutputImageObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.RunStepDetailsToolCallsCodeOutputImageObject` -> `Generated.RunStepDetailsToolCallsCodeOutputImageObject`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.RunStepDetailsToolCallsCodeOutputImageObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.RunStepDetailsToolCallsCodeOutputLogsObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.RunStepDetailsToolCallsCodeOutputLogsObject` -> `Generated.RunStepDetailsToolCallsCodeOutputLogsObject`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.RunStepDetailsToolCallsCodeOutputLogsObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3536,7 +3536,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.RunStepDetailsToolCallsFunctionObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.RunStepDetailsToolCallsObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.RunStepDetailsToolCallsObject` -> `Generated.RunStepDetailsToolCallsObject`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.RunStepDetailsToolCallsObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3562,11 +3562,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.ScrollType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.SearchContextSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.SearchContextSize` -> `Generated.SearchContextSize`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.ServiceTier` -> `Generated.ServiceTier`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.ServiceTierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.ServiceTierEnum` -> `Generated.ServiceTierEnum`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.SpecificApplyPatchParam` -> `Generated.SpecificApplyPatchParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -3578,11 +3578,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.StaticChunkingStrategy` -> `Generated.StaticChunkingStrategy`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.StaticChunkingStrategyRequestParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.StaticChunkingStrategyRequestParam` -> `Generated.StaticChunkingStrategyRequestParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.StaticChunkingStrategyRequestParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.StaticChunkingStrategyResponseParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.StaticChunkingStrategyResponseParam` -> `Generated.StaticChunkingStrategyResponseParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.StaticChunkingStrategyResponseParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3610,7 +3610,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.TaskItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.TaskType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.TaskType` -> `Generated.TaskType`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.TextAnnotation` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3702,7 +3702,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.TranscriptionWord` -> `Generated.TranscriptionWord`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.TruncationEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.TruncationEnum` -> `Generated.TruncationEnum`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.Type` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3746,7 +3746,7 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.UrlAnnotation` -> `Generated.UrlAnnotation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.UrlAnnotationSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.UrlAnnotationSource` -> `Generated.UrlAnnotationSource`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.UrlAnnotationSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3932,17 +3932,17 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.VideoListResource` -> `Generated.VideoListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. -- `Generated.VideoModel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.VideoModel` -> `Generated.VideoModel`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.VideoResource` -> `Generated.VideoResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. - `Generated.VideoResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.VideoSeconds` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.VideoSeconds` -> `Generated.VideoSeconds`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.VideoSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.VideoSize` -> `Generated.VideoSize`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.VideoStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.VideoStatus` -> `Generated.VideoStatus`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.VoiceIdsShared` -> `Generated.VoiceIdsShared`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -3952,15 +3952,15 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.WaitType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.WebSearchActionFind` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.WebSearchActionFind` -> `Generated.WebSearchActionFind`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.WebSearchActionFindType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.WebSearchActionOpenPage` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.WebSearchActionOpenPage` -> `Generated.WebSearchActionOpenPage`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.WebSearchActionOpenPageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.WebSearchActionSearch` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.WebSearchActionSearch` -> `Generated.WebSearchActionSearch`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.WebSearchActionSearchType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. @@ -3994,9 +3994,9 @@ effect/rpc/Utils (barrel: effect/rpc) - `Generated.WidgetMessageItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. -- `Generated.WorkflowParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.WorkflowParam` -> `Generated.WorkflowParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. -- `Generated.WorkflowTracingParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. +- `Generated.WorkflowTracingParam` -> `Generated.WorkflowTracingParam`: Available again in the regenerated v4 OpenAI client; re-check the schema's Type/Encoded shape against the current specification. - `Generated.make` -> `Generated.make`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. @@ -4914,11 +4914,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `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. +- `AiError.HttpResponseError` -> `AiError.make + AiError.reasonFromHttpStatus / AiError.InvalidResponseError / AiError.InvalidOutputError`: There is no single v4 response-error class. Wrap a semantic reason with AiError.make: use reasonFromHttpStatus for status failures. Provider response-schema and SSE decoding failures use non-retryable InvalidResponseError. Lower-level HTTP client DecodeError and EmptyBodyError still map to retryable InvalidOutputError in provider adapters. - `AiError.MalformedInput` -> `AiError.make + AiError.InvalidUserInputError`: Replace the old top-level input error with an AiError whose reason is InvalidUserInputError. Use InvalidRequestError when the provider request parameters themselves are malformed. -- `AiError.MalformedOutput` -> `AiError.make + AiError.InvalidOutputError`: Replace the old top-level output error with an AiError whose reason is InvalidOutputError. The old fromParseError helper becomes InvalidOutputError.fromSchemaError. +- `AiError.MalformedOutput` -> `AiError.make + AiError.InvalidOutputError / AiError.InvalidResponseError`: Replace the old top-level output error with an AiError wrapper. Use retryable InvalidOutputError for invalid model-generated output and non-retryable InvalidResponseError for incompatible provider response formats, including response-schema and SSE decoding failures. Replace fromParseError with InvalidOutputError.fromSchemaError or InvalidResponseError.fromSchemaError according to what was being decoded; pass module and method to AiError.make. - `AiError.TypeId` -> `AiError.isAiError`: The AiError brand is private in v4. Use isAiError for runtime narrowing, or isAiErrorReason for a reason, instead of inspecting or constructing the type id. @@ -5706,6 +5706,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `ClusterSchema.ClientTracingEnabled` -> `effect/cluster/ClusterSchema#ClientTracingEnabled`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value. +- `ClusterSchema.InterruptOnTermination`: TODO: needs guidance + - `ClusterSchema.Persisted` -> `effect/cluster/ClusterSchema#Persisted`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same false default. - `ClusterSchema.ShardGroup` -> `effect/cluster/ClusterSchema#ShardGroup`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value. @@ -5948,9 +5950,11 @@ effect/rpc/Utils (barrel: effect/rpc) - `EventJournal.ErrorTypeId` -> `none`: The v4 error marker is private; narrow with EventJournalError instead. +- `EventJournal.EventJournal` -> `effect/eventlog/EventJournal#EventJournal`: Custom journal services must implement entriesById, writeMany, and writeRemoteAcks. writeFromRemote.effect now receives one array of entry/conflicts pairs per batch, not one pair per callback. Read conflicts before invoking it and commit the batch only after it succeeds. Replay entries in entry-id order; conflicts are stored entries with strictly greater ids, also in id order. Compaction replacements use the source entry as their ordering and conflict anchor. + - `EventJournal.RemoteIdTypeId` -> `effect/eventlog/EventJournal#RemoteIdTypeId`: Import RemoteIdTypeId from the v4 EventJournal module; it is now a string brand. -- `EventJournal.makeEntryId` -> `effect/eventlog/EventJournal#makeEntryIdUnsafe`: The unchecked EntryId constructor was renamed to makeEntryIdUnsafe. +- `EventJournal.makeEntryId` -> `effect/eventlog/EventJournal#makeEntryIdUnsafe`: The unchecked EntryId constructor was renamed to makeEntryIdUnsafe. Without explicit msecs, ids advance monotonically within the process using a logical millisecond and counter, so their timestamp can exceed wall-clock time when the clock stalls or moves backwards. Explicit msecs bypasses this sequence. - `EventJournal.makeMemory` -> `effect/eventlog/EventJournal#makeMemory`: The in-memory constructor moved into core Effect and now returns the Context.Service implementation through its Service projection. @@ -5958,7 +5962,7 @@ effect/rpc/Utils (barrel: effect/rpc) ### `@effect/experimental/EventLog` -- `EventLog.Handlers` -> `effect/eventlog/EventLog#Handlers`: Import Handlers from the v4 EventLog module; handlers now also receive storeId. +- `EventLog.Handlers` -> `effect/eventlog/EventLog#Handlers`: Import Handlers from the v4 EventLog module; handlers now also receive storeId. Remote decode or handler failures leave the entire batch uncommitted and retry it after backoff instead of logging and committing past the failure. Replay handlers must tolerate repeated execution. Conflicts are stored entries with strictly greater ids, in id order; compacted replacements use their source entry as the conflict anchor. - `EventLog.HandlersTypeId` -> `effect/eventlog/EventLog#HandlersTypeId`: Import HandlersTypeId from the v4 EventLog module. @@ -5966,17 +5970,29 @@ effect/rpc/Utils (barrel: effect/rpc) - `EventLog.group` -> `effect/eventlog/EventLog#group`: Import group from the v4 EventLog module; it now requires the shared Registry service. +- `EventLog.groupCompaction` -> `effect/eventlog/EventLog#groupCompaction`: Use forward(entry) to retain an input entry unchanged rather than writing a replacement. Entries neither forwarded nor replaced are dropped; undecodable entries are preserved automatically. Replacements are anchored to the first decoded entry for the primary key and replayed in anchor-id order, then write order, with conflicts newer than that anchor. Collapsing entries across an intervening stored entry for the same event and primary key can break convergence. + - `EventLog.layer` -> `effect/eventlog/EventLog#layer`: The v4 layer takes both the schema and handler layer; use layerEventLog for runtime only. - `EventLog.layerIdentityKvs` -> `none`: Compose KeyValueStore.toSchemaStore, EventLog.IdentitySchema, EventLog.makeIdentity, and Layer.effect manually. +- `EventLog.makeClient` -> `effect/eventlog/EventLog#makeClient`: The returned client now takes one { event, payload } object instead of separate event and payload arguments. The payload remains paired with its event tag, and each call delegates to EventLog.write; use EventLog.writeMany for an atomic batch of events. + +### `@effect/experimental/EventLogEncryption` + +- `EventLogEncryption.EncryptedEntry` -> `effect/eventlog/EventLogEncryption#EncryptedBatch`: Replace per-entry encrypted records with batches containing entryIds, iv, and encryptedBatch. Each batch encrypts multiple entries as one message and authenticates the store id and ordered entry ids; existing per-entry ciphertext is not compatible with this format. + +- `EventLogEncryption.EncryptedRemoteEntry` -> `effect/eventlog/EventLogEncryption#EncryptedRemoteBatch`: Remote encrypted records are now batches with entryIds, iv, and encryptedBatch. sequence is the first entry sequence; subsequent entries have consecutive sequences. Existing per-entry ciphertext is not compatible with the batch format. + +- `EventLogEncryption.EventLogEncryption` -> `effect/eventlog/EventLogEncryption#EventLogEncryption`: Use the Context.Service and its Service projection. encrypt and decrypt now take storeId after identity. encrypt returns EncryptedBatch records rather than an iv/encryptedEntries object; decrypt takes EncryptedRemoteBatch records and returns RemoteEntry records. Encryption authenticates the store id and ordered entry ids, so old per-entry ciphertext cannot be supplied as a batch. + ### `@effect/experimental/EventLogRemote` -- `EventLogRemote.Ack` -> `none`: A write acknowledgement is now the void success of EventLogMessage.WriteSingleRpc or WriteChunkedRpc. +- `EventLogRemote.Ack` -> `effect/eventlog/EventLogMessage#WriteAck`: Write RPCs now return arrays of entryId/sequence acknowledgements in request order. WriteSingleRpc returns them directly; WriteChunkedRpc returns them on the completing chunk, with empty arrays for earlier chunks. -- `EventLogRemote.Changes` -> `effect/eventlog/EventLogMessage#ChangesRpc`: ChangesRpc replaces the separate request and response models with one streaming RPC. +- `EventLogRemote.Changes` -> `effect/eventlog/EventLogMessage#ChangesRpc`: ChangesRpc replaces the separate request and response models with one streaming RPC. Encrypted changes carry EncryptedRemoteBatch records. Built-in servers acknowledge writes from this connection instead of echoing them through changes. -- `EventLogRemote.EventLogRemote` -> `effect/eventlog/EventLogRemote#EventLogRemote`: Use the v4 Context.Service; methods now take storeId-aware options. +- `EventLogRemote.EventLogRemote` -> `effect/eventlog/EventLogRemote#EventLogRemote`: Use the v4 Context.Service; methods now take storeId-aware options. write may return entryId/sequence acknowledgement arrays; custom remotes can still return void when confirming uploads through changes instead. - `EventLogRemote.Hello` -> `effect/eventlog/EventLogMessage#HelloResponse`: HelloResponse replaces Hello and includes the v4 authentication challenge; HelloRpc defines the endpoint. @@ -5996,6 +6012,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `EventLogRemote.StopChanges` -> `none`: Interrupt the ChangesRpc stream instead of sending a StopChanges message. +- `EventLogRemote.WriteEntries` -> `effect/eventlog/EventLogMessage#WriteEntries`: Use the storeId-aware write payload with batches of EncryptedBatch instead of iv and encryptedEntries. Each batch contains entryIds, iv, and encryptedBatch; it is not compatible with the old per-entry ciphertext format. + - `EventLogRemote.decodeRequest` -> `effect/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific request decoder. - `EventLogRemote.decodeResponse` -> `effect/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerSchemaBinary replace the module-specific response decoder. @@ -6012,7 +6030,9 @@ effect/rpc/Utils (barrel: effect/rpc) ### `@effect/experimental/EventLogServer` -- `EventLogServer.Storage` -> `effect/eventlog/EventLogServerEncrypted#Storage`: Use the encrypted server Storage service, which is storeId- and session-aware. +- `EventLogServer.PersistedEntry` -> `effect/eventlog/EventLogEncryption#EncryptedBatch`: The per-entry class was removed; encrypted storage now accepts batch records containing entryIds, iv, and encryptedBatch. The class encoding helpers and entryIdString getter are not retained. Old per-entry ciphertext cannot be reused as an encrypted batch. + +- `EventLogServer.Storage` -> `effect/eventlog/EventLogServerEncrypted#Storage`: Use the storeId- and session-aware encrypted server Storage service. write and changes now operate on encrypted batches. write returns newly stored batches and skips a batch only when all its entry ids are already known. Custom storage must implement entrySequences to look up acknowledgements for skipped batches; each batch owns consecutive sequences starting at its sequence. - `EventLogServer.layerStorageMemory` -> `effect/eventlog/EventLogServerEncrypted#layerStorageMemory`: Use the encrypted server memory storage layer; storage is now storeId- and session-aware. @@ -6508,6 +6528,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `Undici.errors` -> `undici.errors`: Import the upstream errors object directly; individual classes follow the Undici 8 API. +- `Undici.errors.ResponseError`: TODO: needs guidance + - `Undici.errors.ResponseStatusCodeError` -> `undici.errors.ResponseError`: Undici 8 replaced ResponseStatusCodeError with ResponseError; construct it with message, statusCode, and the headers/body object. - `Undici.interceptors` -> `undici.interceptors`: Import the upstream interceptors object and apply returned interceptors with dispatcher.compose(...). @@ -6518,6 +6540,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `Undici.interceptors.RedirectInterceptorOpts` -> `undici.interceptors.RedirectInterceptorOpts`: Import the same namespace type; Undici 8 adds throwOnMaxRedirect and redirect header-stripping options. +- `Undici.interceptors.ResponseErrorInterceptorOpts`: TODO: needs guidance + ### `@effect/platform/ChannelSchema` - `ChannelSchema.decode` -> `ChannelSchema.decode`: The API moved to effect/ChannelSchema; update the import and adapt the schema to the v4 Schema.Constraint model. @@ -6626,6 +6650,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `FileSystem.File.Descriptor` -> `none`: Native file descriptors are no longer part of the portable File interface; use the File methods and keep any platform handle private in custom implementations. +- `FileSystem.File.Info` -> `FileSystem.File.Info`: The module moved to effect/FileSystem. dev is now bigint; ino, rdev, nlink, and blocks are Option\. Update custom stat results, comparisons, arithmetic, and serialization rather than converting these values to number and losing precision. mode, uid, and gid remain numeric; size and blksize use ByteSize. Node and Bun preserve the 64-bit metadata exactly, while Deno may round large values before converting them to bigint. + - `FileSystem.FileDescriptor` -> `none`: The descriptor branding constructor was removed with the public fd field; use File operations instead of exposing a native descriptor. - `FileSystem.FileTypeId` -> `typeof FileSystem.FileTypeId`: The runtime marker remains exported, but the separate type alias was removed. @@ -7070,6 +7096,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `HttpClient.makeWith` -> `HttpClient.makeWith`: Retained with the preprocess and postprocess constructor pattern. +- `HttpClient.mapRequest`: TODO: needs guidance + - `HttpClient.retry` -> `HttpClient.retry`: Retained; the Schedule error channel is included in the resulting client error type. - `HttpClient.tap` -> `effect/http/HttpClient#tap`: Moved to the v4 HTTP module with the same response-effect callback and client error/service widening. @@ -7206,6 +7234,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `HttpMiddleware.withTracerDisabledWhenEffect` -> `Effect.provideService(effect, HttpMiddleware.TracerDisabledWhen, predicate)`: Provide the TracerDisabledWhen reference locally to the effect. +- `HttpMiddleware.xForwardedHeaders` -> `HttpMiddleware.xForwardedHeaders({ trustedProxies })`: Now requires a trusted proxy policy; headers from untrusted peers are ignored and X-Forwarded-For is read from the right. HttpServerRequest.toURL no longer reads x-forwarded-proto directly. + ### `@effect/platform/HttpPlatform` - `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. @@ -7526,6 +7556,8 @@ effect/rpc/Utils (barrel: effect/rpc) ### `@effect/platform/Path` +- `Path.Path` -> `Path.Path`: The module moved to effect/Path. Custom service implementations must now provide resolveWithin as well. Prefer Path.make with the existing platform operations: it adds the type marker and derives resolveWithin, which returns an Option and checks lexical containment without following symlinks. + - `Path.TypeId` -> `typeof Path.TypeId`: The module moved to effect/Path; the runtime marker remains exported but the separate type alias was removed. ### `@effect/platform/PlatformConfigProvider` @@ -7568,6 +7600,8 @@ effect/rpc/Utils (barrel: effect/rpc) - `Socket.layerWebSocket` -> `Socket.layerWebSocket`: The constructor remains in effect/socket/Socket; its URL may now also be an Effect. +- `Socket.makeWebSocket`: TODO: needs guidance + - `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` @@ -7610,11 +7644,13 @@ effect/rpc/Utils (barrel: effect/rpc) ### `@effect/platform/UrlParams` +- `UrlParams.Coercible` -> `Template.PrimitiveValue`: TODO: needs guidance + - `UrlParams.CoercibleRecord` -> `UrlParams.CoercibleRecord`: The recursive interface became a generic mapped type preserving the input shape. - `UrlParams.Input` -> `UrlParams.Input`: Retained and broadened to include UrlParams itself. -- `UrlParams.UrlParams` -> `UrlParams.UrlParams`: Import UrlParams from effect/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.UrlParams` -> `UrlParams.UrlParams`: Import UrlParams from effect/http. It is now a branded iterable object with a params field rather than a ReadonlyArray; construct it with UrlParams.make or UrlParams.fromInput. Iteration and params values are string | Redacted.Redacted, not just string. getAll, getFirst, getLast and toRecord unwrap values; toString also serializes unwrapped values and can expose secrets. - `UrlParams.makeUrl` -> `Url.make`: Moved to Url, returns Result, and takes string | undefined for the hash. @@ -8060,13 +8096,17 @@ effect/rpc/Utils (barrel: effect/rpc) - `SqlClient.SafeIntegers` -> `effect/sql/SqlClient#SafeIntegers`: Moved and changed from a Reference subclass to a Context.Reference value; provide it as a service. -- `SqlClient.SqlClient.MakeOptions`: TODO: needs guidance +- `SqlClient.SqlClient` -> `effect/sql/SqlClient#SqlClient`: Moved into core Effect. Custom service implementations must expose driver and afterCommit; SqlClient.make supplies both. driver identifies the driver independently of its SQL dialect. afterCommit registers effects to run after the outermost successful commit and connection release attempt, discarding rolled-back work. withTransaction now also defers contextual Reactivity.invalidate and mutation invalidations until that commit. Transactions bypassing makeWithTransaction, such as sqlite-do transactions, run callbacks and invalidations immediately. + +- `SqlClient.SqlClient.MakeOptions` -> `effect/sql/SqlClient#SqlClient.MakeOptions`: Moved; driver is now required and conventionally names the driver package without @effect/sql-, for example pg or pglite. Rename reactiveMailbox to reactiveQueue; transactionService is optional. - `SqlClient.TransactionConnection` -> `effect/sql/SqlClient#TransactionConnection`: Now a factory keyed by client id, not a singleton tag. Prefer the client's transactionService; the payload type is TransactionConnection.Service. - `SqlClient.TypeId` -> `none`: The brand is private in v4; do not inspect or attach it, and obtain clients through SqlClient or SqlClient.make. -- `SqlClient.make` -> `effect/sql/SqlClient#make`: Moved; custom clients rename MakeOptions.reactiveMailbox to reactiveQueue and may supply transactionService. +- `SqlClient.make` -> `effect/sql/SqlClient#make`: Moved; custom clients must supply MakeOptions.driver, rename reactiveMailbox to reactiveQueue, and may supply transactionService. Driver names conventionally omit the @effect/sql- prefix, for example pg or pglite. + +- `SqlClient.makeWithTransaction` -> `effect/sql/SqlClient#makeWithTransaction`: Rename transactionTag to transactionService. acquireConnection is now a function receiving restore and returning Effect\, rather than an effect returning a scope/connection pair. Register releases in the provided scope and wrap interruptible connection waits with restore; the wrapper closes the scope on transaction completion or failed acquisition. ### `@effect/sql/SqlConnection` @@ -8084,9 +8124,11 @@ effect/rpc/Utils (barrel: effect/rpc) ### `@effect/sql/SqlEventLogServer` -- `SqlEventLogServer.layerStorage` -> `effect/eventlog/SqlEventLogServerEncrypted#layerStorage`: Moved to the encrypted server module with the same options and EventLogEncryption requirement. +- `SqlEventLogServer.layerStorage` -> `effect/eventlog/SqlEventLogServerEncrypted#layerStorage`: Moved to the encrypted server module; remove insertBatchSize. entryTablePrefix and remoteIdTable remain, as does the EventLogEncryption requirement. Storage uses encrypted batch tables and entry-id lookup tables, not the old per-entry ciphertext schema; existing encrypted tables are not automatically converted. + +- `SqlEventLogServer.layerStorageSubtle` -> `effect/eventlog/SqlEventLogServerEncrypted#layerStorageSubtle`: Moved to the encrypted server module; remove insertBatchSize. The layer still supplies Web Crypto encryption. Storage now uses encrypted batch tables and entry-id lookup tables; existing per-entry ciphertext tables are not automatically converted. -- `SqlEventLogServer.makeStorage` -> `effect/eventlog/SqlEventLogServerEncrypted#makeStorage`: Moved to the encrypted server module with the same SQL, encryption, and scope requirements. +- `SqlEventLogServer.makeStorage` -> `effect/eventlog/SqlEventLogServerEncrypted#makeStorage`: Moved to the encrypted server module; remove insertBatchSize. SQL, encryption, and scope requirements remain. Storage uses encrypted batch tables and entry-id lookup tables, not the old per-entry ciphertext schema; existing encrypted tables are not automatically converted. ### `@effect/sql/SqlPersistedQueue` @@ -8098,6 +8140,10 @@ effect/rpc/Utils (barrel: effect/rpc) - `SqlResolver.SqlResolver` -> `RequestResolver.RequestResolver>`: The wrapper interface was removed; constructors return RequestResolvers. Execute them with effect/sql/SqlResolver#request. +- `SqlResolver.findById`: TODO: needs guidance + +- `SqlResolver.ordered`: TODO: needs guidance + - `SqlResolver.void` -> `effect/sql/SqlResolver#void`: Moved, but remove the leading tag and withContext arguments; it now returns a RequestResolver synchronously and runs through SqlResolver.request. ### `@effect/sql/SqlSchema` @@ -8108,6 +8154,8 @@ effect/rpc/Utils (barrel: effect/rpc) ### `@effect/sql/Statement` +- `Statement.Fragment`: TODO: needs guidance + - `Statement.FragmentId` -> `none`: The v4 fragment brand is private; use Fragment, fragment, and isFragment instead of direct type-id access. - `Statement.Statement` -> `effect/sql/Statement#Statement`: Moved; the nested Transformer type is now top-level and its callback receives Fiber.Fiber rather than FiberRefs.FiberRefs. @@ -8680,7 +8728,9 @@ effect/rpc/Utils (barrel: effect/rpc) - `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.it` -> `@effect/vitest#it`: Vitest 5 removes it.sequential; pass { concurrent: false } to the native test or as the Effect helper's third argument. Synchronous it.prop callbacks must return boolean or void: true and undefined pass, false falsifies, and other values (including Effects and Promises) throw TypeError. Use it.effect.prop for Effect callbacks. + +- `index.prop` -> `@effect/vitest#prop`: Synchronous property callbacks must return boolean or void: true and undefined pass, false falsifies, and other values (including Effects and Promises) throw TypeError. Move Effect callbacks to it.effect.prop instead of returning an Effect from prop. - `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(...). @@ -8980,7 +9030,7 @@ Arbitrary.schema(schema) ### `effect/Cause` -- `Cause.Cause` -> `Cause.Cause`: The name remains, but v4 Cause\ is a wrapper with readonly reasons: ReadonlyArray\\>, not the v3 Empty/Fail/Die/Interrupt/Sequential/Parallel tree. +- `Cause.Cause` -> `Cause.Cause`: V4 Cause\ wraps readonly reasons: ReadonlyArray\\>, replacing the v3 cause tree. Finalizer and concurrent failure aggregation can convert typed errors to defects when a defect is present; Cause.combine itself does not perform this conversion. - `Cause.Cause.Variance` -> `none`: The public variance helper was removed. Cause.Cause is directly branded by Cause.TypeId; application code should not reproduce the old variance member. @@ -9144,7 +9194,7 @@ Arbitrary.schema(schema) - `Channel.bufferChunk` -> `none`: The inferred Channel.fromChunk match is not equivalent. Rebuild the buffered upstream-pull transform with Channel.fromTransform and Channel.toTransform. -- `Channel.catchAll` -> `Channel.catch`: Renamed to catch for typed-error recovery. +- `Channel.catchAll` -> `Channel.catch`: Rename to Channel.catch. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. Use Channel.catchCause for full-cause handling. - `Channel.catchAllCause` -> `Channel.catchCause`: Renamed to catchCause for full-cause recovery. @@ -9190,6 +9240,8 @@ Arbitrary.schema(schema) - `Channel.isChannelException` -> `none`: Removed implementation artifact. Channel.pipeToOrFail now handles upstream failures without exposing the v3 exception wrapper. +- `Channel.mapError` -> `Channel.mapError`: Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. + - `Channel.mapErrorCause` -> `Channel.catchCause`: Use Channel.catchCause(self, (cause) =\> Channel.failCause(f(cause))). - `Channel.mapInputContext` -> `Channel.updateContext`: Renamed to updateContext for transforming the channel requirement Context. @@ -9222,9 +9274,11 @@ Arbitrary.schema(schema) - `Channel.mergeWith` -> `Channel.merge`: Use Channel.merge with haltStrategy left, right, both, or either for standard policies. Custom MergeDecision effects require a Pull-level redesign. -- `Channel.orDieWith` -> `Channel.catch`: Use Channel.catch(self, (error) =\> Channel.die(f(error))); v4 Channel.orDie has no mapping callback. +- `Channel.orDie` -> `Channel.orDie`: Converts typed errors to defects. If a defect is already present, every typed error is converted to a defect, preserving reason order and interruptions. + +- `Channel.orDieWith` -> `Channel.mapError + Channel.orDie`: Use self.pipe(Channel.mapError(f), Channel.orDie). Mapping transforms every typed error while preserving existing defects and interruptions; Channel.catch skips the mapper for causes containing defects. -- `Channel.orElse` -> `Channel.catch`: Use Channel.catch(self, () =\> that()) and keep the fallback lazy. +- `Channel.orElse` -> `Channel.catch`: Use Channel.catch(self, () =\> that()) and keep the fallback lazy. Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery. - `Channel.provideLayer` -> `Channel.provide`: Both collapse into provide. V4 removes services supplied by the layer and retains remaining requirements; use options.local when a fresh layer instance is needed. @@ -9904,20 +9958,26 @@ Arbitrary.schema(schema) - `Effect.cachedFunction` -> `none`: The function memoizer was removed; use `Cache` for keyed effectful caching. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. -- `Effect.catch` -> `Effect.catch`: Still exported in v4; update call sites for the revised signature, options, and channel inference. +- `Effect.catch` -> `Effect.catch`: Typed recovery skips causes containing defects and converts their typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery; use Effect.catchCause for full-cause handling. -- `Effect.catchAll` -> `Effect.catch`: Use the shortened v4 error-handler name. Adapt arguments and imports to the v4 API. +- `Effect.catchAll` -> `Effect.catch`: Rename to Effect.catch. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. Use Effect.catchCause for full-cause handling. - `Effect.catchAllCause` -> `Effect.catchCause`: Use the shortened v4 cause-handler name. Adapt arguments and imports to the v4 API. - `Effect.catchAllDefect` -> `Effect.catchDefect`: Use the shortened v4 defect-handler name. Adapt arguments and imports to the v4 API. -- `Effect.catchSome` -> `Effect.catchFilter`: Replace the Option-returning partial function with a `Filter` and handler. Adapt arguments and imports to the v4 API. +- `Effect.catchIf` -> `Effect.catchIf`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + +- `Effect.catchSome` -> `Effect.catchFilter`: Replace the Option-returning partial function with a Filter and handler. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. - `Effect.catchSomeCause` -> `Effect.catchCauseFilter`: Replace the Option-returning partial function with a cause `Filter` and handler. Adapt arguments and imports to the v4 API. - `Effect.catchSomeDefect` -> `none`: Use `Effect.catchDefect` and branch explicitly, re-dying for unmatched defects. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. +- `Effect.catchTag` -> `Effect.catchTag`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + +- `Effect.catchTags` -> `Effect.catchTags`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + - `Effect.cause` -> `Effect.exit`: Inspect `Exit.Failure.cause`; v4 no longer exposes an Effect-only cause extractor. Adapt arguments and imports to the v4 API. - `Effect.checkInterruptible` -> `none`: Interruptibility introspection was removed; structure the region explicitly with `Effect.interruptible` or `Effect.uninterruptible`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. @@ -9954,7 +10014,7 @@ Arbitrary.schema(schema) - `Effect.dropWhile` -> `none`: Use an explicit `Effect.gen` loop, or `Array.dropWhile` when the predicate is pure. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. -- `Effect.either` -> `Effect.result`: V4 represents typed success/failure as `Result` instead of `Either`. Adapt arguments and imports to the v4 API. +- `Effect.either` -> `Effect.result`: V4 represents typed success/failure as `Result` instead of `Either`. Adapt arguments and imports to the v4 API. Causes containing defects are not captured as Result failures and propagate with typed errors converted to defects; interruptions alongside typed errors do not block typed recovery. - `Effect.ensureErrorType` -> `Effect.satisfiesErrorType`: Use the renamed compile-time channel constraint. Adapt arguments and imports to the v4 API. @@ -10014,7 +10074,9 @@ Arbitrary.schema(schema) - `Effect.if` -> `Effect.suspend`: Select the branch lazily with a JavaScript conditional inside `Effect.suspend`. Adapt arguments and imports to the v4 API. -- `Effect.ignoreLogged` -> `Effect.ignore`: Pass `{ log: true }` to the consolidated ignore combinator. Adapt arguments and imports to the v4 API. +- `Effect.ignore` -> `Effect.ignore`: Ignores recoverable typed errors only. Causes containing defects propagate with typed errors converted to defects; interruptions alongside typed errors do not prevent ignoring them. + +- `Effect.ignoreLogged` -> `Effect.ignore`: Use Effect.ignore(self, { log: true }). Causes containing defects are not ignored and propagate with typed errors converted to defects; interruptions alongside typed errors do not block typed recovery. - `Effect.inheritFiberRefs` -> `none`: Bulk FiberRef inheritance was removed; propagate required context references explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. @@ -10052,10 +10114,18 @@ Arbitrary.schema(schema) - `Effect.mapAccum` -> `Effect.reduce`: Carry `[state, output]` through an effectful reduction. Adapt arguments and imports to the v4 API. +- `Effect.mapBoth` -> `Effect.mapBoth`: Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. + +- `Effect.mapError` -> `Effect.mapError`: Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. + - `Effect.mapErrorCause` -> `Effect.catchCause + Effect.failCause`: Transform the Cause in a cause handler and fail with the mapped Cause. Adapt arguments and imports to the v4 API. - `Effect.mapInputContext` -> `Effect.contextWith + Effect.provide`: Build the required context from the incoming context and provide it explicitly. Adapt arguments and imports to the v4 API. +- `Effect.match` -> `Effect.match`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + +- `Effect.matchEffect` -> `Effect.matchEffect`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + - `Effect.merge` -> `Effect.catch`: Recover each typed error with `Effect.succeed` so both channels become success values. Adapt arguments and imports to the v4 API. - `Effect.mergeAll` -> `Effect.reduce`: Reduce the input effects with an effectful accumulator. Adapt arguments and imports to the v4 API. @@ -10068,14 +10138,20 @@ Arbitrary.schema(schema) - `Effect.once` -> `Effect.cached`: Create the cached Effect once, then execute the returned Effect repeatedly. Adapt arguments and imports to the v4 API. +- `Effect.option` -> `Effect.option`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + - `Effect.optionFromOptional` -> `Effect.catchTag`: Map success to `Option.some` and recover `NoSuchElementError` with `Option.none`. Adapt arguments and imports to the v4 API. +- `Effect.orDie` -> `Effect.orDie`: Converts typed errors to defects. If a defect is already present, every typed error is converted to a defect, preserving reason order and interruptions. + - `Effect.orDieWith` -> `Effect.mapError + Effect.orDie`: Map the typed error to the desired defect, then convert failures to defects. Adapt arguments and imports to the v4 API. -- `Effect.orElse` -> `Effect.catch`: Ignore the caught error and evaluate the fallback Effect. Adapt arguments and imports to the v4 API. +- `Effect.orElse` -> `Effect.catch`: Use Effect.catch(self, () =\> that()). Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery. - `Effect.orElseFail` -> `Effect.mapError`: Replace every typed error with the lazily produced failure value. Adapt arguments and imports to the v4 API. +- `Effect.orElseSucceed` -> `Effect.orElseSucceed`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + - `Effect.parallelErrors` -> `Effect.all`: Use `{ mode: "result", concurrency: "unbounded" }` and collect failures explicitly. Adapt arguments and imports to the v4 API. - `Effect.parallelFinalizers` -> `none`: Parallel finalizer strategy mutation was removed; fork independent cleanup explicitly when ordering is irrelevant. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. @@ -10098,8 +10174,16 @@ Arbitrary.schema(schema) - `Effect.reduceWhile` -> `none`: Use an explicit `Effect.gen` loop that checks the accumulator before each step. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. +- `Effect.repeat` -> `Effect.repeat`: If the source or policy fails with a cause containing defects, typed errors are converted to defects. An interruption alongside a typed error does not block the typed failure path. + - `Effect.repeatN` -> `Effect.repeat`: Pass `{ times: n }` to the consolidated repeat combinator. Adapt arguments and imports to the v4 API. +- `Effect.repeatOrElse` -> `Effect.repeatOrElse`: 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. Causes containing defects from either the source or policy skip the fallback and convert typed errors to defects. Interruptions alongside typed errors do not block the fallback. + +- `Effect.retry` -> `Effect.retry`: Retries typed source errors, including those alongside interruptions. A source cause containing a defect is not retried and propagates with typed errors converted to defects. + +- `Effect.retryOrElse` -> `Effect.retryOrElse`: A source cause containing a defect skips retries and the fallback, converting typed errors to defects. Interruptions alongside typed errors do not block retries. Policy failures propagate unchanged. + - `Effect.runRequestBlock` -> `none`: The request-runtime block runner is internal; submit requests with `Effect.request`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. - `Effect.runtime` -> `Effect.context + Effect.runForkWith`: Capture services as a Context and use the corresponding `run*With` function. Adapt arguments and imports to the v4 API. @@ -10476,6 +10560,10 @@ Arbitrary.schema(schema) - `Exit.isInterrupted` -> `Exit.hasInterrupts`: Direct semantic rename; true for a Failure whose Cause contains at least one Interrupt reason, false for Success. +- `Exit.mapBoth` -> `Exit.mapBoth`: Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. + +- `Exit.mapError` -> `Exit.mapError`: Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. + - `Exit.mapErrorCause` -> `Exit.match + Exit.failCause / Exit.succeed`: No direct v4 combinator. Use Exit.match(self, { onFailure: cause =\> Exit.failCause(f(cause)), onSuccess: Exit.succeed }); f now receives the flattened v4 Cause representation. Cause.map is only equivalent when f merely maps typed errors. - `Exit.matchEffect` -> `Effect.matchCauseEffectEager`: Direct cause-aware migration because Exit is an Effect in v4. Use the same onFailure/onSuccess handlers; the Eager variant preserves v3's immediate branch selection for resolved Exit values. @@ -10990,6 +11078,8 @@ Depend on fast-check and import it directly. Pass the former character arbitrary FastCheck.string({ ...constraints, unit: arbitrary }) ``` +- `FastCheck.stringify`: TODO: needs guidance + - `FastCheck.subarray`: TODO: needs guidance - `FastCheck.toStringMethod`: TODO: needs guidance @@ -11681,7 +11771,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `Layer.matchCause` -> `Layer.fromBuild with Effect.matchCauseEffect over Layer.buildWithMemoMap`: Fold the source acquisition cause, then build the selected failure or success layer. -- `Layer.memoize` -> `automatic shared memoization under Effect.provide`: Reuse the same Layer value; use { local: true } or Layer.fresh to opt out, or MemoMap APIs for manual control. +- `Layer.memoize` -> `enclosing Effect.provide or explicit MemoMap APIs`: A layer already built by an enclosing provide or runtime is reused; sibling provide calls build their own copies. Use { local: true } or Layer.fresh for fresh builds, or MemoMap APIs for manual sharing. - `Layer.orElse` -> `Layer.catch(self, () => fallback())`: Expand the removed lazy fallback alias with Layer.catch. @@ -12005,7 +12095,7 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `Match.Types` -> `Match.Types`: The public type-level namespace is retained with no call-site migration. -- `Match.Types.ExtractAndNarrow` -> `Match.Types.ExtractAndNarrow`: The type-only matching helper is retained unchanged. +- `Match.Types.ExtractAndNarrow` -> `Match.Types.ExtractAndNarrow`: Array prefix patterns now narrow the required positions and preserve the unmatched tail of arrays and variadic tuples. Update explicit handler or helper result types that assumed a prefix pattern described the entire array; runtime prefix matching is unchanged. - `Match.Types.MaybeReplace` -> `Match.Types.MaybeReplace`: The type-only matching helper is retained unchanged. @@ -12027,6 +12117,14 @@ JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) - `Match.either` -> `Match.result`: Renamed finalizer with a container change: matched Right and unmatched Left become Result.Success and Result.Failure. +- `Match.not` -> `Match.not`: Object patterns now reject top-level keys absent from every variant of the remaining input type. Correct stale property names or narrow the input before matching; function, refinement, and array patterns are not subject to this object-key check. + +- `Match.when` -> `Match.when`: Object patterns now reject top-level keys absent from every variant of the remaining input type. Correct stale property names or narrow the input before matching. Array prefix patterns retain the unmatched tail in the handler type rather than treating the pattern as the entire array. + +- `Match.whenAnd` -> `Match.whenAnd`: Each object pattern now rejects top-level keys absent from every variant of the remaining input type, including patterns spread from an array. Correct stale property names or narrow the input before matching. + +- `Match.whenOr` -> `Match.whenOr`: Each object pattern now rejects top-level keys absent from every variant of the remaining input type, including patterns spread from an array. Correct stale property names or narrow the input before matching. + ### `effect/MergeDecision` - `MergeDecision.Await` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. @@ -15537,21 +15635,25 @@ Schema.toFormatter(schema) - `Sink.leftover` -> `Sink.succeed`: Removed as a standalone constructor; Sink.succeed now takes optional leftovers: Sink.succeed(void 0, leftovers) where leftovers is a NonEmptyReadonlyArray\ instead of Chunk\. +- `Sink.mapError` -> `Sink.mapError`: Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. + - `Sink.mapInputChunks` -> `Sink.mapInputArray`: Renamed; f maps NonEmptyReadonlyArray\ =\> NonEmptyReadonlyArray\ (must stay non-empty) instead of Chunk =\> Chunk. - `Sink.mapInputChunksEffect` -> `Sink.mapInputArrayEffect`: Renamed; f maps NonEmptyReadonlyArray\ =\> Effect\\> (must stay non-empty) instead of Chunk =\> Effect\. - `Sink.mkString` -> `Sink.reduceArray`: Removed as a built-in; equivalent one-liner: Sink.reduceArray(() =\> "", (s, arr) =\> s + arr.join("")). +- `Sink.orElse` -> `Sink.orElse`: Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block typed recovery. + - `Sink.race` -> `none`: Sink racing (race/raceBoth/raceWith) was removed from v4; broadcast the stream (Stream.broadcast) into two consumers and race the resulting run effects with Effect.race, or write a custom Channel. -- `Sink.raceBoth` -> `none`: Removed with the race family; broadcast the stream and use Effect.raceBoth (or Effect.race) on the two Stream.run effects to learn which side won. +- `Sink.raceBoth` -> `none`: Removed with the race family; broadcast the stream and tag each Stream.run effect's result before combining them with Effect.race to learn which side succeeded first. -- `Sink.raceWith` -> `none`: Removed, along with the MergeDecision type it depended on; the closest is broadcasting the stream and combining the two run effects manually (Effect.raceWith on the run effects). +- `Sink.raceWith` -> `none`: Removed, along with the MergeDecision type it depended on; broadcast the stream and combine the two run effects manually. Use Effect.race for the first success or Effect.raceFirst for the first completion; custom completion and interruption handling requires explicit fibers. -- `Sink.refineOrDie` -> `Sink.catch`: Removed; rebuild with the typed-error handler: Sink.catch(self, (e) =\> Option.match(pf(e), { onSome: Effect.fail, onNone: () =\> Effect.die(e) })) — note Sink.catch replaces the result on recovery, so refined errors must be re-failed as shown. +- `Sink.refineOrDie` -> `Sink.catch`: For recoverable typed errors, use Sink.catch(self, (e) =\> Option.match(pf(e), { onSome: Effect.fail, onNone: () =\> Effect.die(e) })). Causes containing defects skip pf and convert typed errors to defects; use Sink.catchCause for full-cause handling. -- `Sink.refineOrDieWith` -> `Sink.catch`: Removed; same pattern as refineOrDie but die with the mapped defect: Sink.catch(self, (e) =\> Option.match(pf(e), { onSome: Effect.fail, onNone: () =\> Effect.die(f(e)) })). +- `Sink.refineOrDieWith` -> `Sink.catch`: Use the refineOrDie recipe with Effect.die(f(e)) for unrefined errors. Causes containing defects skip pf and f and convert typed errors to defects; use Sink.catchCause for full-cause handling. - `Sink.splitWhere` -> `none`: Removed; it re-chunked input so the sink stopped before the first later element matching the predicate — closest v4 options are pre-splitting the stream (Stream.split / Stream.takeWhile) or a custom Sink.fromTransform that cuts pulled arrays at the predicate boundary and returns the rest as leftovers. @@ -15685,7 +15787,7 @@ Schema.toFormatter(schema) - `Stream.async` -> `Stream.callback`: Stream.callback((queue) =\> Effect | void, { bufferSize?, strategy? }) replaces the Emit-based async; push with Queue.offer/offerAll, end with Queue.end, fail with Queue.fail. -- `Stream.asyncEffect` -> `Stream.callback`: The register function of Stream.callback may return an Effect (run before the stream starts pulling), covering asyncEffect; signal end/failure through the provided Queue. +- `Stream.asyncEffect` -> `Stream.callback`: The register function of Stream.callback may return an Effect, covering asyncEffect. Unlike v3, it is started before the stream pulls but not awaited: it runs synchronously until its first async boundary, so asynchronous setup may still be in progress when pulling begins. Signal end/failure through the provided Queue. - `Stream.asyncPush` -> `Stream.callback`: Stream.callback's register effect can use Scope for acquire/release of the external subscription, replacing asyncPush; the Emit ops helpers become plain Queue operations. @@ -15715,15 +15817,17 @@ Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(he - `Stream.bufferChunks` -> `Stream.bufferArray`: Chunk-\>Array rename; buffers whole arrays (chunks) up to capacity with the same strategy options. -- `Stream.catchAll` -> `Stream.catch`: Renamed to Stream.catch (exported keyword-style); same (error) =\> Stream handler for all typed failures. +- `Stream.catchAll` -> `Stream.catch`: Rename to Stream.catch with an (error) =\> Stream handler. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. Use Stream.catchCause for full-cause handling. - `Stream.catchAllCause` -> `Stream.catchCause`: Renamed; handler receives the full Cause\ and returns a recovery stream, identical semantics. -- `Stream.catchSome` -> `Stream.catchFilter`: Option-returning partial handler replaced by the Filter API: Stream.catchFilter(filter, f, orElse?) recovers matched errors, unmatched failures pass through (Stream.catchIf for refinement/predicate matching). +- `Stream.catchSome` -> `Stream.catchFilter`: Use Stream.catchFilter(filter, f, orElse?) instead of an Option-returning partial handler. Causes containing defects skip typed recovery and convert typed errors to defects; interruptions alongside typed errors do not block recovery. - `Stream.catchSomeCause` -> `Stream.catchCauseFilter`: Option-returning cause handler replaced by Stream.catchCauseFilter(filter, f, orElse?) using a Filter on the Cause (Stream.catchCauseIf for refinements). -- `Stream.catchTags` -> `Stream.catchTags`: Only keys present in the tagged error union are accepted; remove handlers for tags the stream cannot fail with. Untagged errors remain in the error channel. +- `Stream.catchTag` -> `Stream.catchTag`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + +- `Stream.catchTags` -> `Stream.catchTags`: Only keys in the tagged error union are accepted; untagged errors remain in the error channel. Causes containing defects skip handlers and convert typed errors to defects; interruptions alongside typed errors do not block recovery. - `Stream.chunksWith` -> `Stream.flattenArray(f(Stream.chunks(self)))`: No dedicated combinator; expose chunk structure with Stream.chunks (Stream\\>), transform, then re-flatten with Stream.flattenArray. @@ -15751,7 +15855,7 @@ Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(he - `Stream.distributedWithDynamic` -> `none`: Dynamic predicate-routed fan-out removed with distributedWith. Use Stream.broadcast/Stream.share for dynamic mirrors plus per-subscriber Stream.filter, or hand-roll routing into Queues via Stream.runForEach. -- `Stream.either` -> `Stream.result`: Either is replaced by Result in v4: Stream.result yields Stream\, never, R\> (element -\> Result.succeed, first error -\> Result.fail and the stream ends, as before). +- `Stream.either` -> `Stream.result`: Either is replaced by Result in v4: Stream.result yields Stream\, never, R\> (element -\> Result.succeed, first error -\> Result.fail and the stream ends, as before). Causes containing defects are not emitted as Result failures and propagate with typed errors converted to defects; interruptions alongside typed errors do not block typed recovery. - `Stream.ensuringWith` -> `Stream.onExit`: Renamed; Stream.onExit runs the finalizer with the Exit\ of the stream, identical shape. @@ -15801,7 +15905,7 @@ Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(he - `Stream.let` -> `Stream.let`: Retained in v4 and aligned with Effect.let: the field name may re-bind an existing key (the computed value replaces it) and the record type is Simplify\ & Record\\>. -- `Stream.mapBoth` -> `Stream.mapBoth`: Rename the options onSuccess to onElement and onFailure to onError. The callbacks still transform emitted elements and typed errors respectively. +- `Stream.mapBoth` -> `Stream.mapBoth`: Rename the options onSuccess to onElement and onFailure to onError. The callbacks still transform emitted elements and typed errors respectively. Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. - `Stream.mapChunks` -> `Stream.mapArray`: Chunk-\>Array rename; transforms each emitted chunk as a NonEmptyReadonlyArray. @@ -15815,6 +15919,8 @@ Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(he - `Stream.mapConcatEffect` -> `Stream.flattenIterable(Stream.mapEffect(self, f))`: Removed; effectfully map each element to an Iterable and flatten with Stream.flattenIterable. +- `Stream.mapError` -> `Stream.mapError`: Maps every typed error in the cause, preserving defects, interruptions and reason order. Unlike typed recovery, mapping does not convert errors to defects. + - `Stream.mapErrorCause` -> `Stream.catchCause((cause) => Stream.failCause(f(cause)))`: Removed; transform the full Cause by catching it and re-failing with the mapped cause. - `Stream.mapInputContext` -> `Stream.updateContext`: Renamed; same contravariant (Context\) =\> Context\ mapping of the required services. @@ -15837,18 +15943,22 @@ Stream.mergeAll(Object.entries(streams).map(([_tag, s]) => Stream.map(s, (value) - `Stream.onDone` -> `Stream.onEnd`: Renamed; v4 onEnd takes an Effect value (not a () =\> Effect thunk) run when the stream ends successfully, and its error type may add to the stream's. +- `Stream.orDie` -> `Stream.orDie`: Converts typed errors to defects. If a defect is already present, every typed error is converted to a defect, preserving reason order and interruptions. + - `Stream.orDieWith` -> `Stream.orDie`: orDieWith removed; transform the error first, then convert failures to defects: `self.pipe(Stream.mapError(f), Stream.orDie)`. -- `Stream.orElse` -> `Stream.catch`: v3 catchAll was renamed to Stream.catch in v4; orElse ignored the error, so write `Stream.catch(self, () => that())`. +- `Stream.orElse` -> `Stream.catch`: Use Stream.catch(self, () =\> that()). Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery. -- `Stream.orElseEither` -> `Stream.catch`: Removed; Either is replaced by Result in v4. Emulate: `Stream.map(self, Result.succeed).pipe(Stream.catch(() => Stream.map(that(), Result.fail)))` (same encoding v4 Stream.mergeResult uses). +- `Stream.orElseEither` -> `Stream.catch`: Replace Either with Result: Stream.map(self, Result.succeed).pipe(Stream.catch(() =\> Stream.map(that(), Result.fail))). Causes containing defects skip the fallback and convert typed errors to defects; interruptions alongside typed errors do not block recovery. -- `Stream.orElseFail` -> `Stream.mapError`: Removed; it only replaced the failure value: `Stream.mapError(self, () => error())` or `Stream.catch(self, () => Stream.fail(error()))`. +- `Stream.orElseFail` -> `Stream.mapError`: Use Stream.mapError(self, () =\> error()) to replace each typed error while preserving existing defects and interruptions. Stream.catch is not equivalent for causes containing defects because it skips typed recovery. - `Stream.orElseIfEmptyChunk` -> `Stream.orElseIfEmpty`: Folded into Stream.orElseIfEmpty, which now takes a lazy fallback Stream: `Stream.orElseIfEmpty(self, () => Stream.fromArray(array))`; Chunk is replaced by plain arrays. - `Stream.orElseIfEmptyStream` -> `Stream.orElseIfEmpty`: Direct rename: v4 Stream.orElseIfEmpty takes a LazyArg\ fallback, identical semantics. +- `Stream.orElseSucceed` -> `Stream.orElseSucceed`: Causes containing defects skip typed recovery and convert typed errors to defects, preserving reason order and interruptions. An interruption alongside a typed error does not block recovery. + - `Stream.paginateChunk` -> `Stream.paginate`: v4 Stream.paginate is effectful and array-based: `paginate(s, (s) => Effect, Option]>)`; wrap the pure step in Effect.succeed and use an array instead of a Chunk. - `Stream.paginateChunkEffect` -> `Stream.paginate`: v4 Stream.paginate has exactly this shape; only Chunk becomes ReadonlyArray. @@ -15867,9 +15977,9 @@ Stream.mergeAll(Object.entries(streams).map(([_tag, s]) => Stream.map(s, (value) - `Stream.provideSomeLayer` -> `Stream.provide`: v4 Stream.provide accepts a Layer (or Context) and excludes only the provided services from R — same partial-provision semantics. -- `Stream.refineOrDie` -> `Stream.catch`: Removed; emulate with `Stream.catch(self, (e) => { const r = pf(e); return Option.isSome(r) ? Stream.fail(r.value) : Stream.die(e) })` — refail refined errors, die on the rest. +- `Stream.refineOrDie` -> `Stream.catch`: For recoverable typed errors, use Stream.catch(self, (e) =\> { const r = pf(e); return Option.isSome(r) ? Stream.fail(r.value) : Stream.die(e) }). Causes containing defects skip pf and convert typed errors to defects; use Stream.catchCause for full-cause handling. -- `Stream.refineOrDieWith` -> `Stream.catch`: Removed; same as refineOrDie but die with the mapped defect: `Stream.die(f(e))` for unrefined errors. +- `Stream.refineOrDieWith` -> `Stream.catch`: Use the refineOrDie recipe with Stream.die(f(e)) for unrefined errors. Causes containing defects skip pf and f and convert typed errors to defects; use Stream.catchCause for full-cause handling. - `Stream.repeatEffect` -> `Stream.fromEffectRepeat`: Renamed; repeats the effect forever emitting each result. @@ -15889,6 +15999,8 @@ Stream.mergeAll(Object.entries(streams).map(([_tag, s]) => Stream.map(s, (value) - `Stream.repeatWith` -> `none`: Removed; v4 Stream.repeat(schedule) covers the repetition but drops the schedule outputs and the onElement/onSchedule unification. Hand-roll if schedule outputs must appear in the stream. +- `Stream.retry` -> `Stream.retry`: Retries typed source errors, including those alongside interruptions. A source cause containing a defect is not retried and propagates with typed errors converted to defects. + - `Stream.runFoldScoped` -> `Stream.runFold`: Scoped run variants are gone; v4 run functions manage the stream's scope internally and the initial value is now a LazyArg: `Stream.runFold(self, () => s, f)`. For enclosing-scope control, pull manually via `Stream.toPull` (Effect\). - `Stream.runFoldScopedEffect` -> `Stream.runFoldEffect`: Scoped run variants are gone; use `Stream.runFoldEffect(self, () => s, f)` — scope is managed internally, initial value is a LazyArg. Use Stream.toPull for manual scoped consumption. diff --git a/repos/effect/migration/yieldable.md b/migration/yieldable.md similarity index 100% rename from repos/effect/migration/yieldable.md rename to migration/yieldable.md diff --git a/old.patch b/old.patch deleted file mode 100644 index e69de29bb..000000000 diff --git a/package.json b/package.json index bedc3c2e7..30ef6be55 100644 --- a/package.json +++ b/package.json @@ -1,76 +1,84 @@ { "private": true, - "packageManager": "pnpm@12.14.0", - "engines": { - "node": ">=24.22.1", - "pnpm": ">= 12.0.0" - }, + "type": "module", + "packageManager": "pnpm@11.20.0", "scripts": { - "all": "pnpm -r", - "pub": "pnpm ci:version && git commit -am 'chore: update versions' && pnpm pub:only", - "pub:only": "pnpm ci:publish && git push && git push --follow-tags", - "ci:version": "pnpm changeset version", - "ci:publish": "pnpm build && pnpm changeset publish", - "preinstall": "npx only-allow pnpm", - "clean": "pnpm all clean", - "clean-dist": "pnpm -r clean-dist", - "lint-fix": "NODE_OPTIONS=--max-old-space-size=6144 pnpm -r --no-bail lint-fix", - "lint": "pnpm -r lint", - "circular:dist": "pnpm -r circular:dist", - "test": "pnpm -r --no-bail test:run", - "pr:ready": "node scripts/agent-pr-ready.mjs", - "testsuite": "pnpm -r --no-bail testsuite", - "up-all": "pnpm --recursive update", - "u": "pnpm run update && pnpm i && pnpm dedupe", - "update": "ncu -u && pnpm -r exec ncu -u", - "ncu": "ncu", - "packages": "pnpm -r --workspace-concurrency 1", - "circular-packages": "pnpm packages circular", - "lint-packages": "pnpm packages lint", - "test-packages": "pnpm packages test:run", - "testsuite-packages": "pnpm packages testsuite", - "watch-packages": "pnpm packages build && pnpm packages watch", - "build:tsc": "pnpm check && node scripts/rewrite-dist-declaration-imports.mjs", - "check": "tsgo --build ./tsconfig.all.json", - "watch": "pnpm build:tsc --watch", - "build": "cd packages/eslint-shared-config && pnpm build && cd ../.. && pnpm build:tsc && cd packages/vue-components && pnpm build", - "rbuild": "pnpm clean && pnpm build", - "nnm": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && pnpm i", - "prepare": "node -e \"const v=require('typescript/package.json').version;if(v.startsWith('6.')){require('child_process').execSync('effect-language-service patch',{stdio:'inherit'})}else{console.log('Skipping effect-language-service patch for TypeScript '+v)}\" && effect-tsgo patch --no-typescript --oxlint && node scripts/install-git-hooks.mjs", - "subtree:effect": "node scripts/sync-effect-subtree.js" - }, - "dependencies": { - "fast-check": "^4.9.0", - "vue": "^3.5.35" + "prepare": "node scripts/setup-agents.mjs && effect-tsgo patch", + "clean": "node scripts/clean.mjs", + "codegen": "pnpm --recursive --parallel --filter \"./packages/**/*\" run codegen", + "codemod": "node scripts/codemod.mjs", + "build": "tsc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build && node scripts/copy-ai-docs.mjs", + "bundle-analyze": "bash scripts/bundle-analyze.sh", + "bundle-compare": "bash scripts/bundle-compare.sh", + "bundle-compare-selected": "bash scripts/bundle-compare-selected.sh", + "circular": "node scripts/circular.mjs", + "test": "vitest", + "test-cluster": "vitest run --project cluster-integration", + "doctest": "vitest --config vitest.docs.ts", + "coverage": "vitest --coverage", + "check": "tsc -b tsconfig.json", + "typeperf": "pnpm --dir packages/effect exec node typeperf/run.mjs", + "typeperf-compare": "pnpm --dir packages/effect exec node typeperf/compare.mjs", + "runtimeperf": "pnpm --dir packages/effect exec node runtimeperf/run.mts", + "runtimeperf-compare": "pnpm --dir packages/effect exec node runtimeperf/compare.mts", + "check-recursive": "pnpm --recursive --filter \"./packages/**/*\" exec tsc -b tsconfig.json", + "check-dist-types": "node scripts/check-dist-types.mjs", + "jsdocs": "effect-jsdocs", + "lint": "oxlint -f unix && dprint check", + "lint-fix": "oxlint --fix && dprint fmt", + "ai-docgen": "effect-ai-docgen ai-docs/src -o LLMS.md", + "ai-docgen:watch": "pnpm ai-docgen --watch", + "api-diff": "pnpm --dir packages/tools/api-diff exec node src/bin.ts", + "test-types": "tstyche --target '>=5.9'", + "changeset-version": "changeset version && node scripts/version.mjs", + "changeset-publish": "node scripts/set-strip-internal.mjs && pnpm codemod && pnpm build && changeset publish --tag staging" }, "devDependencies": { - "@changesets/cli": "^2.31.0", - "@effect-app/cli": "workspace:*", - "@effect-app/eslint-codegen-model": "workspace:*", - "@effect-app/infra": "workspace:*", - "@effect/language-service": "0.87.5", - "@effect/tsgo": "^0.56.0", - "@effect/platform-node": "4.0.5", - "@effect/vitest": "4.0.5", - "@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.5", - "effect-app": "workspace:*", - "eslint": "^10.4.1", - "json5": "^2.2.3", - "lint-staged": "^17.3.0", + "@babel/cli": "^8.0.6", + "@babel/core": "^8.0.7", + "@babel/plugin-transform-export-namespace-from": "^8.0.1", + "@babel/plugin-transform-modules-commonjs": "^8.0.1", + "@changesets/changelog-github": "1.0.1", + "@changesets/cli": "3.0.3", + "@effect/ai-docgen": "workspace:^", + "@effect/bundle": "workspace:^", + "@effect/docgen": "workspace:^", + "@effect/doctest": "workspace:^", + "@effect/jsdocs": "workspace:^", + "@effect/oxc": "workspace:^", + "@effect/tsgo": "^0.53.4", + "@effect/utils": "workspace:^", + "@effect/vitest": "workspace:^", + "@faker-js/faker": "^10.6.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@types/jscodeshift": "^17.3.0", + "@types/node": "^26.6.5", + "@vitest/coverage-v8": "^5.0.5", + "@vitest/web-worker": "^5.0.5", + "ast-types": "^0.16.3", + "babel-plugin-annotate-pure-calls": "^0.5.0", + "dprint": "^0.63.0", + "fast-check": "^4.10.2", + "glob": "^13.0.6", + "happy-dom": "^21.0.3", + "jscodeshift": "^17.4.0", + "lalph": "^0.3.139", "madge": "^8.0.0", - "npm-check-updates": "^22.2.1", - "oxlint": "1.89.0", - "oxlint-tsgolint": "7.0.2004", - "pkg-pr-new": "0.0.75", - "ts-plugin-sort-import-suggestions": "^1.0.4", - "typescript": "~6.0.3", - "vite": "^8.0.15", - "vitest": "^5.0.5" + "oxlint": "^1.88.0", + "pkg-pr-new": "0.0.91", + "playwright": "^1.64.1", + "rollup": "^4.64.9", + "rollup-plugin-bundle-stats": "^4.22.3", + "rollup-plugin-esbuild": "^6.2.1", + "rollup-plugin-visualizer": "^7.1.1", + "terser": "^5.51.2", + "tstyche": "^7.2.5", + "typescript": "^7.0.2", + "vite": "^8.3.4", + "vitest": "^5.0.5", + "vitest-websocket-mock": "^0.8.0", + "zod": "^4.6.7" } } diff --git a/repos/effect/packages/ai/anthropic/CHANGELOG.md b/packages/ai/anthropic/CHANGELOG.md similarity index 88% rename from repos/effect/packages/ai/anthropic/CHANGELOG.md rename to packages/ai/anthropic/CHANGELOG.md index d1d417677..f2f101677 100644 --- a/repos/effect/packages/ai/anthropic/CHANGELOG.md +++ b/packages/ai/anthropic/CHANGELOG.md @@ -1,5 +1,25 @@ # @effect/ai-anthropic +## 4.0.6 + +### Patch Changes + +- [#9500](https://github.com/Effect-TS/effect/pull/9500) [`8e9616a`](https://github.com/Effect-TS/effect/commit/8e9616a2b6da0849686ffc11988026411022a408) Thanks @typedrat! - Fix assistant cache breakpoints by sending `cache_control` on the last eligible content block, preserving part-level precedence and skipping thinking blocks. + +- [#9709](https://github.com/Effect-TS/effect/pull/9709) [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117) Thanks @tim-smart! - Add typed HTTP date and `Retry-After` header values to `Headers`, and parse them consistently across the HTTP client, OTLP exporter, GraphQL client, AI clients and static server. + + - `Headers.parseHttpDate` / `Headers.formatHttpDate` read all three RFC 9110 date formats as UTC, whatever the process time zone, and emit IMF-fixdate. + - `Headers.parseRetryAfter`, `Headers.formatRetryAfter` and `Headers.retryAfterDelay` accept whole non-negative seconds of any size, read exactly, or an HTTP date, and the delay is never negative. HTTP dates accept the `23:59:60` leap second, read as the following second. + - `Headers.RetryAfter` holds either a `Duration` (`RetryAfterDuration`) or a `DateTime.Utc` (`RetryAfterDateTime`). `Headers.retryAfterDuration` builds the first from any `Duration.Input`: infinite durations throw, finite negative ones become zero, and fractional seconds are rounded up when formatted. + - Parse failures are reported as `Headers.HeaderParseError`. + - `Schema.DateTimeUtcFromHttpDate` and `Schema.RetryAfterFromString` expose the same parsing as string codecs for HTTP API headers. + - Invalid `Retry-After` values such as `-5`, `1.5`, `1e3` or `Infinity` are now ignored everywhere. `HttpClient.retryTransient` and `HttpClient.withRateLimiter` no longer honour fractional seconds, and the AI clients no longer produce negative or infinite `retryAfter` durations. + - `HttpStaticServer` reads `If-Modified-Since` and `Last-Modified` as UTC; asctime dates were previously shifted by the process time zone. + +- [#9430](https://github.com/Effect-TS/effect/pull/9430) [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310) Thanks @effect-bot! - Add non-retryable `AiError.InvalidResponseError` for provider response schema and SSE decoding failures, with optional HTTP context; model-generated output validation remains retryable. Update exhaustive matches on `AiErrorReason` to handle the new tag, and update serialized-error consumers to accept it. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/ai/anthropic/README.md b/packages/ai/anthropic/README.md similarity index 100% rename from repos/effect/packages/ai/anthropic/README.md rename to packages/ai/anthropic/README.md diff --git a/repos/effect/packages/ai/anthropic/codegen.yml b/packages/ai/anthropic/codegen.yml similarity index 100% rename from repos/effect/packages/ai/anthropic/codegen.yml rename to packages/ai/anthropic/codegen.yml diff --git a/repos/effect/packages/ai/anthropic/package.json b/packages/ai/anthropic/package.json similarity index 98% rename from repos/effect/packages/ai/anthropic/package.json rename to packages/ai/anthropic/package.json index e38e71320..73e99363e 100644 --- a/repos/effect/packages/ai/anthropic/package.json +++ b/packages/ai/anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-anthropic", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "An Anthropic provider integration for Effect AI SDK", diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts b/packages/ai/anthropic/src/AnthropicClient.ts similarity index 99% rename from repos/effect/packages/ai/anthropic/src/AnthropicClient.ts rename to packages/ai/anthropic/src/AnthropicClient.ts index 077d5b3ce..e29e04f53 100644 --- a/repos/effect/packages/ai/anthropic/src/AnthropicClient.ts +++ b/packages/ai/anthropic/src/AnthropicClient.ts @@ -318,7 +318,7 @@ export const make = Effect.fnUntraced( > => client.betaMessagesPost({ ...options, config: { includeResponse: true } }).pipe( Effect.catchTags({ - BetaMessagesPost4XX: (error) => Effect.fail(Errors.mapClientError(error, "createMessage")), + BetaMessagesPost4XX: (error) => Errors.mapClientError(error, "createMessage"), HttpClientError: (error) => Errors.mapHttpClientError(error, "createMessage"), SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createMessage")) }), diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts b/packages/ai/anthropic/src/AnthropicConfig.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/src/AnthropicConfig.ts rename to packages/ai/anthropic/src/AnthropicConfig.ts diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicError.ts b/packages/ai/anthropic/src/AnthropicError.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/src/AnthropicError.ts rename to packages/ai/anthropic/src/AnthropicError.ts diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/packages/ai/anthropic/src/AnthropicLanguageModel.ts similarity index 99% rename from repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts rename to packages/ai/anthropic/src/AnthropicLanguageModel.ts index 8f3e30f94..5d172ff56 100644 --- a/repos/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts +++ b/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -1068,6 +1068,7 @@ const prepareMessages = Effect.fnUntraced( for (let j = 0; j < group.messages.length; j++) { const message = group.messages[j] const isLastMessage = j === group.messages.length - 1 + const messageStart = content.length for (let k = 0; k < message.content.length; k++) { const part = message.content[k] @@ -1077,17 +1078,7 @@ const prepareMessages = Effect.fnUntraced( } const isLastPart = k === message.content.length - 1 - - // Attempt to get the cache control from the part first. If - // the part does not have cache control defined and we are - // evaluating the last part for this message, also check the - // message for cache control. - const cacheControl = getCacheControl(part) ?? ( - isLastPart ? getCacheControl(message) : undefined - ) - - // TODO: use cache_control in content blocks - void cacheControl + const partStart = content.length switch (part.type) { case "text": { @@ -1290,7 +1281,11 @@ const prepareMessages = Effect.fnUntraced( break } } + + setLastCacheControl(content, partStart, getCacheControl(part)) } + + setLastCacheControl(content, messageStart, getCacheControl(message)) } messages.push({ role: "assistant", content }) @@ -2995,6 +2990,28 @@ const getCacheControl = ( | Prompt.ToolMessagePart ): typeof Generated.CacheControlEphemeral.Encoded | null => part.options.anthropic?.cacheControl ?? null +// Skip thinking blocks without crossing the part/message boundary. +// Preserve existing part-level breakpoints over message-level ones. +const setLastCacheControl = ( + content: Array, + start: number, + cacheControl: typeof Generated.CacheControlEphemeral.Encoded | null +): void => { + if (cacheControl === null) { + return + } + for (let i = content.length - 1; i >= start; i--) { + const block = content[i] + if (block.type === "thinking" || block.type === "redacted_thinking") { + continue + } + if (Predicate.isNullish(block.cache_control)) { + content[i] = { ...block, cache_control: cacheControl } + } + return + } +} + const getDocumentMetadata = (part: Prompt.FilePart): { readonly title: string | null readonly context: string | null diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicTelemetry.ts b/packages/ai/anthropic/src/AnthropicTelemetry.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/src/AnthropicTelemetry.ts rename to packages/ai/anthropic/src/AnthropicTelemetry.ts diff --git a/repos/effect/packages/ai/anthropic/src/AnthropicTool.ts b/packages/ai/anthropic/src/AnthropicTool.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/src/AnthropicTool.ts rename to packages/ai/anthropic/src/AnthropicTool.ts diff --git a/repos/effect/packages/ai/anthropic/src/Generated.ts b/packages/ai/anthropic/src/Generated.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/src/Generated.ts rename to packages/ai/anthropic/src/Generated.ts diff --git a/repos/effect/packages/ai/anthropic/src/index.ts b/packages/ai/anthropic/src/index.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/src/index.ts rename to packages/ai/anthropic/src/index.ts diff --git a/repos/effect/packages/ai/anthropic/src/internal/errors.ts b/packages/ai/anthropic/src/internal/errors.ts similarity index 84% rename from repos/effect/packages/ai/anthropic/src/internal/errors.ts rename to packages/ai/anthropic/src/internal/errors.ts index 8576ad8fe..fedae9e22 100644 --- a/repos/effect/packages/ai/anthropic/src/internal/errors.ts +++ b/packages/ai/anthropic/src/internal/errors.ts @@ -1,9 +1,11 @@ import * as AiError from "effect/ai/AiError" import type * as Response from "effect/ai/Response" -import * as Duration from "effect/Duration" +import * as Clock from "effect/Clock" +import type * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import type * as Sse from "effect/encoding/Sse" import { dual } from "effect/Function" +import * as Headers from "effect/http/Headers" import type * as HttpClientError from "effect/http/HttpClientError" import type * as HttpClientRequest from "effect/http/HttpClientRequest" import type * as HttpClientResponse from "effect/http/HttpClientResponse" @@ -11,6 +13,7 @@ import * as Number from "effect/Number" import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import * as Redactable from "effect/Redactable" +import * as Result from "effect/Result" import * as Schema from "effect/Schema" import type { AnthropicErrorMetadata } from "../AnthropicError.ts" import type * as Generated from "../Generated.ts" @@ -50,7 +53,7 @@ export const mapSchemaError = dual< AiError.make({ module: "AnthropicClient", method, - reason: AiError.InvalidOutputError.fromSchemaError(error) + reason: AiError.InvalidResponseError.fromSchemaError(error) })) /** @internal */ @@ -61,31 +64,38 @@ export const mapSseError = dual< AiError.make({ module: "AnthropicClient", method, - reason: new AiError.InvalidOutputError({ description: error.message }) + reason: new AiError.InvalidResponseError({ description: error.message }) })) +type ClientError = Generated.AnthropicClientError + /** @internal */ export const mapClientError = dual< - (method: string) => (error: Generated.AnthropicClientError) => AiError.AiError, - (error: Generated.AnthropicClientError, method: string) => AiError.AiError ->(2, (error, method) => { - const { request, response, cause } = error - const status = response.status - const headers = response.headers as Record - const metadata: AnthropicErrorMetadata = { - errorType: cause.error.type, - requestId: cause.request_id - } - const http = buildHttpContext({ request, response, body: JSON.stringify(cause) }) - const reason = mapStatusCodeToReason({ - status, - headers, - message: cause.error.message, - metadata, - http + (method: string) => (error: ClientError) => Effect.Effect, + (error: ClientError, method: string) => Effect.Effect +>( + 2, + Effect.fnUntraced(function*(error: ClientError, method: string) { + const now = yield* Clock.currentTimeMillis + const { request, response, cause } = error + const status = response.status + const headers = response.headers as Record + const metadata: AnthropicErrorMetadata = { + errorType: cause.error.type, + requestId: cause.request_id + } + const http = buildHttpContext({ request, response, body: JSON.stringify(cause) }) + const reason = mapStatusCodeToReason({ + now, + status, + headers, + message: cause.error.message, + metadata, + http + }) + return yield* AiError.make({ module: "AnthropicClient", method, reason }) }) - return AiError.make({ module: "AnthropicClient", method, reason }) -}) +) /** @internal */ export const mapHttpClientError = dual< @@ -178,7 +188,9 @@ const mapStatusCodeError = Effect.fnUntraced(function*( } const decoded = Schema.decodeUnknownOption(AnthropicErrorBody)(json) + const now = yield* Clock.currentTimeMillis const reason = mapStatusCodeToReason({ + now, status, headers, message: Option.isSome(decoded) ? decoded.value.error.message : undefined, @@ -196,16 +208,16 @@ const mapStatusCodeError = Effect.fnUntraced(function*( // Rate Limits // ============================================================================= +const retryAfterFromHeader = (raw: string, now: number): Duration.Duration | undefined => { + const retryAfter = Headers.parseRetryAfter(raw) + return Result.isSuccess(retryAfter) ? Headers.retryAfterDelay(retryAfter.success, now) : undefined +} + /** @internal */ -export const parseRateLimitHeaders = (headers: Record) => { +export const parseRateLimitHeaders = (headers: Record, now: number) => { const retryAfterRaw = headers["retry-after"] - let retryAfter: Duration.Duration | undefined - if (Predicate.isNotUndefined(retryAfterRaw)) { - const parsed = Number.parse(retryAfterRaw) - if (Option.isSome(parsed)) { - retryAfter = Duration.seconds(parsed.value) - } - } + // an invalid Retry-After is treated as absent + const retryAfter = retryAfterRaw === undefined ? undefined : retryAfterFromHeader(retryAfterRaw, now) const requestsLimitRaw = headers["anthropic-ratelimit-requests-limit"] const requestsRemainingRaw = headers["anthropic-ratelimit-requests-remaining"] const tokensLimitRaw = headers["anthropic-ratelimit-tokens-limit"] @@ -261,8 +273,10 @@ export const buildHttpContext = (params: { // ============================================================================= /** @internal */ -export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http, now }: { readonly status: number + /** Current time in epoch milliseconds, used to measure a Retry-After date. */ + readonly now: number readonly headers: Record readonly message: string | undefined readonly metadata: AnthropicErrorMetadata @@ -312,7 +326,7 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http http }) case 429: { - const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers, now) return new AiError.RateLimitError({ retryAfter, metadata: { diff --git a/repos/effect/packages/ai/anthropic/src/internal/utilities.ts b/packages/ai/anthropic/src/internal/utilities.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/src/internal/utilities.ts rename to packages/ai/anthropic/src/internal/utilities.ts diff --git a/repos/effect/packages/ai/anthropic/test/AnthropicClient.test.ts b/packages/ai/anthropic/test/AnthropicClient.test.ts similarity index 89% rename from repos/effect/packages/ai/anthropic/test/AnthropicClient.test.ts rename to packages/ai/anthropic/test/AnthropicClient.test.ts index e77c4d2f0..a63fe80be 100644 --- a/repos/effect/packages/ai/anthropic/test/AnthropicClient.test.ts +++ b/packages/ai/anthropic/test/AnthropicClient.test.ts @@ -49,6 +49,34 @@ describe("AnthropicClient", () => { body: { error: { message: "gateway rate limit" } } })))) + it.effect.each([ + { name: "ignores a negative Retry-After", header: "-5", expected: undefined }, + { name: "ignores a non-finite Retry-After", header: "Infinity", expected: undefined }, + { name: "honors an HTTP-date Retry-After", header: "Thu, 01 Jan 1970 00:00:30 GMT", expected: 30_000 } + ])("$name", ({ expected, header }) => + Effect.gen(function*() { + const client = yield* AnthropicClient.AnthropicClient + const error = yield* client.createMessage({ + payload: { + model: "claude-sonnet-4-20250514", + max_tokens: 1, + messages: [{ role: "user", content: "hello" }] + } + }).pipe(Effect.flip) + + assert.strictEqual(error.reason._tag, "RateLimitError") + if (error.reason._tag !== "RateLimitError") return + assert.strictEqual( + error.reason.retryAfter === undefined ? undefined : Duration.toMillis(error.reason.retryAfter), + expected + ) + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + status: 429, + headers: { "retry-after": header }, + body: { error: { message: "gateway rate limit" } } + })))) + it.effect("redacts the API key in AI error context", () => Effect.gen(function*() { const client = yield* AnthropicClient.AnthropicClient @@ -127,6 +155,7 @@ describe("AnthropicClient", () => { it("preserves and truncates a fallback HTTP response", () => { const body = `${"a".repeat(200)}b` const reason = Errors.mapStatusCodeToReason({ + now: 0, status: 400, headers: {}, message: undefined, diff --git a/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts b/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts similarity index 87% rename from repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts rename to packages/ai/anthropic/test/AnthropicLanguageModel.test.ts index 2d8975226..a45b0087b 100644 --- a/repos/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts +++ b/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts @@ -1365,56 +1365,55 @@ describe("AnthropicLanguageModel", () => { }) describe("generateObject", () => { - const getRequest = ( + const getRequest = Effect.fnUntraced(function*( model: string, config?: { readonly structuredOutputs?: boolean | undefined } - ) => - Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined - const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( - Layer.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(jsonResponse(request, { - id: "msg_test_1", - type: "message", - role: "assistant", - model, - content: [{ type: "text", text: JSON.stringify({ name: "John", age: 30 }) }], - stop_reason: "end_turn", - stop_sequence: null, - usage: { - cache_creation: null, - cache_creation_input_tokens: null, - cache_read_input_tokens: null, - inference_geo: null, - input_tokens: 10, - output_tokens: 5, - service_tier: null - } - })) - }) - )) - ) - - // Assert the request shape; the response outcome is irrelevant here. - yield* LanguageModel.generateObject({ - prompt: "Give me a person", - schema: Schema.Struct({ name: Schema.String, age: Schema.Number }) - }).pipe( - Effect.provide(AnthropicLanguageModel.model(model, config)), - Effect.provide(layer), - Effect.ignore - ) - - assert.isDefined(capturedRequest) - if (capturedRequest === undefined) { - return yield* Effect.die(new Error("Expected a captured request")) - } + ) { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model, + content: [{ type: "text", text: JSON.stringify({ name: "John", age: 30 }) }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 5, + service_tier: null + } + })) + }) + )) + ) + + // Assert the request shape; the response outcome is irrelevant here. + yield* LanguageModel.generateObject({ + prompt: "Give me a person", + schema: Schema.Struct({ name: Schema.String, age: Schema.Number }) + }).pipe( + Effect.provide(AnthropicLanguageModel.model(model, config)), + Effect.provide(layer), + Effect.ignore + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return yield* Effect.die(new Error("Expected a captured request")) + } - return yield* getRequestBody(capturedRequest) - }) + return yield* getRequestBody(capturedRequest) + }) it.effect("uses native structured output and 128K for Claude 4.6", () => Effect.gen(function*() { @@ -1548,50 +1547,49 @@ describe("AnthropicLanguageModel", () => { }) describe("system messages", () => { - const getRequest = ( + const getRequest = Effect.fnUntraced(function*( model: string, prompt: Prompt.RawInput, config?: { readonly midConversationSystemMessages?: boolean | undefined } - ) => - Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined - const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( - Layer.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(jsonResponse(request, { - id: "msg_test_1", - type: "message", - role: "assistant", - model, - content: [{ type: "text", text: "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, disableToolCallResolution: true }).pipe( - Effect.provide(AnthropicLanguageModel.model(model, config)), - Effect.provide(layer) - ) - - if (capturedRequest === undefined) { - return yield* Effect.die(new Error("Expected a captured request")) - } - return yield* getRequestBody(capturedRequest) - }) + ) { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_1", + type: "message", + role: "assistant", + model, + content: [{ type: "text", text: "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, disableToolCallResolution: true }).pipe( + Effect.provide(AnthropicLanguageModel.model(model, config)), + Effect.provide(layer) + ) + + if (capturedRequest === undefined) { + return yield* Effect.die(new Error("Expected a captured request")) + } + return yield* getRequestBody(capturedRequest) + }) const conversation = Prompt.make([ { role: "system", content: "A" }, @@ -1661,6 +1659,154 @@ describe("AnthropicLanguageModel", () => { )) }) + describe("assistant cache control", () => { + const ephemeral = { type: "ephemeral" } as const + const cached = { anthropic: { cacheControl: ephemeral } } + + const getAssistantContent = Effect.fnUntraced(function*(prompt: Prompt.RawInput) { + 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, disableToolCallResolution: true }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + if (capturedRequest === undefined) { + return yield* Effect.die(new Error("Expected a captured request")) + } + const body = yield* getRequestBody(capturedRequest) + return body.messages.find((message: any) => message.role === "assistant").content as Array + }) + + const thinking = { + type: "reasoning", + text: "Thinking", + options: { anthropic: { info: { type: "thinking", signature: "sig" } } } + } as const + + const redactedThinking = { + type: "reasoning", + text: "", + options: { anthropic: { info: { type: "redacted_thinking", redactedData: "hidden" } } } + } as const + + it.effect("places part and message breakpoints without overriding a part's TTL", () => + Effect.gen(function*() { + const oneHour = { type: "ephemeral", ttl: "1h" } as const + const content = yield* getAssistantContent([ + { role: "user", content: "Question" }, + { + role: "assistant", + content: [ + { type: "text", text: "Answer", options: { anthropic: { cacheControl: oneHour } } }, + { type: "tool-call", id: "toolu_1", name: "lookup", params: {} }, + { + type: "tool-call", + id: "toolu_2", + name: "lookup", + params: {}, + options: { anthropic: { cacheControl: oneHour } } + } + ], + options: cached + }, + { + role: "assistant", + content: [{ type: "tool-call", id: "toolu_3", name: "lookup", params: {} }], + options: cached + }, + { role: "user", content: "Thanks" } + ]) + + assert.deepStrictEqual(content, [ + { type: "text", text: "Answer", cache_control: oneHour }, + { type: "tool_use", id: "toolu_1", name: "lookup", input: {} }, + { type: "tool_use", id: "toolu_2", name: "lookup", input: {}, cache_control: oneHour }, + { type: "tool_use", id: "toolu_3", name: "lookup", input: {}, cache_control: ephemeral } + ]) + })) + + it.effect("places a message breakpoint before trailing thinking and skipped parts", () => + Effect.gen(function*() { + const content = yield* getAssistantContent([ + { role: "user", content: "Question" }, + { + role: "assistant", + content: [ + { type: "text", text: "Answer" }, + thinking, + redactedThinking, + { type: "file", mediaType: "image/png", data: "aGVsbG8=" } + ], + options: cached + }, + { role: "user", content: "Thanks" } + ]) + + assert.deepStrictEqual(content, [ + { type: "text", text: "Answer", cache_control: ephemeral }, + { type: "thinking", thinking: "Thinking", signature: "sig" }, + { type: "redacted_thinking", data: "hidden" } + ]) + })) + + it.effect("drops thinking-only breakpoints without marking other parts or messages", () => + Effect.gen(function*() { + const content = yield* getAssistantContent([ + { role: "user", content: "Question" }, + { role: "assistant", content: "Unmarked" }, + { + role: "assistant", + content: [ + { type: "text", text: "Answer" }, + { ...thinking, options: { anthropic: { ...thinking.options.anthropic, ...cached.anthropic } } }, + { + ...redactedThinking, + options: { anthropic: { ...redactedThinking.options.anthropic, ...cached.anthropic } } + } + ] + }, + { role: "assistant", content: [thinking, redactedThinking], options: cached }, + { role: "user", content: "Thanks" } + ]) + + assert.deepStrictEqual(content, [ + { type: "text", text: "Unmarked" }, + { type: "text", text: "Answer" }, + { type: "thinking", thinking: "Thinking", signature: "sig" }, + { type: "redacted_thinking", data: "hidden" }, + { type: "thinking", thinking: "Thinking", signature: "sig" }, + { type: "redacted_thinking", data: "hidden" } + ]) + })) + }) + // The packaged `Memory_20250818` tool ships `customName: "AnthropicMemory"` / // `providerName: "memory"`, and is a client-executed provider tool. These // tests cover the round-trip that was broken on beta.98 (see #2615): @@ -1837,14 +1983,13 @@ const jsonResponse = ( }) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag !== "Uint8Array") { - return yield* Effect.die(new Error("Expected Uint8Array body")) - } - return JSON.parse(new TextDecoder().decode(body.body)) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag !== "Uint8Array") { + return yield* Effect.die(new Error("Expected Uint8Array body")) + } + return JSON.parse(new TextDecoder().decode(body.body)) +}) const toSseBody = (events: ReadonlyArray): string => events.map((event) => `event: message_stream\ndata: ${JSON.stringify(event)}\n\n`).join("") diff --git a/repos/effect/packages/ai/anthropic/tsconfig.json b/packages/ai/anthropic/tsconfig.json similarity index 100% rename from repos/effect/packages/ai/anthropic/tsconfig.json rename to packages/ai/anthropic/tsconfig.json diff --git a/repos/effect/packages/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts b/packages/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts similarity index 100% rename from repos/effect/packages/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts rename to packages/ai/anthropic/typetest/AnthropicLanguageModel.tst.ts diff --git a/packages/ai/cloudflare/CHANGELOG.md b/packages/ai/cloudflare/CHANGELOG.md new file mode 100644 index 000000000..43ca6ca50 --- /dev/null +++ b/packages/ai/cloudflare/CHANGELOG.md @@ -0,0 +1,61 @@ +# @effect/ai-cloudflare + +## 4.0.6 + +### Patch Changes + +- [#9528](https://github.com/Effect-TS/effect/pull/9528) [`0163546`](https://github.com/Effect-TS/effect/commit/016354639aa7d21d48366f3fd47041992a51d815) Thanks @third774! - Add optional AI Gateway routing via `gatewayId` or `CLOUDFLARE_AI_GATEWAY_ID`, sent as the `cf-aig-gateway-id` header. + +- [#9709](https://github.com/Effect-TS/effect/pull/9709) [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117) Thanks @tim-smart! - Add typed HTTP date and `Retry-After` header values to `Headers`, and parse them consistently across the HTTP client, OTLP exporter, GraphQL client, AI clients and static server. + + - `Headers.parseHttpDate` / `Headers.formatHttpDate` read all three RFC 9110 date formats as UTC, whatever the process time zone, and emit IMF-fixdate. + - `Headers.parseRetryAfter`, `Headers.formatRetryAfter` and `Headers.retryAfterDelay` accept whole non-negative seconds of any size, read exactly, or an HTTP date, and the delay is never negative. HTTP dates accept the `23:59:60` leap second, read as the following second. + - `Headers.RetryAfter` holds either a `Duration` (`RetryAfterDuration`) or a `DateTime.Utc` (`RetryAfterDateTime`). `Headers.retryAfterDuration` builds the first from any `Duration.Input`: infinite durations throw, finite negative ones become zero, and fractional seconds are rounded up when formatted. + - Parse failures are reported as `Headers.HeaderParseError`. + - `Schema.DateTimeUtcFromHttpDate` and `Schema.RetryAfterFromString` expose the same parsing as string codecs for HTTP API headers. + - Invalid `Retry-After` values such as `-5`, `1.5`, `1e3` or `Infinity` are now ignored everywhere. `HttpClient.retryTransient` and `HttpClient.withRateLimiter` no longer honour fractional seconds, and the AI clients no longer produce negative or infinite `retryAfter` durations. + - `HttpStaticServer` reads `If-Modified-Since` and `Last-Modified` as UTC; asctime dates were previously shifted by the process time zone. + +- [#9430](https://github.com/Effect-TS/effect/pull/9430) [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310) Thanks @effect-bot! - Add non-retryable `AiError.InvalidResponseError` for provider response schema and SSE decoding failures, with optional HTTP context; model-generated output validation remains retryable. Update exhaustive matches on `AiErrorReason` to handle the new tag, and update serialized-error consumers to accept it. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + +## 4.0.5 + +### Patch Changes + +- [#9121](https://github.com/Effect-TS/effect/pull/9121) [`bd2c7a3`](https://github.com/Effect-TS/effect/commit/bd2c7a3b8b94859a29b1064f87713dae0b1598f9) Thanks @jem-computer! - Support images in `CloudflareDecisionModel`. Byte arrays, base64 strings, and `data:` URLs are sent to Clef. Remote URLs fail with `AiError.InvalidUserInputError`. + +- [#9124](https://github.com/Effect-TS/effect/pull/9124) [`793d89e`](https://github.com/Effect-TS/effect/commit/793d89efa9201730ba52282a6c53ab12ad50b59e) Thanks @jem-computer! - Support structured decision instructions, descriptions, and score legends in TypeSafe, Cloudflare, and OpenRouter. `Decision.rate` accepts labeled descriptions; `Rate.criteria` entries are now `{ label, description }` objects instead of strings. +- Updated dependencies [[`f665607`](https://github.com/Effect-TS/effect/commit/f665607ee81bee248af7c8129a75692a7ad464e1), [`dbf442b`](https://github.com/Effect-TS/effect/commit/dbf442b914b0ab755b42fabb8d29675508dff8d2), [`74d1930`](https://github.com/Effect-TS/effect/commit/74d19304da077d98cc6dcb294c71e253365f272f), [`a03afb6`](https://github.com/Effect-TS/effect/commit/a03afb6147651b8813f6f6d7218e1e7e2c27d93c), [`f159284`](https://github.com/Effect-TS/effect/commit/f1592843755ec2c41c5b65a0dbe0eb38c96dba82), [`5d301cc`](https://github.com/Effect-TS/effect/commit/5d301cc7aaf23e40cc8b59feea9389f747048d02), [`cd74ea2`](https://github.com/Effect-TS/effect/commit/cd74ea211980de0989ec45f0632cbb0bf07e96ba), [`925cf64`](https://github.com/Effect-TS/effect/commit/925cf641c7070140541aef0d6a7fe6b545f9da23), [`54c51d9`](https://github.com/Effect-TS/effect/commit/54c51d9e96134fe20394336fa7f6fd980b299741), [`995c104`](https://github.com/Effect-TS/effect/commit/995c104f7bfd9fb298d6f921fa21b94fbe53b227), [`816575e`](https://github.com/Effect-TS/effect/commit/816575e11cb8c6d9bc9101f20c16a512b88829d0), [`0df6e06`](https://github.com/Effect-TS/effect/commit/0df6e068b6df2093cefbb1d7ef208d762c5f5b1e), [`52a7d3c`](https://github.com/Effect-TS/effect/commit/52a7d3c8d3ec78caff07e85e7eb88e7c2d67755d), [`ec831c3`](https://github.com/Effect-TS/effect/commit/ec831c3795b060ca6644cabdc0a8f62365f67af7), [`3abc63e`](https://github.com/Effect-TS/effect/commit/3abc63eae45675ad90e52026695366ea0e548c80), [`a841b8b`](https://github.com/Effect-TS/effect/commit/a841b8b144607d3dadddd08c5e6807526d24f584), [`91efea7`](https://github.com/Effect-TS/effect/commit/91efea7860bbb42bd53cd76059538c44fada3217), [`e271997`](https://github.com/Effect-TS/effect/commit/e271997a14a6ae5201c70217f86537818abbe664), [`7719bda`](https://github.com/Effect-TS/effect/commit/7719bda8a5bddd561c5332984b827f02f610be3f), [`563397f`](https://github.com/Effect-TS/effect/commit/563397fbb8484f41eca5ee3e9fee01596a1c6220), [`65b04f8`](https://github.com/Effect-TS/effect/commit/65b04f87b7f0179e6402a3d0e51f039897fdad9b), [`c2859bf`](https://github.com/Effect-TS/effect/commit/c2859bf0176a671154e578653dfb3ba2578a79ff), [`c462214`](https://github.com/Effect-TS/effect/commit/c462214483775e57f32cd4854346f49328043ed2), [`d1ad2c7`](https://github.com/Effect-TS/effect/commit/d1ad2c71a0dace15d14bc6f158ea8de2ccbed866), [`12be65e`](https://github.com/Effect-TS/effect/commit/12be65e5e3a0b2f3231d9ae47665727851d9b7b1), [`352618a`](https://github.com/Effect-TS/effect/commit/352618a562d6f5693e6255a3dcc0c15929c01d88), [`2a8ec8b`](https://github.com/Effect-TS/effect/commit/2a8ec8be832a91292a4c378ac0f94b8dfd547f69), [`8dec89d`](https://github.com/Effect-TS/effect/commit/8dec89d318297fb27916330ea01b8ff17c1faacb), [`f05642f`](https://github.com/Effect-TS/effect/commit/f05642f77734e192c6a382e5cfe225cf48a19c97), [`3ad9a0c`](https://github.com/Effect-TS/effect/commit/3ad9a0c6334f579ee38650060f2690c4273dea87), [`dba4e52`](https://github.com/Effect-TS/effect/commit/dba4e52f505e10730623a7610413156d82fee6fe), [`7ca8224`](https://github.com/Effect-TS/effect/commit/7ca8224ebb87a603f7a3eaee7470537067fe2bc0), [`65d20dd`](https://github.com/Effect-TS/effect/commit/65d20dd6e85a4d1af2ef964625832ae38fa60d8d), [`e3ff963`](https://github.com/Effect-TS/effect/commit/e3ff963eb59eb6a71316b3581c35bcffaaa2c263), [`bc454a3`](https://github.com/Effect-TS/effect/commit/bc454a377dda3b92a24359c7b92810e47694d97a), [`793d89e`](https://github.com/Effect-TS/effect/commit/793d89efa9201730ba52282a6c53ab12ad50b59e), [`ec9332b`](https://github.com/Effect-TS/effect/commit/ec9332ba2b79a3539579e3241da7b053a5e1e11a), [`f2b3d8a`](https://github.com/Effect-TS/effect/commit/f2b3d8a573d9189d46cf8ab2c209512dabc89331), [`cb2cf5f`](https://github.com/Effect-TS/effect/commit/cb2cf5f19dd23cc65a5cd826844fe7b11e95f4f6), [`b8c201d`](https://github.com/Effect-TS/effect/commit/b8c201d0f492f669bbdbdf5d6f1834abe25ed1fc), [`118c9c4`](https://github.com/Effect-TS/effect/commit/118c9c4fe5fa5bac292bed88c01a7c10fe6ed2e6)]: + - effect@4.0.5 + +## 4.0.4 + +### Patch Changes + +- Updated dependencies [[`7547a99`](https://github.com/Effect-TS/effect/commit/7547a99f81fa6f7e651d52f3d42081f399bfbca8), [`711719d`](https://github.com/Effect-TS/effect/commit/711719d28d78dddda4d1685b916d1edf1eae9e98), [`8f1ea52`](https://github.com/Effect-TS/effect/commit/8f1ea52f0c073daf445adb987d23860dd76a147c), [`c93485e`](https://github.com/Effect-TS/effect/commit/c93485e365555763b8cf043d2e2a1399ac5bb236), [`37e088f`](https://github.com/Effect-TS/effect/commit/37e088f2af8dc03271bb9e2ca2874b750487b0f5), [`9314666`](https://github.com/Effect-TS/effect/commit/93146666ec49cd6831e9c9d5fa0f9bc72730aaa7), [`a0a26b7`](https://github.com/Effect-TS/effect/commit/a0a26b789c5e865a2f2baf0b29b7b369b100d8be), [`cc843ea`](https://github.com/Effect-TS/effect/commit/cc843ea091e68a57d91f39fc51baff094c50e41d), [`ab90be9`](https://github.com/Effect-TS/effect/commit/ab90be98ea693df4c0d647cf29a4b913ecbef8f3), [`a59b971`](https://github.com/Effect-TS/effect/commit/a59b9711aff7b1af8ab3838b23d0981fb53d7a9a), [`fccf660`](https://github.com/Effect-TS/effect/commit/fccf660d83f1db48087cc32da767c43d8e385f64), [`1bfd8fd`](https://github.com/Effect-TS/effect/commit/1bfd8fddebc978643b7a2077a837b82dc8998177), [`78e4c43`](https://github.com/Effect-TS/effect/commit/78e4c435ede00c7757c3507e60feb821edc85b5f), [`fde67a1`](https://github.com/Effect-TS/effect/commit/fde67a10e9245aa3974ecdca00239de8dfd2cc9f), [`118deef`](https://github.com/Effect-TS/effect/commit/118deefe7c02089b1bc5d202ce3225f05a98cd04), [`cea74c9`](https://github.com/Effect-TS/effect/commit/cea74c92ac839009c6ba112504689d8726f648d7), [`b7c1ac6`](https://github.com/Effect-TS/effect/commit/b7c1ac683775f37c78061da3304d37a800d227d6), [`f837966`](https://github.com/Effect-TS/effect/commit/f8379667c914cc5bbc5acfd41eee0090ee247925), [`09738f4`](https://github.com/Effect-TS/effect/commit/09738f4aa61f4e188d0b1db76b405618f0e31d94), [`22e7129`](https://github.com/Effect-TS/effect/commit/22e7129023f15aea70ec3bb5ab92c93aad605b4e), [`fdcb47e`](https://github.com/Effect-TS/effect/commit/fdcb47e64304b52299bad350aa57722fa9e3c013), [`fc349a1`](https://github.com/Effect-TS/effect/commit/fc349a13f77c5cc4c94752546913b90be8d0bfaa), [`ab90be9`](https://github.com/Effect-TS/effect/commit/ab90be98ea693df4c0d647cf29a4b913ecbef8f3), [`892d14d`](https://github.com/Effect-TS/effect/commit/892d14d88b84f842457494792bf702eb15d5b491), [`077c557`](https://github.com/Effect-TS/effect/commit/077c55770db747f04213af82bb4651376ea04447), [`4a999a3`](https://github.com/Effect-TS/effect/commit/4a999a34e81ef754b959f6ded75b272503bc1163), [`797696a`](https://github.com/Effect-TS/effect/commit/797696a2b7b5b3c73e01b8d72d9a327e49a39708), [`cf5179b`](https://github.com/Effect-TS/effect/commit/cf5179b48867597342ccc9912252ac75637aec06), [`e1c6053`](https://github.com/Effect-TS/effect/commit/e1c6053c5e813596a24793c90cbf732b63c98852)]: + - effect@4.0.4 + +## 4.0.3 + +### Patch Changes + +- Updated dependencies [[`b4e5af8`](https://github.com/Effect-TS/effect/commit/b4e5af825ac4c869d09c008b2d1b58cdd0ce4c02), [`b8d6e7a`](https://github.com/Effect-TS/effect/commit/b8d6e7a89b8c2283aebbfa154963efccd7addbd7), [`89d5c88`](https://github.com/Effect-TS/effect/commit/89d5c8881024e31b08344bf9f992be0e5d024e4f), [`66257d2`](https://github.com/Effect-TS/effect/commit/66257d29224e949f7b300ff33416098519c7e86a), [`cd2ab19`](https://github.com/Effect-TS/effect/commit/cd2ab19600d00e1e9c84028f29dcb22978e8c681), [`ca068ce`](https://github.com/Effect-TS/effect/commit/ca068cef445101cbb2710c24360ca3a7ab9b94d5), [`f686000`](https://github.com/Effect-TS/effect/commit/f686000760c2d3ff27aea5767e73c1618c5d21c0), [`e052f1b`](https://github.com/Effect-TS/effect/commit/e052f1b30b379f120854f9b2394efa7ac44fc01f), [`c755b69`](https://github.com/Effect-TS/effect/commit/c755b69701e29e055ef5c4c87ced83e15b3faa81), [`ebe487f`](https://github.com/Effect-TS/effect/commit/ebe487f88846a131faa0c61a5f65f80187782e52), [`63bfad8`](https://github.com/Effect-TS/effect/commit/63bfad88fc60de2d8c21dc7f02a62f48a0b5281e), [`8598157`](https://github.com/Effect-TS/effect/commit/8598157653e46fb12af342cb8bd6d30122afa4fa), [`76a03f7`](https://github.com/Effect-TS/effect/commit/76a03f759980de88cdcb7c906fca92a6212b760e), [`182e26d`](https://github.com/Effect-TS/effect/commit/182e26dae1c8d16adf0372b6b106babb87d0f7b0), [`a46592a`](https://github.com/Effect-TS/effect/commit/a46592aed944b046e562815083b367ad28c8eaee), [`1d3419e`](https://github.com/Effect-TS/effect/commit/1d3419e76ca8ad7c2504b9ec75b281ef3dff6b60), [`e00eaae`](https://github.com/Effect-TS/effect/commit/e00eaae8d9d503bea1871031670c28f82935c303), [`133ceb2`](https://github.com/Effect-TS/effect/commit/133ceb2b77331742f381c09552478a86975826fa), [`a494a64`](https://github.com/Effect-TS/effect/commit/a494a647885d8eacff4fce0ebde1263e8b261be6), [`d6082a2`](https://github.com/Effect-TS/effect/commit/d6082a25a3779f7e2abba2787323d5d9c7e1d24a), [`ee3a350`](https://github.com/Effect-TS/effect/commit/ee3a35076854eb6a49183bf8d5ac610e46393d02), [`7ba976c`](https://github.com/Effect-TS/effect/commit/7ba976c620d62125ecc7924be379308cf9b87cd7), [`9dbd07e`](https://github.com/Effect-TS/effect/commit/9dbd07e0063301b2361e04dd9b3697d5469ab85e), [`d4f7dc3`](https://github.com/Effect-TS/effect/commit/d4f7dc3300a04b45800bd5331bd8282ce1dd0ea0), [`a9b5148`](https://github.com/Effect-TS/effect/commit/a9b514822573c0e27452d80bcf35a6fa036d32fb), [`e7d84e4`](https://github.com/Effect-TS/effect/commit/e7d84e449a0e75d1d04486ea81dfb58531fd6503), [`0e2b988`](https://github.com/Effect-TS/effect/commit/0e2b988eec37873bf37f056e2b1b1e52193c4a7a), [`84f48df`](https://github.com/Effect-TS/effect/commit/84f48df586ef1be9290910e3f37b5bb2811b9545), [`48ed32f`](https://github.com/Effect-TS/effect/commit/48ed32f846c11ab702b22c84ed103c767d0dd21c), [`59e58a6`](https://github.com/Effect-TS/effect/commit/59e58a6b9839b475e02f2f551dd4c1328b548911), [`b9e7821`](https://github.com/Effect-TS/effect/commit/b9e7821be3efd61b5ac01f8ced0818e50ed7f0e5), [`aa811e4`](https://github.com/Effect-TS/effect/commit/aa811e454b61033ec150204d959bbf09e44e2112), [`bfc5742`](https://github.com/Effect-TS/effect/commit/bfc5742e18945026e4360e928e311042d8e47ab7), [`f9f7a5a`](https://github.com/Effect-TS/effect/commit/f9f7a5a16e1f7685b6653a71cd0265ad027c69df), [`73fa287`](https://github.com/Effect-TS/effect/commit/73fa2873cb1269874eec8998d2f5ce9d44b0bb0b), [`7ff5048`](https://github.com/Effect-TS/effect/commit/7ff5048db51daba7c31ee90d5e3d17861a9e6b50), [`7803e51`](https://github.com/Effect-TS/effect/commit/7803e51e940727382c8af049f4744955fa21f53a), [`93410da`](https://github.com/Effect-TS/effect/commit/93410da528b46607caf006f8c8d37dc6cee94530), [`aef1d1f`](https://github.com/Effect-TS/effect/commit/aef1d1f2e59cdfd85766a2b168bb3ec1b64538a1), [`d6d515b`](https://github.com/Effect-TS/effect/commit/d6d515bafc83e02bee055231b52fdce2ea24d88d), [`ea52c24`](https://github.com/Effect-TS/effect/commit/ea52c245f64b378628ca290bf99bdb6cd033809c), [`7e44402`](https://github.com/Effect-TS/effect/commit/7e444029d3101640ea2edbffafa882097b78bb68), [`0b17b9e`](https://github.com/Effect-TS/effect/commit/0b17b9e2e43c2f95726470aee188fd9e2bf1baf6), [`9753be9`](https://github.com/Effect-TS/effect/commit/9753be9ecf8dfa9c58169f41a4b876d638b9cc08), [`977096b`](https://github.com/Effect-TS/effect/commit/977096bcea55317ef0e845218c818430cc6e5318), [`cf58cb0`](https://github.com/Effect-TS/effect/commit/cf58cb0362972b4b46b762d8c9062ec7349b438e)]: + - effect@4.0.3 + +## 4.0.2 + +### Patch Changes + +- [#8770](https://github.com/Effect-TS/effect/pull/8770) [`540ecd1`](https://github.com/Effect-TS/effect/commit/540ecd1a94742aeacfb9ef39ebd0cbf7e61a6dfa) Thanks @fubhy! - Tag every module and directly importable export with an explicit `@stability`. `@stability stable` follows strict semver, and `@stability unstable` may receive breaking changes in minor releases. +- Updated dependencies [[`26c1089`](https://github.com/Effect-TS/effect/commit/26c108919f73b92aa8889e367d3a5a9161a652f1), [`6c59de7`](https://github.com/Effect-TS/effect/commit/6c59de782b9c5b5cdb53fcde0455984bbfd9ff63), [`467d3ac`](https://github.com/Effect-TS/effect/commit/467d3aceff3e8c98ba7dd4ffef9a267a8f14d105), [`375c1b0`](https://github.com/Effect-TS/effect/commit/375c1b021c6a3d794e798112d5486821586173f3), [`45a70ce`](https://github.com/Effect-TS/effect/commit/45a70ced8ca1c4f1e6b489e9fe3e050bf3364058), [`fb757e3`](https://github.com/Effect-TS/effect/commit/fb757e3eaef9ddfb9f63e5db788d12af8209be91), [`716e358`](https://github.com/Effect-TS/effect/commit/716e358e0ed263ea26da20f834cea69b6d5b1584), [`af114e6`](https://github.com/Effect-TS/effect/commit/af114e664b1f2f3f50794b1176f3531c2070cda6), [`d0b3862`](https://github.com/Effect-TS/effect/commit/d0b3862b6525d09277b8fb48d981beb200ad09f5), [`7064da9`](https://github.com/Effect-TS/effect/commit/7064da92f2716f3e8d57e31695ad7d50c7ca2023), [`eee0eeb`](https://github.com/Effect-TS/effect/commit/eee0eeb1e40e47cfa6a4dfb0f07bb037c0f1ce02), [`b9d6581`](https://github.com/Effect-TS/effect/commit/b9d65819314d7eedca17bbafec6ae6377cab6740), [`7a721ce`](https://github.com/Effect-TS/effect/commit/7a721cee6b6b251642a6cc495c3fbd92a9ae56ff), [`a0cb4c2`](https://github.com/Effect-TS/effect/commit/a0cb4c25d86f61c8522ad24d6abf418e23342204), [`3cb395e`](https://github.com/Effect-TS/effect/commit/3cb395e15156aaa7e36f0bbfaaebe61a3966a319), [`540ecd1`](https://github.com/Effect-TS/effect/commit/540ecd1a94742aeacfb9ef39ebd0cbf7e61a6dfa), [`aefe05a`](https://github.com/Effect-TS/effect/commit/aefe05aea1d6f2fb9da0f313752a4efee5779eb3), [`c381fbc`](https://github.com/Effect-TS/effect/commit/c381fbc0451e450f7a650eaca848635b8c967302), [`c6f2179`](https://github.com/Effect-TS/effect/commit/c6f2179d3d104f934905d273668ce0a0b6871ef9), [`0aeb7fd`](https://github.com/Effect-TS/effect/commit/0aeb7fd6f5fad365f957f321721781a096fe4f1c), [`2fbecc8`](https://github.com/Effect-TS/effect/commit/2fbecc82c3817fb0a5fc12883091f343496695c4), [`7095539`](https://github.com/Effect-TS/effect/commit/7095539c2328b07bdefddb1a708338f3325baea6), [`46b75b1`](https://github.com/Effect-TS/effect/commit/46b75b1a7c8d4e1883454e5adc52ebaec992e535), [`99b6b45`](https://github.com/Effect-TS/effect/commit/99b6b45aff2d7b1a1ec65f2c282c927681cbf676), [`e4cb9f1`](https://github.com/Effect-TS/effect/commit/e4cb9f1f58e18f443d883863240ad19dca4bffde), [`2e03688`](https://github.com/Effect-TS/effect/commit/2e03688cdf32194a30cc43bdcc85700fe211a6db), [`b817ebe`](https://github.com/Effect-TS/effect/commit/b817ebefc0fc88d2bcfbef803bdf27298888df5e), [`4153144`](https://github.com/Effect-TS/effect/commit/4153144de56b785409efadae655b4889452d0e10), [`2b3c63c`](https://github.com/Effect-TS/effect/commit/2b3c63c14f89c6d82f9829e214151da12f7931f6), [`bd00773`](https://github.com/Effect-TS/effect/commit/bd00773b252970e576ffe0cce17b84c10ce3c81f), [`146e86a`](https://github.com/Effect-TS/effect/commit/146e86a6894ee2485b43db22dfe767822829ded4), [`44defad`](https://github.com/Effect-TS/effect/commit/44defad43f1f4c396fcd9373484be191506d7df8), [`22d6f4d`](https://github.com/Effect-TS/effect/commit/22d6f4dcd413d86ed81719b6cd5678cb5c88048f), [`7d8d497`](https://github.com/Effect-TS/effect/commit/7d8d4979c6c3e9448c83b1e34c21735e76e5bb2e), [`98a5ed7`](https://github.com/Effect-TS/effect/commit/98a5ed7603de413c0939c9ae66503ebe462846cf), [`d350f8e`](https://github.com/Effect-TS/effect/commit/d350f8e7ba7bb68aff2e029fbca7d7a9017fdd60), [`bd96be0`](https://github.com/Effect-TS/effect/commit/bd96be0f1c9d842e381ed437ec319c5401cbb9be), [`f6af4b6`](https://github.com/Effect-TS/effect/commit/f6af4b6d24b866c1fe665a7af5562b329160a39d), [`7234786`](https://github.com/Effect-TS/effect/commit/723478668709ccefcec406522e2fce2431c653c9), [`1f74b62`](https://github.com/Effect-TS/effect/commit/1f74b62b6c918bdea3e4416e701c490fcf6866ce), [`42763ce`](https://github.com/Effect-TS/effect/commit/42763ce0b43aa9006a7d572a56b0955477dd4e2b), [`5630677`](https://github.com/Effect-TS/effect/commit/5630677ff5df3e868f8edaf8c67504f6b7f277ad), [`a0e1b87`](https://github.com/Effect-TS/effect/commit/a0e1b871b445a7dadbae33bca3644119e1375710), [`f5716ad`](https://github.com/Effect-TS/effect/commit/f5716ad55d84dfa4aa935008ba7e336dff5bf3bb), [`26931d0`](https://github.com/Effect-TS/effect/commit/26931d0fee36119a2e8b61480c0b1137a5f55c03), [`972c80f`](https://github.com/Effect-TS/effect/commit/972c80fb023a18bd8259b62f227420b2042ed3a9), [`efebfa9`](https://github.com/Effect-TS/effect/commit/efebfa9cf9fb21ad8ac8156ecf6ca555ca55efa3), [`a3b4cf3`](https://github.com/Effect-TS/effect/commit/a3b4cf3e2b35b336d05116ec464c4883c7aaf878), [`3ed997b`](https://github.com/Effect-TS/effect/commit/3ed997b1f43cd022840013a53ec914f4cf63de12), [`7b396bd`](https://github.com/Effect-TS/effect/commit/7b396bd3b50d07457f5c55d84628f93f98215a42), [`c07c144`](https://github.com/Effect-TS/effect/commit/c07c144b1a7c17fc908cd386b186bd951614c10f), [`8903b68`](https://github.com/Effect-TS/effect/commit/8903b68addaded54f78d9cff34821665f583c40f), [`a84aa4e`](https://github.com/Effect-TS/effect/commit/a84aa4e863dd1f03cc1316e52357b689bad4bade), [`88917eb`](https://github.com/Effect-TS/effect/commit/88917ebb2e30aee2d03e24766b3e8624309b664e), [`50a5686`](https://github.com/Effect-TS/effect/commit/50a5686ab6ab0627572ed86a34288560cb9f8321), [`c419a0e`](https://github.com/Effect-TS/effect/commit/c419a0edaab68290d53fea7543e22eb1506dcf23), [`d85a309`](https://github.com/Effect-TS/effect/commit/d85a3095af16b8f69b623f0436fe57935cda86cf), [`7e7ede6`](https://github.com/Effect-TS/effect/commit/7e7ede67d838a10bf7b9324a6fcc0070cc8241d2), [`1803500`](https://github.com/Effect-TS/effect/commit/18035009c92bf4aed95c92b0d34392042aa66469), [`495740d`](https://github.com/Effect-TS/effect/commit/495740d9d0134bde99a24e0dc96fdb197002beec), [`985860d`](https://github.com/Effect-TS/effect/commit/985860df71322250ec93daf5d776dcf27f10be29), [`e36abbb`](https://github.com/Effect-TS/effect/commit/e36abbba72674cfe366747ae52639448e48da1ec), [`c6086ee`](https://github.com/Effect-TS/effect/commit/c6086ee5a2fc4176943ac5a3062000e0cb4c082c), [`0ac6ac8`](https://github.com/Effect-TS/effect/commit/0ac6ac813c26c0745b3258a50f921b5ceeb0c2c4), [`90a1bae`](https://github.com/Effect-TS/effect/commit/90a1bae8987dfcf94db7302a109d48b3a3ce098c), [`b1d200c`](https://github.com/Effect-TS/effect/commit/b1d200c40a1dad69def51ebdbf0a1a612a12b8ac), [`1ba8521`](https://github.com/Effect-TS/effect/commit/1ba85215823b3ad9fe725619ecca4c2c39f3548b), [`1b35732`](https://github.com/Effect-TS/effect/commit/1b35732952db4e5600462bab154a1cd382e4bccf), [`763b445`](https://github.com/Effect-TS/effect/commit/763b445f553d96a49045fd2163c87d9305fa6761), [`00f1cc7`](https://github.com/Effect-TS/effect/commit/00f1cc783f5e90e747ba0dad98468311b76eeb30), [`d4f4bc8`](https://github.com/Effect-TS/effect/commit/d4f4bc8b83c303c74a85e5f4b9277833b631fcd0), [`f11526a`](https://github.com/Effect-TS/effect/commit/f11526aea2c65fdd110a1ddce13c510da08a9dea), [`8781388`](https://github.com/Effect-TS/effect/commit/87813882fafcd8e7ac6231f825c139df64636d23), [`5326527`](https://github.com/Effect-TS/effect/commit/53265279182da264091a278013dfd7fc430e89a2), [`8409eb4`](https://github.com/Effect-TS/effect/commit/8409eb45925e23c10a9fb5b9bd31baaafca25d81), [`c5cf8ce`](https://github.com/Effect-TS/effect/commit/c5cf8ce40217e84bcd0f53c0125d9e3213e45a4e), [`684ccdc`](https://github.com/Effect-TS/effect/commit/684ccdc20b7803f3c3be054f336fbf8c1ed78304), [`33303ea`](https://github.com/Effect-TS/effect/commit/33303ea6347d9db747d57f0b3cfa400b0c9225d1), [`b79fcb8`](https://github.com/Effect-TS/effect/commit/b79fcb8b628d4800a6dcadcad76befa4d21b9562), [`888a189`](https://github.com/Effect-TS/effect/commit/888a18915cabb00b9d81a118569f3f7c4d911dfb), [`9b8200d`](https://github.com/Effect-TS/effect/commit/9b8200da0fc8d60ece93f3c9f60f584436dcf43e), [`82303c1`](https://github.com/Effect-TS/effect/commit/82303c1bc81e954cf518173ee264efa027aff93f), [`76fcd89`](https://github.com/Effect-TS/effect/commit/76fcd898c57a633ab993af1326e57d7f2ece7ca6), [`f31e6b7`](https://github.com/Effect-TS/effect/commit/f31e6b7fd2d85baaed21ec38e9ebf308f6b18a15), [`332d363`](https://github.com/Effect-TS/effect/commit/332d363f07ad1d83ef533f9e73688572b4777d75), [`a6c7dcb`](https://github.com/Effect-TS/effect/commit/a6c7dcb042893ef654ee3ff10950d4958b7b1cc3)]: + - effect@4.0.2 + +## 4.0.1 + +### Patch Changes + +- [#8677](https://github.com/Effect-TS/effect/pull/8677) [`480bba2`](https://github.com/Effect-TS/effect/commit/480bba2dfebba5b0e58695a9837895dfb65895ee) Thanks @danieljvdm! - Add a Cloudflare Workers AI decision provider for Clef and Clef-flash. +- Updated dependencies [[`ed2cc2f`](https://github.com/Effect-TS/effect/commit/ed2cc2f322dfd24da550c8d0ac811c3130b79d74), [`58a2ee8`](https://github.com/Effect-TS/effect/commit/58a2ee8e0db8e0a90bae83c6cceeff6916772fee), [`bc44526`](https://github.com/Effect-TS/effect/commit/bc445262eeaaba6f7f28a154023a590e193e547d), [`073bb47`](https://github.com/Effect-TS/effect/commit/073bb475d1e84e09cdcd8303a4a558f2a73d8396), [`2ec021e`](https://github.com/Effect-TS/effect/commit/2ec021eff7fbd2206adc3e8bf4e803ed4bf07d6a), [`cad1118`](https://github.com/Effect-TS/effect/commit/cad111867a666cebe8ff9bf2237a997fa6e4cb14), [`1672d9c`](https://github.com/Effect-TS/effect/commit/1672d9ca8da3e5b536bf7afb73365cccc6c01f53), [`a81e4e6`](https://github.com/Effect-TS/effect/commit/a81e4e6cb3dfbf01b1c53c1c877fddeaced8a278), [`f5c2a91`](https://github.com/Effect-TS/effect/commit/f5c2a91621401a018b770446d35ef99806c5561f), [`498e72b`](https://github.com/Effect-TS/effect/commit/498e72ba14d0191d6bf66cc0e931c229a1ff442f), [`cabf123`](https://github.com/Effect-TS/effect/commit/cabf1230723e92684a4b0b836ccfde25c657f9f1), [`a5b1d4e`](https://github.com/Effect-TS/effect/commit/a5b1d4e4cae01dbe30890d3796f846bda00ad2ae), [`878cb33`](https://github.com/Effect-TS/effect/commit/878cb33f4f5f5a31d2c80a9ddb7d62a611230e1c), [`5e6f756`](https://github.com/Effect-TS/effect/commit/5e6f7568a4c895eab3087249b191c5dcba5e73ef), [`18dfb98`](https://github.com/Effect-TS/effect/commit/18dfb98cf6d2aeedbab8ef5734f9bc273a82d56e), [`04706ef`](https://github.com/Effect-TS/effect/commit/04706eff5da94b4d475b99eab8b0256bed97b080), [`f157482`](https://github.com/Effect-TS/effect/commit/f15748273a89afff4b05d5e6bec22c567c2a3cda), [`b5a2d4c`](https://github.com/Effect-TS/effect/commit/b5a2d4c1d62c9620a68d72b7f20248c69ef7663b), [`1c18c68`](https://github.com/Effect-TS/effect/commit/1c18c688df10b697c10283160dba143e049f3d50), [`43416c4`](https://github.com/Effect-TS/effect/commit/43416c4142f973f4988440716eb54a498659cd3a), [`d6ac8f9`](https://github.com/Effect-TS/effect/commit/d6ac8f923a33ff298a87f31230735e71d2d082e9), [`1d9fb8c`](https://github.com/Effect-TS/effect/commit/1d9fb8c540a11326650baa0866a17951665189e7), [`20dcd50`](https://github.com/Effect-TS/effect/commit/20dcd5060581077b46325a433f787ba10a56963d), [`42e5750`](https://github.com/Effect-TS/effect/commit/42e5750f7e2fbf349cfe45452d7cc9dce98d6fe9), [`888e326`](https://github.com/Effect-TS/effect/commit/888e326a3180aea9b117fe7c97aace5dd527596e), [`0b52bb4`](https://github.com/Effect-TS/effect/commit/0b52bb42fac5aecb8bcb8d899293c641b62c3f66), [`e823e69`](https://github.com/Effect-TS/effect/commit/e823e693773aa163c0081af00a5236118446c6ae), [`962647b`](https://github.com/Effect-TS/effect/commit/962647b6a16000f882e6999f001c513a8cccc711), [`43fdcdc`](https://github.com/Effect-TS/effect/commit/43fdcdc6c81b6ad22d54061d6c6c45d9243d7c33), [`1f89b8e`](https://github.com/Effect-TS/effect/commit/1f89b8ec64e6362da3eaaff8cbb235df88d3441c), [`e1a521a`](https://github.com/Effect-TS/effect/commit/e1a521a6886a281abee016ff9aa57c3b172057c8)]: + - effect@4.0.1 diff --git a/repos/effect/packages/ai/cloudflare/README.md b/packages/ai/cloudflare/README.md similarity index 89% rename from repos/effect/packages/ai/cloudflare/README.md rename to packages/ai/cloudflare/README.md index 35a9d0f27..041eeb3b1 100644 --- a/repos/effect/packages/ai/cloudflare/README.md +++ b/packages/ai/cloudflare/README.md @@ -57,3 +57,7 @@ Instructions and descriptions can be strings, JSON objects, or JSON arrays. A ra The client unwraps Cloudflare's REST envelope and maps failures to `AiError`. It does not retry automatically. Rate-limit errors include `Retry-After` when provided. The decision model validates answers and rescales small probability drift from four-decimal rounding. Cloudflare limits each request to 64 questions, classification to 2–255 choices, and ratings to 2–10 levels. See the [Clef API documentation](https://developers.cloudflare.com/workers-ai/models/clef/) for request limits. + +## AI Gateway + +Set `CLOUDFLARE_AI_GATEWAY_ID` when using `CloudflareClient.layerConfig()` to route requests through a Cloudflare AI Gateway. You can also pass `gatewayId` to `CloudflareClient.layer`. The client adds the `cf-aig-gateway-id` header. Use `transformClient` to add other `cf-aig-*` headers. diff --git a/repos/effect/packages/ai/cloudflare/package.json b/packages/ai/cloudflare/package.json similarity index 98% rename from repos/effect/packages/ai/cloudflare/package.json rename to packages/ai/cloudflare/package.json index d8c9a520a..3d8ff105b 100644 --- a/repos/effect/packages/ai/cloudflare/package.json +++ b/packages/ai/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-cloudflare", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "A Cloudflare Workers AI decision provider for Effect", diff --git a/repos/effect/packages/ai/cloudflare/src/CloudflareClient.ts b/packages/ai/cloudflare/src/CloudflareClient.ts similarity index 86% rename from repos/effect/packages/ai/cloudflare/src/CloudflareClient.ts rename to packages/ai/cloudflare/src/CloudflareClient.ts index 73eba727b..2e58de058 100644 --- a/repos/effect/packages/ai/cloudflare/src/CloudflareClient.ts +++ b/packages/ai/cloudflare/src/CloudflareClient.ts @@ -44,7 +44,7 @@ export class CloudflareClient {} /** - * Account, authentication, API base URL, and HTTP customization options. + * Account, authentication, AI Gateway, API base URL, and HTTP customization options. * * @stability unstable * @category options @@ -53,6 +53,7 @@ export class CloudflareClient export interface Options { readonly accountId: string readonly apiKey: Redacted.Redacted + readonly gatewayId?: string | undefined readonly apiUrl?: string | undefined readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined } @@ -76,7 +77,10 @@ export const make = Effect.fnUntraced( HttpClient.mapRequest(flow( HttpClientRequest.prependUrl(options.apiUrl ?? "https://api.cloudflare.com/client/v4"), HttpClientRequest.bearerToken(Redacted.value(options.apiKey)), - HttpClientRequest.acceptJson + HttpClientRequest.acceptJson, + options.gatewayId === undefined + ? identity + : HttpClientRequest.setHeader("cf-aig-gateway-id", options.gatewayId) )), HttpClient.filterStatusOk, options.transformClient ?? identity @@ -105,7 +109,7 @@ export const make = Effect.fnUntraced( Effect.fail(AiError.make({ module: "CloudflareClient", method: "createDecisions", - reason: AiError.InvalidOutputError.fromSchemaError(error) + reason: AiError.InvalidResponseError.fromSchemaError(error) })) }) ) @@ -124,7 +128,8 @@ export const layer = (options: Options): Layer.Layer | undefined readonly apiKey?: Config.Config> | undefined + readonly gatewayId?: Config.Config | undefined readonly apiUrl?: Config.Config | undefined readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined }): Layer.Layer => @@ -142,6 +148,8 @@ export const layerConfig = (options?: { return yield* make({ accountId: yield* (options?.accountId ?? Config.String("CLOUDFLARE_ACCOUNT_ID")), apiKey: yield* (options?.apiKey ?? Config.Redacted("CLOUDFLARE_API_TOKEN")), + gatewayId: yield* (options?.gatewayId ?? + Config.String("CLOUDFLARE_AI_GATEWAY_ID").pipe(Config.withDefault(undefined))), apiUrl: options?.apiUrl ? yield* options.apiUrl : undefined, transformClient: options?.transformClient }) diff --git a/repos/effect/packages/ai/cloudflare/src/CloudflareDecisionModel.ts b/packages/ai/cloudflare/src/CloudflareDecisionModel.ts similarity index 100% rename from repos/effect/packages/ai/cloudflare/src/CloudflareDecisionModel.ts rename to packages/ai/cloudflare/src/CloudflareDecisionModel.ts diff --git a/repos/effect/packages/ai/cloudflare/src/CloudflareSchema.ts b/packages/ai/cloudflare/src/CloudflareSchema.ts similarity index 100% rename from repos/effect/packages/ai/cloudflare/src/CloudflareSchema.ts rename to packages/ai/cloudflare/src/CloudflareSchema.ts diff --git a/repos/effect/packages/ai/cloudflare/src/index.ts b/packages/ai/cloudflare/src/index.ts similarity index 100% rename from repos/effect/packages/ai/cloudflare/src/index.ts rename to packages/ai/cloudflare/src/index.ts diff --git a/repos/effect/packages/ai/cloudflare/src/internal/errors.ts b/packages/ai/cloudflare/src/internal/errors.ts similarity index 86% rename from repos/effect/packages/ai/cloudflare/src/internal/errors.ts rename to packages/ai/cloudflare/src/internal/errors.ts index b2f5caad4..cd2fb92fb 100644 --- a/repos/effect/packages/ai/cloudflare/src/internal/errors.ts +++ b/packages/ai/cloudflare/src/internal/errors.ts @@ -1,12 +1,14 @@ import * as AiError from "effect/ai/AiError" -import * as Duration from "effect/Duration" +import * as Clock from "effect/Clock" +import type * as Duration from "effect/Duration" import * as Effect from "effect/Effect" +import * as Headers from "effect/http/Headers" import type * as HttpClientError from "effect/http/HttpClientError" import type * as HttpClientRequest from "effect/http/HttpClientRequest" import type * as HttpClientResponse from "effect/http/HttpClientResponse" -import * as Num from "effect/Number" import * as Option from "effect/Option" import * as Redactable from "effect/Redactable" +import * as Result from "effect/Result" import * as Schema from "effect/Schema" const requestDetails = (request: HttpClientRequest.HttpClientRequest) => ({ @@ -23,12 +25,11 @@ const ErrorBody = Schema.Struct({ const decodeErrorBody = Schema.decodeUnknownOption(ErrorBody) -const retryAfter = (raw: string | undefined): Duration.Duration | undefined => { - if (raw === undefined || raw.trim() === "") return undefined - const seconds = Option.getOrUndefined(Num.parse(raw)) - if (seconds !== undefined) return Number.isFinite(seconds) && seconds >= 0 ? Duration.seconds(seconds) : undefined - const date = Date.parse(raw) - return Number.isNaN(date) ? undefined : Duration.millis(Math.max(0, date - Date.now())) +// an invalid Retry-After is treated as absent +const retryAfter = (raw: string | undefined, now: number): Duration.Duration | undefined => { + if (raw === undefined) return undefined + const retryAfter = Headers.parseRetryAfter(raw) + return Result.isSuccess(retryAfter) ? Headers.retryAfterDelay(retryAfter.success, now) : undefined } const fail = (reason: AiError.AiErrorReason) => @@ -57,7 +58,7 @@ export const mapResponseError = Effect.fnUntraced( if (status === 429) { return yield* fail( new AiError.RateLimitError({ - retryAfter: retryAfter(response.headers["retry-after"]), + retryAfter: retryAfter(response.headers["retry-after"], yield* Clock.currentTimeMillis), http, metadata: { cloudflare: { rayId: response.headers["cf-ray"] ?? null, errors: details?.errors ?? [] } } }) diff --git a/repos/effect/packages/ai/cloudflare/test/CloudflareClient.test.ts b/packages/ai/cloudflare/test/CloudflareClient.test.ts similarity index 88% rename from repos/effect/packages/ai/cloudflare/test/CloudflareClient.test.ts rename to packages/ai/cloudflare/test/CloudflareClient.test.ts index daef1a985..b1fe28db1 100644 --- a/repos/effect/packages/ai/cloudflare/test/CloudflareClient.test.ts +++ b/packages/ai/cloudflare/test/CloudflareClient.test.ts @@ -31,8 +31,11 @@ const httpLayer = ( ) ) -const clientLayer = (response: (request: HttpClientRequest.HttpClientRequest) => Response) => - CloudflareClient.layer({ accountId: "test-account", apiKey: Redacted.make("test-token") }).pipe( +const clientLayer = ( + response: (request: HttpClientRequest.HttpClientRequest) => Response, + options?: Partial +) => + CloudflareClient.layer({ accountId: "test-account", apiKey: Redacted.make("test-token"), ...options }).pipe( Layer.provide(httpLayer((request) => Effect.succeed(HttpClientResponse.fromWeb(request, response(request))))) ) @@ -53,6 +56,7 @@ describe("CloudflareClient", () => { "https://api.cloudflare.com/client/v4/accounts/test-account/ai/run/@cf/cloudflare/clef-flash" ) assert.strictEqual(request.headers.authorization, "Bearer test-token") + assert.strictEqual(request.headers["cf-aig-gateway-id"], undefined) assert.strictEqual(request.body._tag, "Uint8Array") if (request.body._tag === "Uint8Array") { assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(request.body.body)), payload) @@ -60,7 +64,15 @@ describe("CloudflareClient", () => { return Response.json({ success: true, result, errors: [], messages: [] }) })))) - it.effect("layerConfig reads Cloudflare credentials from the environment", () => + it.effect("routes requests through the configured AI Gateway", () => + createDecisions.pipe( + Effect.provide(clientLayer((request) => { + assert.strictEqual(request.headers["cf-aig-gateway-id"], "test-gateway") + return Response.json({ success: true, result }) + }, { gatewayId: "test-gateway" })) + )) + + it.effect("layerConfig reads Cloudflare credentials and AI Gateway configuration from the environment", () => createDecisions.pipe( Effect.provide(CloudflareClient.layerConfig({ apiUrl: Config.String("TEST_API_URL"), @@ -70,6 +82,7 @@ describe("CloudflareClient", () => { env: { CLOUDFLARE_ACCOUNT_ID: "configured-account", CLOUDFLARE_API_TOKEN: "configured-token", + CLOUDFLARE_AI_GATEWAY_ID: "configured-gateway", TEST_API_URL: "https://example.com/api" } }))), @@ -79,6 +92,7 @@ describe("CloudflareClient", () => { "https://example.com/api/accounts/configured-account/ai/run/@cf/cloudflare/clef-flash" ) assert.strictEqual(request.headers.authorization, "Bearer configured-token") + assert.strictEqual(request.headers["cf-aig-gateway-id"], "configured-gateway") assert.strictEqual(request.headers["x-test"], "configured") return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ success: true, result }))) })) @@ -148,10 +162,10 @@ describe("CloudflareClient", () => { } }).pipe(Effect.provide(clientLayer(() => new Response("Bad gateway", { status: 502 }))))) - it.effect("maps a success body without a result envelope to InvalidOutputError", () => + it.effect("maps a success body without a result envelope to a non-retryable InvalidResponseError", () => Effect.gen(function*() { const error = yield* Effect.flip(createDecisions) - assert.strictEqual(error.reason._tag, "InvalidOutputError") + assert.strictEqual(error.reason._tag, "InvalidResponseError") }).pipe(Effect.provide(clientLayer(() => Response.json(result))))) it.effect("maps TransportError to NetworkError", () => diff --git a/repos/effect/packages/ai/cloudflare/test/CloudflareDecisionModel.test.ts b/packages/ai/cloudflare/test/CloudflareDecisionModel.test.ts similarity index 100% rename from repos/effect/packages/ai/cloudflare/test/CloudflareDecisionModel.test.ts rename to packages/ai/cloudflare/test/CloudflareDecisionModel.test.ts diff --git a/repos/effect/packages/ai/cloudflare/tsconfig.json b/packages/ai/cloudflare/tsconfig.json similarity index 100% rename from repos/effect/packages/ai/cloudflare/tsconfig.json rename to packages/ai/cloudflare/tsconfig.json diff --git a/repos/effect/packages/ai/cloudflare/typetest/CloudflareDecisionModel.tst.ts b/packages/ai/cloudflare/typetest/CloudflareDecisionModel.tst.ts similarity index 100% rename from repos/effect/packages/ai/cloudflare/typetest/CloudflareDecisionModel.tst.ts rename to packages/ai/cloudflare/typetest/CloudflareDecisionModel.tst.ts diff --git a/repos/effect/packages/ai/openai-compat/CHANGELOG.md b/packages/ai/openai-compat/CHANGELOG.md similarity index 87% rename from repos/effect/packages/ai/openai-compat/CHANGELOG.md rename to packages/ai/openai-compat/CHANGELOG.md index 915ba4c40..6bac3e05f 100644 --- a/repos/effect/packages/ai/openai-compat/CHANGELOG.md +++ b/packages/ai/openai-compat/CHANGELOG.md @@ -1,5 +1,27 @@ # @effect/ai-openai-compat +## 4.0.6 + +### Patch Changes + +- [#9552](https://github.com/Effect-TS/effect/pull/9552) [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685) Thanks @wmaurer! - Add an optional `batchKey` to `EmbeddingModel.make` to group concurrent `embed` calls by provider-relevant context. OpenAI and OpenAI-compatible providers now separate batches with different configuration overrides or client transforms. Undeclared context, such as per-call `FetchHttpClient.RequestInit`, still uses the first caller's value. + +- [#9709](https://github.com/Effect-TS/effect/pull/9709) [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117) Thanks @tim-smart! - Add typed HTTP date and `Retry-After` header values to `Headers`, and parse them consistently across the HTTP client, OTLP exporter, GraphQL client, AI clients and static server. + + - `Headers.parseHttpDate` / `Headers.formatHttpDate` read all three RFC 9110 date formats as UTC, whatever the process time zone, and emit IMF-fixdate. + - `Headers.parseRetryAfter`, `Headers.formatRetryAfter` and `Headers.retryAfterDelay` accept whole non-negative seconds of any size, read exactly, or an HTTP date, and the delay is never negative. HTTP dates accept the `23:59:60` leap second, read as the following second. + - `Headers.RetryAfter` holds either a `Duration` (`RetryAfterDuration`) or a `DateTime.Utc` (`RetryAfterDateTime`). `Headers.retryAfterDuration` builds the first from any `Duration.Input`: infinite durations throw, finite negative ones become zero, and fractional seconds are rounded up when formatted. + - Parse failures are reported as `Headers.HeaderParseError`. + - `Schema.DateTimeUtcFromHttpDate` and `Schema.RetryAfterFromString` expose the same parsing as string codecs for HTTP API headers. + - Invalid `Retry-After` values such as `-5`, `1.5`, `1e3` or `Infinity` are now ignored everywhere. `HttpClient.retryTransient` and `HttpClient.withRateLimiter` no longer honour fractional seconds, and the AI clients no longer produce negative or infinite `retryAfter` durations. + - `HttpStaticServer` reads `If-Modified-Since` and `Last-Modified` as UTC; asctime dates were previously shifted by the process time zone. + +- [#9430](https://github.com/Effect-TS/effect/pull/9430) [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310) Thanks @effect-bot! - Add non-retryable `AiError.InvalidResponseError` for provider response schema and SSE decoding failures, with optional HTTP context; model-generated output validation remains retryable. Update exhaustive matches on `AiErrorReason` to handle the new tag, and update serialized-error consumers to accept it. + +- [#9242](https://github.com/Effect-TS/effect/pull/9242) [`12b2726`](https://github.com/Effect-TS/effect/commit/12b2726651a6d6eeb2fd66539d34c1401f9afd94) Thanks @effect-bot! - Consistently nest authentication, internal provider, and unknown error metadata under the `openai` or `openai-compat` provider key, including error codes, error types, and request IDs. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/ai/openai-compat/README.md b/packages/ai/openai-compat/README.md similarity index 100% rename from repos/effect/packages/ai/openai-compat/README.md rename to packages/ai/openai-compat/README.md diff --git a/repos/effect/packages/ai/openai-compat/package.json b/packages/ai/openai-compat/package.json similarity index 98% rename from repos/effect/packages/ai/openai-compat/package.json rename to packages/ai/openai-compat/package.json index c908e4b93..209dc0654 100644 --- a/repos/effect/packages/ai/openai-compat/package.json +++ b/packages/ai/openai-compat/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai-compat", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "An OpenAI compat integration for Effect", diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiClient.ts b/packages/ai/openai-compat/src/OpenAiClient.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/src/OpenAiClient.ts rename to packages/ai/openai-compat/src/OpenAiClient.ts diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiConfig.ts b/packages/ai/openai-compat/src/OpenAiConfig.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/src/OpenAiConfig.ts rename to packages/ai/openai-compat/src/OpenAiConfig.ts diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts b/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts similarity index 95% rename from repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts rename to packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts index d7cc36604..e7e24be11 100644 --- a/repos/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts +++ b/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts @@ -18,6 +18,7 @@ import * as Layer from "effect/Layer" import type { Simplify } from "effect/Types" import type { CreateEmbedding200, CreateEmbeddingRequestJson } from "./OpenAiClient.ts" import { OpenAiClient } from "./OpenAiClient.ts" +import { OpenAiConfig } from "./OpenAiConfig.ts" /** * A model identifier accepted by an OpenAI-compatible embeddings endpoint. @@ -108,6 +109,10 @@ export const model = ( * * **Gotchas** * + * Concurrent `embed` calls share a request when their `Config` and + * `OpenAiConfig` values are equal. Other per-call context, such as + * `FetchHttpClient.RequestInit`, takes the first caller's value for the batch. + * * Provider responses must contain one numeric vector for every requested input * with unique, in-range `index` values; otherwise embedding operations fail with * `AiError.InvalidOutputError`. @@ -131,6 +136,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig ) return yield* EmbeddingModel.make({ + batchKey: (context) => [Context.getOrUndefined(context, Config), Context.getOrUndefined(context, OpenAiConfig)], embedMany: Effect.fnUntraced(function*({ inputs }) { const config = yield* makeConfig const response = yield* client.createEmbedding({ ...config, input: inputs }) diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiError.ts b/packages/ai/openai-compat/src/OpenAiError.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/src/OpenAiError.ts rename to packages/ai/openai-compat/src/OpenAiError.ts diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts b/packages/ai/openai-compat/src/OpenAiLanguageModel.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts rename to packages/ai/openai-compat/src/OpenAiLanguageModel.ts diff --git a/repos/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts b/packages/ai/openai-compat/src/OpenAiTelemetry.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts rename to packages/ai/openai-compat/src/OpenAiTelemetry.ts diff --git a/repos/effect/packages/ai/openai-compat/src/index.ts b/packages/ai/openai-compat/src/index.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/src/index.ts rename to packages/ai/openai-compat/src/index.ts diff --git a/repos/effect/packages/ai/openai-compat/src/internal/errors.ts b/packages/ai/openai-compat/src/internal/errors.ts similarity index 88% rename from repos/effect/packages/ai/openai-compat/src/internal/errors.ts rename to packages/ai/openai-compat/src/internal/errors.ts index b80a6b413..11f73613f 100644 --- a/repos/effect/packages/ai/openai-compat/src/internal/errors.ts +++ b/packages/ai/openai-compat/src/internal/errors.ts @@ -1,16 +1,19 @@ import * as AiError from "effect/ai/AiError" import type * as Response from "effect/ai/Response" import * as Arr from "effect/Array" -import * as Duration from "effect/Duration" +import * as Clock from "effect/Clock" +import type * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import type * as Sse from "effect/encoding/Sse" import { dual } from "effect/Function" +import * as Headers from "effect/http/Headers" import type * as HttpClientError from "effect/http/HttpClientError" import type * as HttpClientRequest from "effect/http/HttpClientRequest" import type * as HttpClientResponse from "effect/http/HttpClientResponse" import * as Number from "effect/Number" import * as Option from "effect/Option" import * as Redactable from "effect/Redactable" +import * as Result from "effect/Result" import * as Schema from "effect/Schema" import * as SchemaTransformation from "effect/SchemaTransformation" import * as String from "effect/String" @@ -47,7 +50,7 @@ export const mapSchemaError = dual< AiError.make({ module: "OpenAiClient", method, - reason: AiError.InvalidOutputError.fromSchemaError(error) + reason: AiError.InvalidResponseError.fromSchemaError(error) })) /** @internal */ @@ -58,7 +61,7 @@ export const mapSseError = dual< AiError.make({ module: "OpenAiClient", method, - reason: new AiError.InvalidOutputError({ description: error.message }) + reason: new AiError.InvalidResponseError({ description: error.message }) })) /** @internal */ @@ -140,7 +143,9 @@ const mapStatusCodeError = Effect.fnUntraced(function*( ) const decoded = OpenAiErrorBodyJson(body) + const now = yield* Clock.currentTimeMillis const reason = mapStatusCodeToReason({ + now, status, headers, message: Option.isSome(decoded) ? decoded.value.error.message : undefined, @@ -159,16 +164,16 @@ const mapStatusCodeError = Effect.fnUntraced(function*( return yield* AiError.make({ module: "OpenAiClient", method, reason }) }) +const retryAfterFromHeader = (raw: string, now: number): Duration.Duration | undefined => { + const retryAfter = Headers.parseRetryAfter(raw) + return Result.isSuccess(retryAfter) ? Headers.retryAfterDelay(retryAfter.success, now) : undefined +} + /** @internal */ -export const parseRateLimitHeaders = (headers: Record) => { +export const parseRateLimitHeaders = (headers: Record, now: number) => { const retryAfterRaw = headers["retry-after"] - let retryAfter: Duration.Duration | undefined - if (retryAfterRaw !== undefined) { - const parsed = Number.parse(retryAfterRaw) - if (Option.isSome(parsed)) { - retryAfter = Duration.seconds(parsed.value) - } - } + // an invalid Retry-After is treated as absent + const retryAfter = retryAfterRaw === undefined ? undefined : retryAfterFromHeader(retryAfterRaw, now) const remainingRaw = headers["x-ratelimit-remaining-requests"] const remaining = remainingRaw !== undefined ? Option.getOrNull(Number.parse(remainingRaw)) : null return { @@ -208,8 +213,10 @@ export const buildHttpContext = (params: { }) /** @internal */ -export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http, now }: { readonly status: number + /** Current time in epoch milliseconds, used to measure a Retry-After date. */ + readonly now: number readonly headers: Record readonly message: string | undefined readonly metadata: OpenAiErrorMetadata @@ -237,14 +244,14 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http return new AiError.AuthenticationError({ kind: "InvalidKey", description: errorDescription, - metadata, + metadata: { "openai-compat": metadata }, http }) case 403: return new AiError.AuthenticationError({ kind: "InsufficientPermissions", description: errorDescription, - metadata, + metadata: { "openai-compat": metadata }, http }) case 404: @@ -272,7 +279,7 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http http }) } - const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers, now) return new AiError.RateLimitError({ retryAfter, metadata: { @@ -288,13 +295,13 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http if (status >= 500) { return new AiError.InternalProviderError({ description: message ?? "Server error", - metadata, + metadata: { "openai-compat": metadata }, http }) } return new AiError.UnknownError({ description: message, - metadata, + metadata: { "openai-compat": metadata }, http }) } diff --git a/repos/effect/packages/ai/openai-compat/src/internal/utilities.ts b/packages/ai/openai-compat/src/internal/utilities.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/src/internal/utilities.ts rename to packages/ai/openai-compat/src/internal/utilities.ts diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts b/packages/ai/openai-compat/test/OpenAiClient.test.ts similarity index 98% rename from repos/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts rename to packages/ai/openai-compat/test/OpenAiClient.test.ts index 8cbd49581..89845ee4f 100644 --- a/repos/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts +++ b/packages/ai/openai-compat/test/OpenAiClient.test.ts @@ -407,6 +407,7 @@ describe("OpenAiClient", () => { it("preserves and truncates a fallback HTTP response", () => { const body = `${"a".repeat(200)}b` const reason = Errors.mapStatusCodeToReason({ + now: 0, status: 400, headers: {}, message: undefined, @@ -596,15 +597,14 @@ const makeResponse = ( ) } -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) const toSseBody = (events: ReadonlyArray): string => events.map((event) => { diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts b/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts similarity index 95% rename from repos/effect/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts rename to packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts index b7f6132c2..f414e6c96 100644 --- a/repos/effect/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts +++ b/packages/ai/openai-compat/test/OpenAiEmbeddingModel.test.ts @@ -198,15 +198,14 @@ const jsonResponse = ( }) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) const noopOpenAiClient: OpenAiClient.Service = { client: undefined as unknown as OpenAiClient.Service["client"], diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts b/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts similarity index 99% rename from repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts rename to packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts index c1ae663ba..b231ba651 100644 --- a/repos/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts +++ b/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts @@ -2466,15 +2466,14 @@ const sseResponse = ( }) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) const decodeToolParamsFromStream = ( parts: ReadonlyArray, diff --git a/repos/effect/packages/ai/openai-compat/test/OpenAiTelemetry.test.ts b/packages/ai/openai-compat/test/OpenAiTelemetry.test.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/test/OpenAiTelemetry.test.ts rename to packages/ai/openai-compat/test/OpenAiTelemetry.test.ts diff --git a/repos/effect/packages/ai/openai-compat/tsconfig.json b/packages/ai/openai-compat/tsconfig.json similarity index 100% rename from repos/effect/packages/ai/openai-compat/tsconfig.json rename to packages/ai/openai-compat/tsconfig.json diff --git a/repos/effect/packages/ai/openai-compat/typetest/OpenAiConfig.tst.ts b/packages/ai/openai-compat/typetest/OpenAiConfig.tst.ts similarity index 100% rename from repos/effect/packages/ai/openai-compat/typetest/OpenAiConfig.tst.ts rename to packages/ai/openai-compat/typetest/OpenAiConfig.tst.ts diff --git a/repos/effect/packages/ai/openai/CHANGELOG.md b/packages/ai/openai/CHANGELOG.md similarity index 87% rename from repos/effect/packages/ai/openai/CHANGELOG.md rename to packages/ai/openai/CHANGELOG.md index 1ed5bd3f2..ee9a9f479 100644 --- a/repos/effect/packages/ai/openai/CHANGELOG.md +++ b/packages/ai/openai/CHANGELOG.md @@ -1,5 +1,37 @@ # @effect/ai-openai +## 4.0.6 + +### Patch Changes + +- [#9437](https://github.com/Effect-TS/effect/pull/9437) [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b) Thanks @effect-bot! - Add `DecisionModel.decideSettled` to retain valid answers alongside per-decision `Result` failures (`Missing`, `Refused`, `NoDistribution`, or `Invalid`). Provider failures still fail the effect; `decide` remains all-or-nothing. + + OpenRouter now passes missing distributions to core validation. OpenAI reports refusals per decision and wrong-kind score answers as invalid rather than missing. + + Update hand-built services to implement `decideSettled`, handle optional provider `probabilities` and refusal answers, and use settled outcomes instead of the removed `OpenAiContentPolicyMetadata` and `metadata.openai.refusedDecisions`. + +- [#9552](https://github.com/Effect-TS/effect/pull/9552) [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685) Thanks @wmaurer! - Add an optional `batchKey` to `EmbeddingModel.make` to group concurrent `embed` calls by provider-relevant context. OpenAI and OpenAI-compatible providers now separate batches with different configuration overrides or client transforms. Undeclared context, such as per-call `FetchHttpClient.RequestInit`, still uses the first caller's value. + +- [#9709](https://github.com/Effect-TS/effect/pull/9709) [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117) Thanks @tim-smart! - Add typed HTTP date and `Retry-After` header values to `Headers`, and parse them consistently across the HTTP client, OTLP exporter, GraphQL client, AI clients and static server. + + - `Headers.parseHttpDate` / `Headers.formatHttpDate` read all three RFC 9110 date formats as UTC, whatever the process time zone, and emit IMF-fixdate. + - `Headers.parseRetryAfter`, `Headers.formatRetryAfter` and `Headers.retryAfterDelay` accept whole non-negative seconds of any size, read exactly, or an HTTP date, and the delay is never negative. HTTP dates accept the `23:59:60` leap second, read as the following second. + - `Headers.RetryAfter` holds either a `Duration` (`RetryAfterDuration`) or a `DateTime.Utc` (`RetryAfterDateTime`). `Headers.retryAfterDuration` builds the first from any `Duration.Input`: infinite durations throw, finite negative ones become zero, and fractional seconds are rounded up when formatted. + - Parse failures are reported as `Headers.HeaderParseError`. + - `Schema.DateTimeUtcFromHttpDate` and `Schema.RetryAfterFromString` expose the same parsing as string codecs for HTTP API headers. + - Invalid `Retry-After` values such as `-5`, `1.5`, `1e3` or `Infinity` are now ignored everywhere. `HttpClient.retryTransient` and `HttpClient.withRateLimiter` no longer honour fractional seconds, and the AI clients no longer produce negative or infinite `retryAfter` durations. + - `HttpStaticServer` reads `If-Modified-Since` and `Last-Modified` as UTC; asctime dates were previously shifted by the process time zone. + +- [#9430](https://github.com/Effect-TS/effect/pull/9430) [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310) Thanks @effect-bot! - Add non-retryable `AiError.InvalidResponseError` for provider response schema and SSE decoding failures, with optional HTTP context; model-generated output validation remains retryable. Update exhaustive matches on `AiErrorReason` to handle the new tag, and update serialized-error consumers to accept it. + +- [#9242](https://github.com/Effect-TS/effect/pull/9242) [`12b2726`](https://github.com/Effect-TS/effect/commit/12b2726651a6d6eeb2fd66539d34c1401f9afd94) Thanks @effect-bot! - Consistently nest authentication, internal provider, and unknown error metadata under the `openai` or `openai-compat` provider key, including error codes, error types, and request IDs. + +- [#9667](https://github.com/Effect-TS/effect/pull/9667) [`225f2c6`](https://github.com/Effect-TS/effect/commit/225f2c6b8f05a849abaea063043b4a09c92c703b) Thanks @effect-bot! - Support `promptCacheBreakpoint` on OpenAI image and file prompt parts, sending `prompt_cache_breakpoint` on `input_image` and `input_file` content blocks. + +- [#9670](https://github.com/Effect-TS/effect/pull/9670) [`ab5c1a8`](https://github.com/Effect-TS/effect/commit/ab5c1a8a3581ed3c2b05130b9e58fb7bae41dfe5) Thanks @effect-bot! - Support the `safety_identifier` and `prompt_cache_retention` Responses request options in `OpenAiSchema.CreateResponse` and `OpenAiLanguageModel.Config`. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/ai/openai/README.md b/packages/ai/openai/README.md similarity index 100% rename from repos/effect/packages/ai/openai/README.md rename to packages/ai/openai/README.md diff --git a/repos/effect/packages/ai/openai/codegen.yaml b/packages/ai/openai/codegen.yaml similarity index 100% rename from repos/effect/packages/ai/openai/codegen.yaml rename to packages/ai/openai/codegen.yaml diff --git a/repos/effect/packages/ai/openai/package.json b/packages/ai/openai/package.json similarity index 98% rename from repos/effect/packages/ai/openai/package.json rename to packages/ai/openai/package.json index f9a91a928..525c68ace 100644 --- a/repos/effect/packages/ai/openai/package.json +++ b/packages/ai/openai/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "An OpenAI provider integration for Effect AI SDK", diff --git a/repos/effect/packages/ai/openai/src/Generated.ts b/packages/ai/openai/src/Generated.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/Generated.ts rename to packages/ai/openai/src/Generated.ts diff --git a/repos/effect/packages/ai/openai/src/OpenAiClient.ts b/packages/ai/openai/src/OpenAiClient.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/OpenAiClient.ts rename to packages/ai/openai/src/OpenAiClient.ts diff --git a/repos/effect/packages/ai/openai/src/OpenAiClientGenerated.ts b/packages/ai/openai/src/OpenAiClientGenerated.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/OpenAiClientGenerated.ts rename to packages/ai/openai/src/OpenAiClientGenerated.ts diff --git a/repos/effect/packages/ai/openai/src/OpenAiConfig.ts b/packages/ai/openai/src/OpenAiConfig.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/OpenAiConfig.ts rename to packages/ai/openai/src/OpenAiConfig.ts diff --git a/repos/effect/packages/ai/openai/src/OpenAiDecisionModel.ts b/packages/ai/openai/src/OpenAiDecisionModel.ts similarity index 89% rename from repos/effect/packages/ai/openai/src/OpenAiDecisionModel.ts rename to packages/ai/openai/src/OpenAiDecisionModel.ts index 3e5995b2e..1d150549c 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiDecisionModel.ts +++ b/packages/ai/openai/src/OpenAiDecisionModel.ts @@ -54,9 +54,8 @@ export const model = ( * * - Remote URLs fail with `AiError.InvalidUserInputError` because OpenAI only * accepts inline images. - * - If OpenAI refuses any decision, the whole call fails with - * `AiError.ContentPolicyError`. The refused names are in - * `metadata.openai.refusedDecisions`. + * - Refusals yield `"Refused"` with `decideSettled`; `decide` fails with + * `AiError.ContentPolicyError` naming the refused decisions. * * @stability unstable * @category constructors @@ -91,11 +90,11 @@ export const make = Effect.fnUntraced( questions }) const answers: Record = Object.create(null) - const refused: Array = [] for (const answer of response.answers) { + if (answers[answer.name]?._tag === "Refusal") continue switch (answer.type) { case "refusal": - refused.push(answer.name) + answers[answer.name] = { _tag: "Refusal" } break case "choice": answers[answer.name] = { @@ -107,10 +106,10 @@ export const make = Effect.fnUntraced( break case "score": { const decision = decisions[answer.name] - if (decision?._tag !== "Rate") break + const levels = decision?._tag === "Rate" ? decision.criteria : [] const probabilities: Record = Object.create(null) for (const { value, probability } of answer.probabilities) { - const level = decision.criteria[value] + const level = levels[value] if (level !== undefined) probabilities[level.label] = probability } answers[answer.name] = { @@ -126,16 +125,6 @@ export const make = Effect.fnUntraced( break } } - if (refused.length > 0) { - return yield* Effect.fail(AiError.make({ - module: "OpenAiDecisionModel", - method: "decide", - reason: new AiError.ContentPolicyError({ - description: `OpenAI refused to answer: ${refused.join(", ")}`, - metadata: { openai: { errorCode: null, errorType: null, requestId: null, refusedDecisions: refused } } - }) - })) - } return { answers, usage: { inputTokens: response.usage?.input_tokens, outputTokens: response.usage?.output_tokens } diff --git a/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts b/packages/ai/openai/src/OpenAiEmbeddingModel.ts similarity index 95% rename from repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts rename to packages/ai/openai/src/OpenAiEmbeddingModel.ts index 4e809ad5a..f9b84ad36 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiEmbeddingModel.ts +++ b/packages/ai/openai/src/OpenAiEmbeddingModel.ts @@ -17,6 +17,7 @@ import { dual } from "effect/Function" import * as Layer from "effect/Layer" import type { Simplify } from "effect/Types" import { OpenAiClient } from "./OpenAiClient.ts" +import { OpenAiConfig } from "./OpenAiConfig.ts" import type * as OpenAiSchema from "./OpenAiSchema.ts" /** @@ -116,6 +117,10 @@ export const model = ( * * **Gotchas** * + * Concurrent `embed` calls share a request when their `Config` and + * `OpenAiConfig` values are equal. Other per-call context, such as + * `FetchHttpClient.RequestInit`, takes the first caller's value for the batch. + * * The service expects numeric embedding vectors. It fails with * `InvalidOutputError` when the provider returns base64 embeddings, * out-of-range indexes, duplicate indexes, or an unexpected number of @@ -140,6 +145,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig ) return yield* EmbeddingModel.make({ + batchKey: (context) => [Context.getOrUndefined(context, Config), Context.getOrUndefined(context, OpenAiConfig)], embedMany: Effect.fnUntraced(function*({ inputs }) { const config = yield* makeConfig const response = yield* client.createEmbedding({ ...config, input: inputs }) diff --git a/repos/effect/packages/ai/openai/src/OpenAiError.ts b/packages/ai/openai/src/OpenAiError.ts similarity index 92% rename from repos/effect/packages/ai/openai/src/OpenAiError.ts rename to packages/ai/openai/src/OpenAiError.ts index 470a9ef03..1f777293e 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiError.ts +++ b/packages/ai/openai/src/OpenAiError.ts @@ -30,25 +30,6 @@ export type OpenAiErrorMetadata = { readonly requestId: string | null } -/** - * OpenAI-specific content policy metadata fields. - * - * **Details** - * - * Extends base error metadata with the names of decisions that OpenAI refused - * to answer. - * - * @stability unstable - * @category models - * @since 4.0.5 - */ -export type OpenAiContentPolicyMetadata = OpenAiErrorMetadata & { - /** - * Names of the decisions that came back as refusals. - */ - readonly refusedDecisions?: Array -} - /** * OpenAI-specific rate limit metadata fields. * @@ -151,7 +132,7 @@ declare module "effect/ai/AiError" { /** * OpenAI-specific details for the content policy response. */ - readonly openai?: OpenAiContentPolicyMetadata | null + readonly openai?: OpenAiErrorMetadata | null } /** diff --git a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts b/packages/ai/openai/src/OpenAiLanguageModel.ts similarity index 99% rename from repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts rename to packages/ai/openai/src/OpenAiLanguageModel.ts index 8217553d5..ee506e090 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiLanguageModel.ts +++ b/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -192,6 +192,13 @@ declare module "effect/ai/Prompt" { * The detail level of the image to be sent to the model. One of `high`, `low`, or `auto`. Defaults to `auto`. */ readonly imageDetail?: ImageDetail | null + /** + * Marks the input image or file as the end of a reusable prompt prefix. + * + * Requires GPT-5.6 or later. OpenAI may reject requests that use this + * option with earlier models. + */ + readonly promptCacheBreakpoint?: PromptCacheBreakpoint | null } | null } @@ -925,12 +932,13 @@ const prepareMessages = Effect.fnUntraced( } case "file": { + const promptCacheBreakpoint = getPromptCacheBreakpoint(part) if (part.mediaType.startsWith("image/")) { const detail = getImageDetail(part) const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType if (typeof part.data === "string" && isFileId(part.data, config)) { - content.push({ type: "input_image", file_id: part.data, detail }) + content.push({ type: "input_image", file_id: part.data, detail, ...promptCacheBreakpoint }) } else { const imageUrl = part.data instanceof URL ? part.data.toString() @@ -939,22 +947,27 @@ const prepareMessages = Effect.fnUntraced( : /^(data:|https?:\/\/)/i.test(part.data) ? part.data : `data:${mediaType};base64,${part.data}` - content.push({ type: "input_image", image_url: imageUrl, detail }) + content.push({ type: "input_image", image_url: imageUrl, detail, ...promptCacheBreakpoint }) } } else if (part.mediaType === "application/pdf") { if (typeof part.data === "string" && isFileId(part.data, config)) { - content.push({ type: "input_file", file_id: part.data }) + content.push({ type: "input_file", file_id: part.data, ...promptCacheBreakpoint }) } if (part.data instanceof URL) { - content.push({ type: "input_file", file_url: part.data.toString() }) + content.push({ type: "input_file", file_url: part.data.toString(), ...promptCacheBreakpoint }) } if (part.data instanceof Uint8Array) { const base64 = Base64.encode(part.data) const fileName = part.fileName ?? `part-${index}.pdf` const fileData = `data:application/pdf;base64,${base64}` - content.push({ type: "input_file", filename: fileName, file_data: fileData }) + content.push({ + type: "input_file", + filename: fileName, + file_data: fileData, + ...promptCacheBreakpoint + }) } } else { return yield* AiError.make({ @@ -3133,7 +3146,7 @@ const getEncryptedContent = ( const getImageDetail = (part: Prompt.FilePart): ImageDetail => part.options.openai?.imageDetail ?? "auto" const getPromptCacheBreakpoint = ( - input: Prompt.SystemMessage | Prompt.TextPart + input: Prompt.SystemMessage | Prompt.TextPart | Prompt.FilePart ) => { const promptCacheBreakpoint = input.options.openai?.promptCacheBreakpoint return Predicate.isNotNullish(promptCacheBreakpoint) ? { prompt_cache_breakpoint: promptCacheBreakpoint } : undefined diff --git a/repos/effect/packages/ai/openai/src/OpenAiSchema.ts b/packages/ai/openai/src/OpenAiSchema.ts similarity index 98% rename from repos/effect/packages/ai/openai/src/OpenAiSchema.ts rename to packages/ai/openai/src/OpenAiSchema.ts index 484996088..2c07429d6 100644 --- a/repos/effect/packages/ai/openai/src/OpenAiSchema.ts +++ b/packages/ai/openai/src/OpenAiSchema.ts @@ -93,7 +93,8 @@ const InputImageContent = Schema.Struct({ type: Schema.Literal("input_image"), image_url: Schema.optionalKey(Schema.NullOr(Schema.String)), file_id: Schema.optionalKey(Schema.NullOr(Schema.String)), - detail: Schema.optionalKey(Schema.NullOr(ImageDetail)) + detail: Schema.optionalKey(Schema.NullOr(ImageDetail)), + prompt_cache_breakpoint: Schema.optional(PromptCacheBreakpoint) }) const InputFileContent = Schema.Struct({ @@ -101,7 +102,8 @@ const InputFileContent = Schema.Struct({ file_id: Schema.optionalKey(Schema.NullOr(Schema.String)), filename: Schema.optionalKey(Schema.String), file_url: Schema.optionalKey(Schema.String), - file_data: Schema.optionalKey(Schema.String) + file_data: Schema.optionalKey(Schema.String), + prompt_cache_breakpoint: Schema.optional(PromptCacheBreakpoint) }) /** @@ -703,7 +705,8 @@ export type TextResponseFormatConfiguration = typeof TextResponseFormatConfigura * Validates the Responses API request payload, including input content, model * selection, instructions, reasoning options, text output format, tools, * `tool_choice`, streaming, storage, response continuation, sampling options, - * prompt caching, and optional response fields requested through `include`. + * safety identifiers, prompt caching, and optional response fields requested + * through `include`. * * **Gotchas** * @@ -723,7 +726,9 @@ export const CreateResponse = Schema.Struct({ temperature: Schema.optional(Schema.Finite), top_p: Schema.optional(Schema.Finite), user: Schema.optional(Schema.String), + safety_identifier: Schema.optional(Schema.String), prompt_cache_key: Schema.optional(Schema.String), + prompt_cache_retention: Schema.optional(Schema.Literals(["in_memory", "24h"])), prompt_cache_options: Schema.optional(Schema.Struct({ mode: Schema.optional(Schema.Literals(["implicit", "explicit"])), ttl: Schema.optional(Schema.Literal("30m")), diff --git a/repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts b/packages/ai/openai/src/OpenAiTelemetry.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/OpenAiTelemetry.ts rename to packages/ai/openai/src/OpenAiTelemetry.ts diff --git a/repos/effect/packages/ai/openai/src/OpenAiTool.ts b/packages/ai/openai/src/OpenAiTool.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/OpenAiTool.ts rename to packages/ai/openai/src/OpenAiTool.ts diff --git a/repos/effect/packages/ai/openai/src/index.ts b/packages/ai/openai/src/index.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/index.ts rename to packages/ai/openai/src/index.ts diff --git a/repos/effect/packages/ai/openai/src/internal/errors.ts b/packages/ai/openai/src/internal/errors.ts similarity index 90% rename from repos/effect/packages/ai/openai/src/internal/errors.ts rename to packages/ai/openai/src/internal/errors.ts index 4e2ec36aa..8572fc4d4 100644 --- a/repos/effect/packages/ai/openai/src/internal/errors.ts +++ b/packages/ai/openai/src/internal/errors.ts @@ -1,9 +1,11 @@ import * as AiError from "effect/ai/AiError" import type * as Response from "effect/ai/Response" -import * as Duration from "effect/Duration" +import * as Clock from "effect/Clock" +import type * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import type * as Sse from "effect/encoding/Sse" import { dual } from "effect/Function" +import * as Headers from "effect/http/Headers" import type * as HttpClientError from "effect/http/HttpClientError" import type * as HttpClientRequest from "effect/http/HttpClientRequest" import type * as HttpClientResponse from "effect/http/HttpClientResponse" @@ -11,6 +13,7 @@ import * as Number from "effect/Number" import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import * as Redactable from "effect/Redactable" +import * as Result from "effect/Result" import * as Schema from "effect/Schema" import type { OpenAiErrorMetadata } from "../OpenAiError.ts" @@ -45,7 +48,7 @@ export const mapSchemaError = dual< AiError.make({ module: "OpenAiClient", method, - reason: AiError.InvalidOutputError.fromSchemaError(error) + reason: AiError.InvalidResponseError.fromSchemaError(error) })) /** @internal */ @@ -56,7 +59,7 @@ export const mapSseError = dual< AiError.make({ module: "OpenAiClient", method, - reason: new AiError.InvalidOutputError({ description: error.message }) + reason: new AiError.InvalidResponseError({ description: error.message }) })) /** @internal */ @@ -164,7 +167,9 @@ const mapStatusCodeError = Effect.fnUntraced(function*( ? compatibleDecoded.value.code ?? null : null + const now = yield* Clock.currentTimeMillis const reason = mapStatusCodeToReason({ + now, status, headers, message, @@ -183,16 +188,16 @@ const mapStatusCodeError = Effect.fnUntraced(function*( // Rate Limits // ============================================================================= +const retryAfterFromHeader = (raw: string, now: number): Duration.Duration | undefined => { + const retryAfter = Headers.parseRetryAfter(raw) + return Result.isSuccess(retryAfter) ? Headers.retryAfterDelay(retryAfter.success, now) : undefined +} + /** @internal */ -export const parseRateLimitHeaders = (headers: Record) => { +export const parseRateLimitHeaders = (headers: Record, now: number) => { const retryAfterRaw = headers["retry-after"] - let retryAfter: Duration.Duration | undefined - if (Predicate.isNotUndefined(retryAfterRaw)) { - const parsed = Number.parse(retryAfterRaw) - if (Option.isSome(parsed)) { - retryAfter = Duration.seconds(parsed.value) - } - } + // an invalid Retry-After is treated as absent + const retryAfter = retryAfterRaw === undefined ? undefined : retryAfterFromHeader(retryAfterRaw, now) const remainingRaw = headers["x-ratelimit-remaining-requests"] const remaining = Predicate.isNotUndefined(remainingRaw) ? Option.getOrNull(Number.parse(remainingRaw)) @@ -242,8 +247,10 @@ export const buildHttpContext = (params: { // ============================================================================= /** @internal */ -export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http, now }: { readonly status: number + /** Current time in epoch milliseconds, used to measure a Retry-After date. */ + readonly now: number readonly headers: Record readonly message: string | undefined readonly metadata: OpenAiErrorMetadata @@ -271,14 +278,14 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http return new AiError.AuthenticationError({ kind: "InvalidKey", description: errorDescription, - metadata, + metadata: { openai: metadata }, http }) case 403: return new AiError.AuthenticationError({ kind: "InsufficientPermissions", description: errorDescription, - metadata, + metadata: { openai: metadata }, http }) case 404: @@ -312,7 +319,7 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http http }) } - const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers, now) return new AiError.RateLimitError({ retryAfter, metadata: { @@ -328,13 +335,13 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http if (status >= 500) { return new AiError.InternalProviderError({ description: message ?? "Server error", - metadata, + metadata: { openai: metadata }, http }) } return new AiError.UnknownError({ description: message, - metadata, + metadata: { openai: metadata }, http }) } diff --git a/repos/effect/packages/ai/openai/src/internal/utilities.ts b/packages/ai/openai/src/internal/utilities.ts similarity index 100% rename from repos/effect/packages/ai/openai/src/internal/utilities.ts rename to packages/ai/openai/src/internal/utilities.ts diff --git a/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts b/packages/ai/openai/test/OpenAiClient.test.ts similarity index 93% rename from repos/effect/packages/ai/openai/test/OpenAiClient.test.ts rename to packages/ai/openai/test/OpenAiClient.test.ts index 2a3b3d531..4e3c64308 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiClient.test.ts +++ b/packages/ai/openai/test/OpenAiClient.test.ts @@ -6,7 +6,7 @@ import * as OpenAiClientGenerated from "@effect/ai-openai/OpenAiClientGenerated" import * as OpenAiConfig from "@effect/ai-openai/OpenAiConfig" import * as OpenAiLanguageModel from "@effect/ai-openai/OpenAiLanguageModel" import { assert, describe, it } from "@effect/vitest" -import { Config, ConfigProvider, Context, Effect, Layer, Redacted, Schema, Stream } from "effect" +import { Config, ConfigProvider, Context, Duration, Effect, Layer, Redacted, Schema, Stream } from "effect" import { Chat } from "effect/ai" import * as HttpClient from "effect/http/HttpClient" import * as HttpClientError from "effect/http/HttpClientError" @@ -309,6 +309,7 @@ describe("OpenAiClient", () => { it("preserves and truncates a fallback HTTP response", () => { const body = `${"a".repeat(200)}b` const reason = Errors.mapStatusCodeToReason({ + now: 0, status: 400, headers: {}, message: undefined, @@ -358,6 +359,28 @@ describe("OpenAiClient", () => { body: { error: { message: "Rate limit exceeded", type: "rate_limit_error", code: null } } })))) + it.effect.each([ + { name: "ignores a negative Retry-After", header: "-5", expected: undefined }, + { name: "ignores a non-finite Retry-After", header: "Infinity", expected: undefined }, + { name: "honors an HTTP-date Retry-After", header: "Thu, 01 Jan 1970 00:00:30 GMT", expected: 30_000 } + ])("$name", ({ expected, header }) => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "RateLimitError") + if (result.reason._tag !== "RateLimitError") return + assert.strictEqual( + result.reason.retryAfter === undefined ? undefined : Duration.toMillis(result.reason.retryAfter), + expected + ) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 429, + headers: { "retry-after": header }, + body: { error: { message: "Rate limit exceeded", type: "rate_limit_error", code: null } } + })))) + it.effect("maps 429 with insufficient_quota code to QuotaExhaustedError", () => Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient @@ -417,6 +440,7 @@ describe("OpenAiClient", () => { } } const reason = Errors.mapStatusCodeToReason({ + now: 0, status: 429, headers: {}, message: "You exceeded your current quota", @@ -458,14 +482,15 @@ describe("OpenAiClient", () => { body: { error: { message: "Internal server error" } } })))) - it.effect("maps schema error to InvalidOutputError reason", () => + it.effect("maps a malformed successful response envelope to a non-retryable InvalidResponseError reason", () => Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.method, "createResponse") - assert.strictEqual(result.reason._tag, "InvalidOutputError") + assert.strictEqual(result.reason._tag, "InvalidResponseError") + assert.isFalse(result.isRetryable) }).pipe(Effect.provide(makeTestLayer(undefined, { _tag: "Json", body: { invalid: "response" } as any @@ -961,6 +986,52 @@ describe("OpenAiClient", () => { ] })))) + it.effect("maps a malformed stream event to a non-retryable InvalidResponseError", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponseStream({ + model: "gpt-4o", + input: "test" + }).pipe( + Effect.andThen(([_, stream]) => Stream.runDrain(stream)), + Effect.flip + ) + + assert.strictEqual(result.reason._tag, "InvalidResponseError") + assert.isFalse(result.isRetryable) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Sse", + events: [ + { + type: "response.output_text.delta", + output_index: "zero", + item_id: "msg_123", + content_index: 0, + delta: "hello" + } + ] + })))) + + it.effect("maps an oversized SSE event to a non-retryable InvalidResponseError", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponseStream({ + model: "gpt-4o", + input: "test" + }).pipe( + Effect.andThen(([_, stream]) => Stream.runDrain(stream)), + Effect.flip + ) + + assert.strictEqual(result.reason._tag, "InvalidResponseError") + assert.isFalse(result.isRetryable) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Sse", + events: [], + // Exceed the default 10 MiB SSE limit without completing an event. + trailer: `data: ${"x".repeat(10 * 1024 * 1024 + 1)}` + })))) + it.effect("maps HTTP error before stream starts", () => Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient diff --git a/repos/effect/packages/ai/openai/test/OpenAiDecisionModel.test.ts b/packages/ai/openai/test/OpenAiDecisionModel.test.ts similarity index 89% rename from repos/effect/packages/ai/openai/test/OpenAiDecisionModel.test.ts rename to packages/ai/openai/test/OpenAiDecisionModel.test.ts index 2b4b425e0..a18ab0350 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiDecisionModel.test.ts +++ b/packages/ai/openai/test/OpenAiDecisionModel.test.ts @@ -1,7 +1,7 @@ import { OpenAiClient } from "@effect/ai-openai" import * as OpenAiDecisionModel from "@effect/ai-openai/OpenAiDecisionModel" import { assert, describe, it } from "@effect/vitest" -import { Effect, Layer, Redacted, Schema } from "effect" +import { Effect, Layer, Redacted, Result, Schema } from "effect" import { Decision, DecisionModel, Model } from "effect/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/http" @@ -250,21 +250,42 @@ describe("OpenAiDecisionModel", () => { assert.strictEqual(error.reason._tag, "InvalidOutputError") }).pipe(Effect.provide(layer(response(answers.slice(0, 2)))))) - it.effect("fails the whole call with ContentPolicyError when any answer is a refusal", () => + it.effect("decide fails with ContentPolicyError when any answer is a refusal, even beside a missing answer", () => Effect.gen(function*() { const error = yield* Effect.flip(DecisionModel.decide(Triage, { input })) assert.strictEqual(error.reason._tag, "ContentPolicyError") if (error.reason._tag === "ContentPolicyError") { assert.include(error.reason.description, "severity") assert.notInclude(error.reason.description, "team") - assert.include(JSON.stringify(error.reason.metadata.openai), "severity") } }).pipe(Effect.provide(layer(response([ - answers[0], { name: "severity", type: "refusal", refusal: "I can't help with that." }, answers[2] ]))))) + it.effect("decideSettled reports a refusal and a mismatched score per decision", () => + Effect.gen(function*() { + const { outcomes } = yield* DecisionModel.decideSettled(Triage, { input }) + assert.isTrue(Result.isFailure(outcomes.team) && outcomes.team.failure.reason === "Invalid") + assert.isTrue(Result.isFailure(outcomes.severity) && outcomes.severity.failure.reason === "Refused") + assert.deepStrictEqual(outcomes.urgent, Result.succeed({ probability: 0.99 })) + }).pipe(Effect.provide(layer(response([ + { ...answers[1], name: "team" }, + { name: "severity", type: "refusal", refusal: "I can't help with that." }, + answers[2] + ]))))) + + it.effect("a refusal outranks a later answer with the same name", () => + Effect.gen(function*() { + const error = yield* Effect.flip(DecisionModel.decide(Urgency, { input: "Checkout is down" })) + assert.strictEqual(error.reason._tag, "ContentPolicyError") + const { outcomes } = yield* DecisionModel.decideSettled(Urgency, { input: "Checkout is down" }) + assert.isTrue(Result.isFailure(outcomes.urgent) && outcomes.urgent.failure.reason === "Refused") + }).pipe(Effect.provide(layer(response([ + { name: "urgent", type: "refusal" }, + { name: "urgent", type: "predicate", probability: 0.99 } + ]))))) + it.effect("sends string and JSON state with byte array, base64 string, and data URL images", () => { const dataUrl = "data:image/webp;base64,UklGRg==" const images = [ diff --git a/repos/effect/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts b/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts similarity index 82% rename from repos/effect/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts rename to packages/ai/openai/test/OpenAiEmbeddingModel.test.ts index ffe9c4496..bd76466b0 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts +++ b/packages/ai/openai/test/OpenAiEmbeddingModel.test.ts @@ -113,6 +113,47 @@ describe("OpenAiEmbeddingModel", () => { assert.deepStrictEqual(requestBody.input, ["hello"]) })) + it.effect("concurrent embed calls batch by their withConfigOverride", () => + Effect.gen(function*() { + const requests: Array> = [] + const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.gen(function*() { + const body = yield* getRequestBody(request) + requests.push(body.input) + return jsonResponse(request, { + data: body.input.map((_: string, index: number) => ({ + index, + embedding: Array.from({ length: body.dimensions }, () => 1), + object: "embedding" + })), + model: body.model, + object: "list", + usage: { prompt_tokens: 1, total_tokens: 1 } + }) + }) + ) + )) + ) + + const responses = yield* Effect.gen(function*() { + const model = yield* EmbeddingModel.EmbeddingModel + return yield* Effect.all([ + model.embed("a").pipe(OpenAiEmbeddingModel.withConfigOverride({ dimensions: 2 })), + model.embed("b").pipe(OpenAiEmbeddingModel.withConfigOverride({ dimensions: 3 })), + model.embed("c").pipe(OpenAiEmbeddingModel.withConfigOverride({ dimensions: 2 })) + ], { concurrency: "unbounded" }) + }).pipe( + Effect.provide(OpenAiEmbeddingModel.layer({ model: "text-embedding-3-small" })), + Effect.provide(clientLayer) + ) + + assert.deepStrictEqual(responses.map((response) => response.vector), [[1, 1], [1, 1, 1], [1, 1]]) + assert.deepStrictEqual(requests, [["a", "c"], ["b"]]) + })) + it.effect("fails with InvalidOutputError when provider returns duplicate indices", () => Effect.gen(function*() { const clientLayer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( @@ -271,15 +312,14 @@ const jsonResponse = ( }) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) const noopOpenAiClient: OpenAiClient.Service = { client: undefined as unknown as OpenAiClient.Service["client"], diff --git a/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts b/packages/ai/openai/test/OpenAiLanguageModel.test.ts similarity index 98% rename from repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts rename to packages/ai/openai/test/OpenAiLanguageModel.test.ts index 197f5ff28..4dc0d3130 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts +++ b/packages/ai/openai/test/OpenAiLanguageModel.test.ts @@ -184,7 +184,7 @@ describe("OpenAiLanguageModel", () => { describe("generateText", () => { describe("message preparation", () => { - it.effect("forwards prompt cache configuration and text breakpoints", () => + it.effect("forwards prompt cache, safety configuration and text breakpoints", () => Effect.gen(function*() { const breakpoint = { mode: "explicit" } as const yield* LanguageModel.generateText({ @@ -203,7 +203,9 @@ describe("OpenAiLanguageModel", () => { }).pipe( Effect.provide(OpenAiLanguageModel.model("gpt-5.6", { prompt_cache_key: "assistant:v1", - prompt_cache_options: { mode: "explicit", ttl: "30m", comparison_response_id: "resp_baseline" } + prompt_cache_options: { mode: "explicit", ttl: "30m", comparison_response_id: "resp_baseline" }, + prompt_cache_retention: "24h", + safety_identifier: "user-hash-123" })) ) @@ -211,6 +213,8 @@ describe("OpenAiLanguageModel", () => { const body = yield* getRequestBody(requests[0]) strictEqual(body.prompt_cache_key, "assistant:v1") + strictEqual(body.prompt_cache_retention, "24h") + strictEqual(body.safety_identifier, "user-hash-123") deepStrictEqual(body.prompt_cache_options, { mode: "explicit", ttl: "30m", @@ -233,6 +237,46 @@ describe("OpenAiLanguageModel", () => { }]) }).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5.6" as any } })))) + it.effect("forwards image and file breakpoints", () => + Effect.gen(function*() { + const breakpoint = { mode: "explicit" } as const + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + Prompt.userMessage({ + content: [ + Prompt.filePart({ + mediaType: "image/png", + data: new URL("https://example.com/image.png"), + options: { openai: { promptCacheBreakpoint: breakpoint } } + }), + Prompt.filePart({ + mediaType: "application/pdf", + data: new URL("https://example.com/document.pdf"), + options: { openai: { promptCacheBreakpoint: breakpoint } } + }) + ] + }) + ]) + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-5.6"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + + deepStrictEqual(body.input, [{ + role: "user", + content: [{ + type: "input_image", + image_url: "https://example.com/image.png", + detail: "auto", + prompt_cache_breakpoint: breakpoint + }, { + type: "input_file", + file_url: "https://example.com/document.pdf", + prompt_cache_breakpoint: breakpoint + }] + }]) + }).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5.6" as any } })))) + it.effect("forwards implicit prompt cache mode without text breakpoints", () => Effect.gen(function*() { yield* LanguageModel.generateText({ prompt: "Stable context" }).pipe( @@ -2754,15 +2798,14 @@ const makeTestLayer = (options: { })) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) const decodeToolParamsFromStream = ( parts: ReadonlyArray, diff --git a/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts b/packages/ai/openai/test/OpenAiSchema.test.ts similarity index 97% rename from repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts rename to packages/ai/openai/test/OpenAiSchema.test.ts index da4e2bb83..869d1b255 100644 --- a/repos/effect/packages/ai/openai/test/OpenAiSchema.test.ts +++ b/packages/ai/openai/test/OpenAiSchema.test.ts @@ -55,6 +55,18 @@ describe("OpenAiSchema", () => { assert.strictEqual(decoded.reasoning?.effort, "max") }) + it("accepts safety and prompt cache retention request options", () => { + const decoded = Schema.decodeUnknownSync(OpenAiSchema.CreateResponse)({ + safety_identifier: "user-hash-123", + prompt_cache_retention: "24h" + }) + + assert.deepStrictEqual(decoded, { + safety_identifier: "user-hash-123", + prompt_cache_retention: "24h" + }) + }) + it("decodes a representative response payload", () => { const decoded = Schema.decodeUnknownSync(OpenAiSchema.Response)({ ...makeResponse(), diff --git a/repos/effect/packages/ai/openai/test/OpenAiTelemetry.test.ts b/packages/ai/openai/test/OpenAiTelemetry.test.ts similarity index 100% rename from repos/effect/packages/ai/openai/test/OpenAiTelemetry.test.ts rename to packages/ai/openai/test/OpenAiTelemetry.test.ts diff --git a/repos/effect/packages/ai/openai/tsconfig.json b/packages/ai/openai/tsconfig.json similarity index 100% rename from repos/effect/packages/ai/openai/tsconfig.json rename to packages/ai/openai/tsconfig.json diff --git a/repos/effect/packages/ai/openai/typetest/OpenAiDecisionModel.tst.ts b/packages/ai/openai/typetest/OpenAiDecisionModel.tst.ts similarity index 100% rename from repos/effect/packages/ai/openai/typetest/OpenAiDecisionModel.tst.ts rename to packages/ai/openai/typetest/OpenAiDecisionModel.tst.ts diff --git a/repos/effect/packages/ai/openai/typetest/OpenAiTool.tst.ts b/packages/ai/openai/typetest/OpenAiTool.tst.ts similarity index 100% rename from repos/effect/packages/ai/openai/typetest/OpenAiTool.tst.ts rename to packages/ai/openai/typetest/OpenAiTool.tst.ts diff --git a/repos/effect/packages/ai/openrouter/CHANGELOG.md b/packages/ai/openrouter/CHANGELOG.md similarity index 87% rename from repos/effect/packages/ai/openrouter/CHANGELOG.md rename to packages/ai/openrouter/CHANGELOG.md index 7e564ba4f..346409adf 100644 --- a/repos/effect/packages/ai/openrouter/CHANGELOG.md +++ b/packages/ai/openrouter/CHANGELOG.md @@ -1,5 +1,37 @@ # @effect/ai-openrouter +## 4.0.6 + +### Patch Changes + +- [#9437](https://github.com/Effect-TS/effect/pull/9437) [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b) Thanks @effect-bot! - Add `DecisionModel.decideSettled` to retain valid answers alongside per-decision `Result` failures (`Missing`, `Refused`, `NoDistribution`, or `Invalid`). Provider failures still fail the effect; `decide` remains all-or-nothing. + + OpenRouter now passes missing distributions to core validation. OpenAI reports refusals per decision and wrong-kind score answers as invalid rather than missing. + + Update hand-built services to implement `decideSettled`, handle optional provider `probabilities` and refusal answers, and use settled outcomes instead of the removed `OpenAiContentPolicyMetadata` and `metadata.openai.refusedDecisions`. + +- [#9422](https://github.com/Effect-TS/effect/pull/9422) [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7) Thanks @candrewlee14! - `DecisionModel.decide` now returns the response `id`, the `modelId` that answered and provider `metadata`. `OpenRouterDecisionModel` reports `cost` and the serving provider under `metadata.openrouter`. + +- [#9709](https://github.com/Effect-TS/effect/pull/9709) [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117) Thanks @tim-smart! - Add typed HTTP date and `Retry-After` header values to `Headers`, and parse them consistently across the HTTP client, OTLP exporter, GraphQL client, AI clients and static server. + + - `Headers.parseHttpDate` / `Headers.formatHttpDate` read all three RFC 9110 date formats as UTC, whatever the process time zone, and emit IMF-fixdate. + - `Headers.parseRetryAfter`, `Headers.formatRetryAfter` and `Headers.retryAfterDelay` accept whole non-negative seconds of any size, read exactly, or an HTTP date, and the delay is never negative. HTTP dates accept the `23:59:60` leap second, read as the following second. + - `Headers.RetryAfter` holds either a `Duration` (`RetryAfterDuration`) or a `DateTime.Utc` (`RetryAfterDateTime`). `Headers.retryAfterDuration` builds the first from any `Duration.Input`: infinite durations throw, finite negative ones become zero, and fractional seconds are rounded up when formatted. + - Parse failures are reported as `Headers.HeaderParseError`. + - `Schema.DateTimeUtcFromHttpDate` and `Schema.RetryAfterFromString` expose the same parsing as string codecs for HTTP API headers. + - Invalid `Retry-After` values such as `-5`, `1.5`, `1e3` or `Infinity` are now ignored everywhere. `HttpClient.retryTransient` and `HttpClient.withRateLimiter` no longer honour fractional seconds, and the AI clients no longer produce negative or infinite `retryAfter` durations. + - `HttpStaticServer` reads `If-Modified-Since` and `Last-Modified` as UTC; asctime dates were previously shifted by the process time zone. + +- [#9430](https://github.com/Effect-TS/effect/pull/9430) [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310) Thanks @effect-bot! - Add non-retryable `AiError.InvalidResponseError` for provider response schema and SSE decoding failures, with optional HTTP context; model-generated output validation remains retryable. Update exhaustive matches on `AiErrorReason` to handle the new tag, and update serialized-error consumers to accept it. + +- [#9502](https://github.com/Effect-TS/effect/pull/9502) [`447fdbf`](https://github.com/Effect-TS/effect/commit/447fdbf27a57d0e9e84b641bb20219941a1790e8) Thanks @typedrat! - Preserve `cacheControl` options on assistant text and tool results when sending messages to OpenRouter. + +- [#9421](https://github.com/Effect-TS/effect/pull/9421) [`e33e867`](https://github.com/Effect-TS/effect/commit/e33e867ef5823f585ba56bd2b258128804e6b212) Thanks @candrewlee14! - Correct the provider name in `OpenRouterLanguageModel.model` metadata and return type from `"openai"` to `"openrouter"`. Update explicit provider type annotations or comparisons accordingly. + +- [#9263](https://github.com/Effect-TS/effect/pull/9263) [`d453790`](https://github.com/Effect-TS/effect/commit/d45379064ace1c497970fdf772719ca3f7026028) Thanks @effect-bot! - Default schema-backed OpenRouter response formats to strict validation for models using the OpenAI schema transformer (`openai/`, `gpt-`, `o1-`, `o3-`, and `o4-` prefixes). Other models retain `strict: null`, and tool strictness is unchanged. Endpoint support still varies; callers using incompatible endpoints can set `strictJsonSchema: false` to opt out, and explicit overrides remain respected. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/ai/openrouter/README.md b/packages/ai/openrouter/README.md similarity index 100% rename from repos/effect/packages/ai/openrouter/README.md rename to packages/ai/openrouter/README.md diff --git a/repos/effect/packages/ai/openrouter/codegen.yml b/packages/ai/openrouter/codegen.yml similarity index 100% rename from repos/effect/packages/ai/openrouter/codegen.yml rename to packages/ai/openrouter/codegen.yml diff --git a/repos/effect/packages/ai/openrouter/package.json b/packages/ai/openrouter/package.json similarity index 98% rename from repos/effect/packages/ai/openrouter/package.json rename to packages/ai/openrouter/package.json index 0d63a0084..76de6e1bb 100644 --- a/repos/effect/packages/ai/openrouter/package.json +++ b/packages/ai/openrouter/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openrouter", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "An OpenRouter provider integration for Effect AI SDK", diff --git a/repos/effect/packages/ai/openrouter/src/Generated.ts b/packages/ai/openrouter/src/Generated.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/src/Generated.ts rename to packages/ai/openrouter/src/Generated.ts diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts b/packages/ai/openrouter/src/OpenRouterClient.ts similarity index 89% rename from repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts rename to packages/ai/openrouter/src/OpenRouterClient.ts index 67b4a9262..869c82e99 100644 --- a/repos/effect/packages/ai/openrouter/src/OpenRouterClient.ts +++ b/packages/ai/openrouter/src/OpenRouterClient.ts @@ -238,20 +238,20 @@ export const make = Effect.fnUntraced( const createChatCompletion: Service["createChatCompletion"] = (payload) => client.sendChatCompletionRequest({ payload, config: { includeResponse: true } }).pipe( Effect.catchTags({ - SendChatCompletionRequest400: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest401: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest402: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest403: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest404: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest408: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest413: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest422: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest429: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest500: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest502: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest503: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest524: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), - SendChatCompletionRequest529: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest400: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest401: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest402: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest403: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest404: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest408: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest413: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest422: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest429: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest500: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest502: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest503: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest524: (error) => Errors.mapClientError(error, "createChatCompletion"), + SendChatCompletionRequest529: (error) => Errors.mapClientError(error, "createChatCompletion"), HttpClientError: (error) => Errors.mapHttpClientError(error, "createChatCompletion"), SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createChatCompletion")) }) diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts b/packages/ai/openrouter/src/OpenRouterConfig.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/src/OpenRouterConfig.ts rename to packages/ai/openrouter/src/OpenRouterConfig.ts diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterDecisionModel.ts b/packages/ai/openrouter/src/OpenRouterDecisionModel.ts similarity index 76% rename from repos/effect/packages/ai/openrouter/src/OpenRouterDecisionModel.ts rename to packages/ai/openrouter/src/OpenRouterDecisionModel.ts index b548ed5d6..683f91d39 100644 --- a/repos/effect/packages/ai/openrouter/src/OpenRouterDecisionModel.ts +++ b/packages/ai/openrouter/src/OpenRouterDecisionModel.ts @@ -13,6 +13,28 @@ import * as Layer from "effect/Layer" import { OpenRouterClient } from "./OpenRouterClient.ts" import type * as OpenRouterSchema from "./OpenRouterSchema.ts" +declare module "effect/ai/DecisionModel" { + /** + * OpenRouter metadata returned with a decision response. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ + export interface DecideResponseMetadata { + readonly openrouter?: { + /** + * Credits charged for the request. + */ + readonly cost?: number + /** + * Upstream provider that served the request. + */ + readonly provider?: string + } + } +} + /** * Request options that override model defaults at call time. * @@ -42,8 +64,11 @@ export const model = ( Model.make("openrouter", model, layer({ model, config })) /** - * Creates a decision service that requires full choice and score distributions. - * Score indices map to criteria labels; cost, id, and provider metadata are omitted. + * Creates a decision service that validates full choice and score distributions. + * Missing distributions yield `"NoDistribution"` with `decideSettled` or + * `AiError.InvalidOutputError` with `decide`. + * Score indices map to criteria labels. Reported `cost` and the serving + * provider are returned under `metadata.openrouter`. * Probabilities arrive rounded to two decimals, so small sum drift is rescaled. * * @stability unstable @@ -95,15 +120,6 @@ export const make = Effect.fnUntraced(function*(options: { answers[key] = { _tag: "Probability", probability: answer.noul } continue } - if (answer.probabilities === undefined) { - return yield* AiError.make({ - module: "OpenRouterDecisionModel", - method: "decide", - reason: new AiError.InvalidOutputError({ - description: `Provider returned no probabilities for decision "${key}"` - }) - }) - } if (answer.type === "choice") { answers[key] = { _tag: "Classify", @@ -116,18 +132,31 @@ export const make = Effect.fnUntraced(function*(options: { const probabilities: Record = Object.create(null) if (decision?._tag === "Rate") { for (const [index, { label }] of decision.criteria.entries()) { - const probability = answer.probabilities[String(index)] + const probability = answer.probabilities?.[String(index)] if (probability !== undefined) { probabilities[label] = probability } } } - answers[key] = { _tag: "Rate", rating: answer.score, probabilities, confidence: answer.confidence } + answers[key] = { + _tag: "Rate", + rating: answer.score, + probabilities: answer.probabilities === undefined ? undefined : probabilities, + confidence: answer.confidence + } } } return { answers, - usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens } + usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens }, + id: response.id, + modelId: response.model, + metadata: { + openrouter: { + ...(response.usage.cost === undefined ? undefined : { cost: response.usage.cost }), + ...(response.provider === undefined ? undefined : { provider: response.provider }) + } + } } }) }) diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterError.ts b/packages/ai/openrouter/src/OpenRouterError.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/src/OpenRouterError.ts rename to packages/ai/openrouter/src/OpenRouterError.ts diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/packages/ai/openrouter/src/OpenRouterLanguageModel.ts similarity index 96% rename from repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts rename to packages/ai/openrouter/src/OpenRouterLanguageModel.ts index 0952a3504..667a76d5b 100644 --- a/repos/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts +++ b/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -72,8 +72,20 @@ export class Config extends Context.Service< /** * Whether to use strict JSON schema validation for structured outputs. * - * Only applies to models that support structured outputs. Defaults to - * `true` when structured outputs are supported. + * For schema-backed response formats, defaults to `true` when the selected + * schema transformer is the OpenAI transformer (`openai/`, `gpt-`, `o1-`, + * `o3-`, and `o4-` model prefixes). Other models, including Anthropic models, + * send `strict: null` by default, leaving strictness unspecified. Explicit + * `true` or `false` overrides either default. This does not affect tool + * strictness. + * + * **Gotchas** + * + * Transformer selection is not an endpoint-capability check. Support and + * enforcement depend on the selected model and provider. For incompatible + * endpoints, set `false` to opt out of the strict default. Strict mode may + * restrict supported JSON Schema features; see OpenRouter + * [structured output guidance](https://openrouter.ai/docs/guides/features/structured-outputs). */ readonly strictJsonSchema?: boolean | undefined } @@ -527,8 +539,8 @@ declare module "effect/ai/Response" { export const model = ( model: string, config?: Omit -): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenRouterClient> => - AiModel.make("openai", model, layer({ model, config })) +): AiModel.Model<"openrouter", LanguageModel.LanguageModel, OpenRouterClient> => + AiModel.make("openrouter", model, layer({ model, config })) /** * Creates an OpenRouter `LanguageModel` service from a model identifier and @@ -845,14 +857,16 @@ const prepareMessages = Effect.fnUntraced( } case "assistant": { - let text = "" let reasoning = "" + const textParts: Array = [] const toolCalls: Array = [] for (const part of message.content) { switch (part.type) { case "text": { - text += part.text + if (part.text.length > 0) { + textParts.push(part) + } break } @@ -902,9 +916,22 @@ const prepareMessages = Effect.fnUntraced( } } + // Tool calls cannot carry cache breakpoints; use the last text part. + const messageCacheControl = getCacheControl(message) + const content = textParts.map((part, i) => { + const cache_control = getCacheControl(part) ?? (i === textParts.length - 1 ? messageCacheControl : null) + return { + type: "text", + text: part.text, + ...(Predicate.isNotNull(cache_control) ? { cache_control } : undefined) + } + }) + messages.push({ role: "assistant", - content: text, + content: content.some((part) => Predicate.isNotNullish(part.cache_control)) + ? content + : content.map((part) => part.text).join(""), reasoning: reasoning.length > 0 ? reasoning : null, ...(Predicate.isNotNull(reasoningDetails) ? { reasoning_details: reasoningDetails } : undefined), ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : undefined) @@ -914,16 +941,20 @@ const prepareMessages = Effect.fnUntraced( } case "tool": { - for (const part of message.content) { - // Skip tool approval parts - if (part.type === "tool-approval-response") { - continue - } + const results = message.content.filter((part) => part.type === "tool-result") + const messageCacheControl = getCacheControl(message) + + for (let i = 0; i < results.length; i++) { + const part = results[i] + const text = typeof part.result === "string" ? part.result : JSON.stringify(part.result) + const cache_control = getCacheControl(part) ?? (i === results.length - 1 ? messageCacheControl : null) messages.push({ role: "tool", tool_call_id: part.id, - content: typeof part.result === "string" ? part.result : JSON.stringify(part.result) + content: Predicate.isNotNull(cache_control) && typeof text === "string" && text.length > 0 + ? [{ type: "text", text, cache_control }] + : text }) } @@ -1811,7 +1842,7 @@ const getResponseFormat = Effect.fnUntraced(function*({ config, options, transfo json_schema: { name: options.responseFormat.objectName, schema: jsonSchema, - strict: config.strictJsonSchema ?? null, + strict: config.strictJsonSchema ?? (transformer === toCodecOpenAI ? true : null), ...(Predicate.isNotUndefined(description) ? { description } : undefined) } } diff --git a/repos/effect/packages/ai/openrouter/src/OpenRouterSchema.ts b/packages/ai/openrouter/src/OpenRouterSchema.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/src/OpenRouterSchema.ts rename to packages/ai/openrouter/src/OpenRouterSchema.ts diff --git a/repos/effect/packages/ai/openrouter/src/index.ts b/packages/ai/openrouter/src/index.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/src/index.ts rename to packages/ai/openrouter/src/index.ts diff --git a/repos/effect/packages/ai/openrouter/src/internal/errors.ts b/packages/ai/openrouter/src/internal/errors.ts similarity index 83% rename from repos/effect/packages/ai/openrouter/src/internal/errors.ts rename to packages/ai/openrouter/src/internal/errors.ts index 618c2c800..6cbef4004 100644 --- a/repos/effect/packages/ai/openrouter/src/internal/errors.ts +++ b/packages/ai/openrouter/src/internal/errors.ts @@ -1,9 +1,11 @@ import * as AiError from "effect/ai/AiError" import type * as Response from "effect/ai/Response" -import * as Duration from "effect/Duration" +import * as Clock from "effect/Clock" +import type * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import type * as Sse from "effect/encoding/Sse" import { dual } from "effect/Function" +import * as Headers from "effect/http/Headers" import type * as HttpClientError from "effect/http/HttpClientError" import type * as HttpClientRequest from "effect/http/HttpClientRequest" import type * as HttpClientResponse from "effect/http/HttpClientResponse" @@ -11,6 +13,7 @@ import * as Number from "effect/Number" import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import * as Redactable from "effect/Redactable" +import * as Result from "effect/Result" import * as Schema from "effect/Schema" import type * as Generated from "../Generated.ts" import type { OpenRouterErrorMetadata } from "../OpenRouterError.ts" @@ -50,7 +53,7 @@ export const mapSchemaError = dual< AiError.make({ module: "OpenRouterClient", method, - reason: AiError.InvalidOutputError.fromSchemaError(error) + reason: AiError.InvalidResponseError.fromSchemaError(error) })) /** @internal */ @@ -61,32 +64,39 @@ export const mapSseError = dual< AiError.make({ module: "OpenRouterClient", method, - reason: new AiError.InvalidOutputError({ description: error.message }) + reason: new AiError.InvalidResponseError({ description: error.message }) })) +type ClientError = Generated.OpenRouterClientError + /** @internal */ export const mapClientError = dual< - (method: string) => (error: Generated.OpenRouterClientError) => AiError.AiError, - (error: Generated.OpenRouterClientError, method: string) => AiError.AiError ->(2, (error, method) => { - const { request, response, cause } = error - const status = response.status - const headers = response.headers as Record - const metadata: OpenRouterErrorMetadata = { - errorCode: cause.error.code ?? null, - errorType: cause.error.type ?? null, - requestId: headers["x-request-id"] ?? null - } - const http = buildHttpContext({ request, response, body: JSON.stringify(cause) }) - const reason = mapStatusCodeToReason({ - status, - headers, - message: cause.error.message, - metadata, - http + (method: string) => (error: ClientError) => Effect.Effect, + (error: ClientError, method: string) => Effect.Effect +>( + 2, + Effect.fnUntraced(function*(error: ClientError, method: string) { + const now = yield* Clock.currentTimeMillis + const { request, response, cause } = error + const status = response.status + const headers = response.headers as Record + const metadata: OpenRouterErrorMetadata = { + errorCode: cause.error.code ?? null, + errorType: cause.error.type ?? null, + requestId: headers["x-request-id"] ?? null + } + const http = buildHttpContext({ request, response, body: JSON.stringify(cause) }) + const reason = mapStatusCodeToReason({ + now, + status, + headers, + message: cause.error.message, + metadata, + http + }) + return yield* AiError.make({ module: "OpenRouterClient", method, reason }) }) - return AiError.make({ module: "OpenRouterClient", method, reason }) -}) +) /** @internal */ export const mapHttpClientError = dual< @@ -179,7 +189,9 @@ const mapStatusCodeError = Effect.fnUntraced(function*( } const decoded = Schema.decodeUnknownOption(OpenRouterErrorBody)(json) + const now = yield* Clock.currentTimeMillis const reason = mapStatusCodeToReason({ + now, status, headers, message: Option.isSome(decoded) ? decoded.value.error.message : undefined, @@ -198,16 +210,16 @@ const mapStatusCodeError = Effect.fnUntraced(function*( // Rate Limits // ============================================================================= +const retryAfterFromHeader = (raw: string, now: number): Duration.Duration | undefined => { + const retryAfter = Headers.parseRetryAfter(raw) + return Result.isSuccess(retryAfter) ? Headers.retryAfterDelay(retryAfter.success, now) : undefined +} + /** @internal */ -export const parseRateLimitHeaders = (headers: Record) => { +export const parseRateLimitHeaders = (headers: Record, now: number) => { const retryAfterRaw = headers["retry-after"] - let retryAfter: Duration.Duration | undefined - if (Predicate.isNotUndefined(retryAfterRaw)) { - const parsed = Number.parse(retryAfterRaw) - if (Option.isSome(parsed)) { - retryAfter = Duration.seconds(parsed.value) - } - } + // an invalid Retry-After is treated as absent + const retryAfter = retryAfterRaw === undefined ? undefined : retryAfterFromHeader(retryAfterRaw, now) const remainingRaw = headers["x-ratelimit-remaining-requests"] const remaining = Predicate.isNotUndefined(remainingRaw) ? Option.getOrNull(Number.parse(remainingRaw)) @@ -257,8 +269,10 @@ export const buildHttpContext = (params: { // ============================================================================= /** @internal */ -export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: { +export const mapStatusCodeToReason = ({ status, headers, message, metadata, http, now }: { readonly status: number + /** Current time in epoch milliseconds, used to measure a Retry-After date. */ + readonly now: number readonly headers: Record readonly message: string | undefined readonly metadata: OpenRouterErrorMetadata @@ -314,7 +328,7 @@ export const mapStatusCodeToReason = ({ status, headers, message, metadata, http http }) } - const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers) + const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers, now) return new AiError.RateLimitError({ retryAfter, metadata: { diff --git a/repos/effect/packages/ai/openrouter/src/internal/utilities.ts b/packages/ai/openrouter/src/internal/utilities.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/src/internal/utilities.ts rename to packages/ai/openrouter/src/internal/utilities.ts diff --git a/repos/effect/packages/ai/openrouter/test/Generated.test.ts b/packages/ai/openrouter/test/Generated.test.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/test/Generated.test.ts rename to packages/ai/openrouter/test/Generated.test.ts diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts b/packages/ai/openrouter/test/OpenRouterClient.test.ts similarity index 99% rename from repos/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts rename to packages/ai/openrouter/test/OpenRouterClient.test.ts index c2fba232d..660c7a79d 100644 --- a/repos/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts +++ b/packages/ai/openrouter/test/OpenRouterClient.test.ts @@ -97,6 +97,7 @@ describe("OpenRouterClient", () => { it("preserves and truncates a fallback HTTP response", () => { const body = `${"a".repeat(200)}b` const reason = Errors.mapStatusCodeToReason({ + now: 0, status: 400, headers: {}, message: undefined, diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts b/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts similarity index 94% rename from repos/effect/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts rename to packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts index c3da61a01..af01f1bbc 100644 --- a/repos/effect/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts +++ b/packages/ai/openrouter/test/OpenRouterDecisionModel.test.ts @@ -408,7 +408,7 @@ describe("OpenRouterDecisionModel", () => { assert.strictEqual(error.reason._tag, "InvalidUserInputError") })) - it.effect("maps a malformed successful response to a client InvalidOutputError", () => + it.effect("maps a malformed successful response to a non-retryable client InvalidResponseError", () => Effect.gen(function*() { const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), @@ -423,7 +423,7 @@ describe("OpenRouterDecisionModel", () => { assert.strictEqual(error._tag, "AiError") assert.strictEqual(error.module, "OpenRouterClient") assert.strictEqual(error.method, "createDecisions") - assert.strictEqual(error.reason._tag, "InvalidOutputError") + assert.strictEqual(error.reason._tag, "InvalidResponseError") })) it.effect("make constructs a DecisionModel service", () => @@ -462,7 +462,7 @@ describe("OpenRouterDecisionModel", () => { Effect.gen(function*() { const { confidence: _choiceConfidence, ...department } = decisionsResponse.answers.department const { confidence: _scoreConfidence, legend: _legend, ...frustration } = decisionsResponse.answers.frustration - const { answers, usage } = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( + const { answers, id, metadata, usage } = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), Effect.provide(makeClientLayer((request) => Effect.succeed(jsonResponse(request, { @@ -476,15 +476,18 @@ describe("OpenRouterDecisionModel", () => { assert.strictEqual(answers.frustration.label, "Very angry, strong language") assert.strictEqual(answers.urgent.probability, 0.999) assert.deepStrictEqual({ ...usage }, { inputTokens: 312, outputTokens: 48 }) + assert.isUndefined(id) + assert.deepStrictEqual(metadata.openrouter, {}) })) - it.effect("drops provider-specific cost, id and provider metadata", () => + it.effect("returns the response id, resolved model, cost and serving provider", () => Effect.gen(function*() { const result = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( - Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model-latest" })), Effect.provide(makeClientLayer((request) => Effect.succeed(jsonResponse(request, { ...decisionsResponse, + model: "test/decision-model-1.2", id: "decision-1", provider: "test-provider", usage: { ...decisionsResponse.usage, cost: 0.001 } @@ -492,8 +495,9 @@ describe("OpenRouterDecisionModel", () => { )) ) assert.deepStrictEqual({ ...result.usage }, { inputTokens: 312, outputTokens: 48 }) - assert.isFalse("id" in result) - assert.isFalse("provider" in result) + assert.strictEqual(result.id, "decision-1") + assert.strictEqual(result.modelId, "test/decision-model-1.2") + assert.deepStrictEqual(result.metadata.openrouter, { cost: 0.001, provider: "test-provider" }) assert.isFalse("cost" in result.answers.department) })) @@ -771,6 +775,27 @@ describe("OpenRouterDecisionModel", () => { assert.strictEqual(error.reason._tag, "InvalidOutputError") })) + it.effect("decideSettled reports choice and score answers without probabilities as NoDistribution", () => + Effect.gen(function*() { + const { probabilities: _choice, ...department } = decisionsResponse.answers.department + const { probabilities: _score, ...frustration } = decisionsResponse.answers.frustration + const { outcomes } = yield* DecisionModel.decideSettled(TicketTriage, { input: ticket }).pipe( + Effect.provide(OpenRouterDecisionModel.layer({ model: "test/decision-model" })), + Effect.provide(makeClientLayer((request) => + Effect.succeed(jsonResponse(request, { + ...decisionsResponse, + answers: { ...decisionsResponse.answers, department, frustration } + })) + )) + ) + + assert.isTrue(Result.isFailure(outcomes.department) && outcomes.department.failure.reason === "NoDistribution") + assert.isTrue( + Result.isFailure(outcomes.frustration) && outcomes.frustration.failure.reason === "NoDistribution" + ) + assert.isTrue(Result.isSuccess(outcomes.urgent)) + })) + it.effect("fails with InvalidOutputError when a score distribution skips a level index", () => Effect.gen(function*() { const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( @@ -902,12 +927,11 @@ const jsonResponse = ( }) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts b/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts similarity index 78% rename from repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts rename to packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts index 4d0723c5f..04b6c228c 100644 --- a/repos/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts +++ b/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts @@ -2,11 +2,38 @@ import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai import { assert, describe, it } from "@effect/vitest" import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { Array, Context, Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" -import { LanguageModel, Prompt, Tool, Toolkit } from "effect/ai" +import { LanguageModel, Model, Prompt, Tool, Toolkit } from "effect/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/http" describe("OpenRouterLanguageModel", () => { describe("strictJsonSchema", () => { + for ( + const [model, strict] of [ + ["openai/gpt-4o-mini", true], + ["google/gemini-2.5-flash", null] + ] as const + ) { + it.effect(`defaults response strictness to ${strict} for ${model}`, () => + Effect.gen(function*() { + yield* LanguageModel.generateObject({ + prompt: "Give me a name", + schema: Schema.Struct({ name: Schema.String }) + }).pipe(Effect.provide(OpenRouterLanguageModel.model(model))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + strictEqual(body.response_format.json_schema.strict, strict) + }).pipe(Effect.provide(makeTestLayer({ + body: { + choices: [{ + finish_reason: "stop", + index: 0, + message: { role: "assistant", content: JSON.stringify({ name: "Alice" }) } + }] + } + })))) + } + it.effect("omits false from requests while preserving response strictness", () => Effect.gen(function*() { yield* LanguageModel.generateObject({ @@ -49,6 +76,14 @@ describe("OpenRouterLanguageModel", () => { }).pipe(Effect.provide(makeStreamTestLayer([])))) }) + it.effect("model provides openrouter provider metadata", () => + Effect.gen(function*() { + strictEqual(yield* Model.ProviderName, "openrouter") + }).pipe( + Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")), + Effect.provide(makeTestLayer()) + )) + describe("generateText", () => { describe("message preparation", () => { describe("audio file parts", () => { @@ -250,6 +285,113 @@ describe("OpenRouterLanguageModel", () => { assert.isDefined(toolResult) strictEqual(toolResult.content, "PLAIN_TEXT_SENTINEL\n") }).pipe(Effect.provide(makeTestLayer()))) + + describe("cache control", () => { + const ephemeral = { type: "ephemeral" } as const + const oneHour = { type: "ephemeral", ttl: "1h" } as const + const cached = { openrouter: { cacheControl: ephemeral } } + const partCached = { openrouter: { cacheControl: oneHour } } + + it.effect("sends cached void tool results without throwing", () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_void", + name: "void_tool", + result: undefined, + isFailure: false, + providerExecuted: false + })], + options: cached + }]), + disableToolCallResolution: true + }).pipe(Effect.provide(OpenRouterLanguageModel.model("anthropic/claude-sonnet-4.5"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const toolResult = body.messages.find((message: any) => message.role === "tool") + strictEqual(toolResult.tool_call_id, "call_void") + }).pipe(Effect.provide(makeTestLayer()))) + + for (const partLevel of [false, true]) { + it.effect( + partLevel + ? "preserves part breakpoints and prefers them to message breakpoints" + : "puts message breakpoints on the last assistant text and tool result", + () => + Effect.gen(function*() { + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Look up both" }, + { + role: "assistant", + content: [ + Prompt.reasoningPart({ text: "Thinking" }), + Prompt.textPart({ text: "First", options: partLevel ? partCached : undefined }), + Prompt.textPart({ text: "Second" }), + Prompt.textPart({ text: "Last", options: partLevel ? partCached : undefined }), + ...["a", "b", "c"].map((id) => + Prompt.toolCallPart({ id, name: "lookup", params: {}, providerExecuted: false }) + ) + ], + options: cached + }, + { + role: "tool", + content: [ + ...["a", "b", "c"].map((id) => + Prompt.toolResultPart({ + id, + name: "lookup", + result: id === "c" ? { value: 42 } : id, + isFailure: false, + providerExecuted: false, + options: partLevel && id !== "b" ? partCached : undefined + }) + ), + Prompt.toolApprovalResponsePart({ approvalId: "approval", approved: true }) + ], + options: cached + } + ]), + disableToolCallResolution: true + }).pipe(Effect.provide(OpenRouterLanguageModel.model("anthropic/claude-sonnet-4.5"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const assistant = body.messages.find((message: any) => message.role === "assistant") + deepStrictEqual(assistant.content, [ + partLevel + ? { type: "text", text: "First", cache_control: oneHour } + : { type: "text", text: "First" }, + { type: "text", text: "Second" }, + { type: "text", text: "Last", cache_control: partLevel ? oneHour : ephemeral } + ]) + strictEqual(assistant.reasoning, "Thinking") + deepStrictEqual(assistant.tool_calls, [ + { id: "a", type: "function", function: { name: "lookup", arguments: "{}" } }, + { id: "b", type: "function", function: { name: "lookup", arguments: "{}" } }, + { id: "c", type: "function", function: { name: "lookup", arguments: "{}" } } + ]) + deepStrictEqual(body.messages.filter((message: any) => message.role === "tool"), [ + { + role: "tool", + tool_call_id: "a", + content: partLevel ? [{ type: "text", text: "a", cache_control: oneHour }] : "a" + }, + { role: "tool", tool_call_id: "b", content: "b" }, + { + role: "tool", + tool_call_id: "c", + content: [{ type: "text", text: "{\"value\":42}", cache_control: partLevel ? oneHour : ephemeral }] + } + ]) + }).pipe(Effect.provide(makeTestLayer())) + ) + } + }) }) describe("tool preparation", () => { @@ -626,15 +768,14 @@ const makeTestLayer = (options: { })) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) const makeStreamTestLayer = (events: ReadonlyArray) => { const body = events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + "data: [DONE]\n\n" diff --git a/repos/effect/packages/ai/openrouter/test/OpenRouterSchema.test.ts b/packages/ai/openrouter/test/OpenRouterSchema.test.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/test/OpenRouterSchema.test.ts rename to packages/ai/openrouter/test/OpenRouterSchema.test.ts diff --git a/repos/effect/packages/ai/openrouter/tsconfig.json b/packages/ai/openrouter/tsconfig.json similarity index 100% rename from repos/effect/packages/ai/openrouter/tsconfig.json rename to packages/ai/openrouter/tsconfig.json diff --git a/repos/effect/packages/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts b/packages/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts similarity index 100% rename from repos/effect/packages/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts rename to packages/ai/openrouter/typetest/OpenRouterDecisionModel.tst.ts diff --git a/repos/effect/packages/ai/typesafe/CHANGELOG.md b/packages/ai/typesafe/CHANGELOG.md similarity index 64% rename from repos/effect/packages/ai/typesafe/CHANGELOG.md rename to packages/ai/typesafe/CHANGELOG.md index b8eae2571..ab018bc33 100644 --- a/repos/effect/packages/ai/typesafe/CHANGELOG.md +++ b/packages/ai/typesafe/CHANGELOG.md @@ -1,5 +1,23 @@ # @effect/ai-typesafe +## 4.0.6 + +### Patch Changes + +- [#9709](https://github.com/Effect-TS/effect/pull/9709) [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117) Thanks @tim-smart! - Add typed HTTP date and `Retry-After` header values to `Headers`, and parse them consistently across the HTTP client, OTLP exporter, GraphQL client, AI clients and static server. + + - `Headers.parseHttpDate` / `Headers.formatHttpDate` read all three RFC 9110 date formats as UTC, whatever the process time zone, and emit IMF-fixdate. + - `Headers.parseRetryAfter`, `Headers.formatRetryAfter` and `Headers.retryAfterDelay` accept whole non-negative seconds of any size, read exactly, or an HTTP date, and the delay is never negative. HTTP dates accept the `23:59:60` leap second, read as the following second. + - `Headers.RetryAfter` holds either a `Duration` (`RetryAfterDuration`) or a `DateTime.Utc` (`RetryAfterDateTime`). `Headers.retryAfterDuration` builds the first from any `Duration.Input`: infinite durations throw, finite negative ones become zero, and fractional seconds are rounded up when formatted. + - Parse failures are reported as `Headers.HeaderParseError`. + - `Schema.DateTimeUtcFromHttpDate` and `Schema.RetryAfterFromString` expose the same parsing as string codecs for HTTP API headers. + - Invalid `Retry-After` values such as `-5`, `1.5`, `1e3` or `Infinity` are now ignored everywhere. `HttpClient.retryTransient` and `HttpClient.withRateLimiter` no longer honour fractional seconds, and the AI clients no longer produce negative or infinite `retryAfter` durations. + - `HttpStaticServer` reads `If-Modified-Since` and `Last-Modified` as UTC; asctime dates were previously shifted by the process time zone. + +- [#9430](https://github.com/Effect-TS/effect/pull/9430) [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310) Thanks @effect-bot! - Add non-retryable `AiError.InvalidResponseError` for provider response schema and SSE decoding failures, with optional HTTP context; model-generated output validation remains retryable. Update exhaustive matches on `AiErrorReason` to handle the new tag, and update serialized-error consumers to accept it. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/ai/typesafe/README.md b/packages/ai/typesafe/README.md similarity index 100% rename from repos/effect/packages/ai/typesafe/README.md rename to packages/ai/typesafe/README.md diff --git a/repos/effect/packages/ai/typesafe/package.json b/packages/ai/typesafe/package.json similarity index 98% rename from repos/effect/packages/ai/typesafe/package.json rename to packages/ai/typesafe/package.json index 0c89df764..1b01bedce 100644 --- a/repos/effect/packages/ai/typesafe/package.json +++ b/packages/ai/typesafe/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-typesafe", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "A TypeSafe DecisionModel provider for Effect", diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeClient.ts b/packages/ai/typesafe/src/TypeSafeClient.ts similarity index 100% rename from repos/effect/packages/ai/typesafe/src/TypeSafeClient.ts rename to packages/ai/typesafe/src/TypeSafeClient.ts diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeConfig.ts b/packages/ai/typesafe/src/TypeSafeConfig.ts similarity index 100% rename from repos/effect/packages/ai/typesafe/src/TypeSafeConfig.ts rename to packages/ai/typesafe/src/TypeSafeConfig.ts diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeDecisionModel.ts b/packages/ai/typesafe/src/TypeSafeDecisionModel.ts similarity index 100% rename from repos/effect/packages/ai/typesafe/src/TypeSafeDecisionModel.ts rename to packages/ai/typesafe/src/TypeSafeDecisionModel.ts diff --git a/repos/effect/packages/ai/typesafe/src/TypeSafeSchema.ts b/packages/ai/typesafe/src/TypeSafeSchema.ts similarity index 100% rename from repos/effect/packages/ai/typesafe/src/TypeSafeSchema.ts rename to packages/ai/typesafe/src/TypeSafeSchema.ts diff --git a/repos/effect/packages/ai/typesafe/src/index.ts b/packages/ai/typesafe/src/index.ts similarity index 100% rename from repos/effect/packages/ai/typesafe/src/index.ts rename to packages/ai/typesafe/src/index.ts diff --git a/repos/effect/packages/ai/typesafe/src/internal/errors.ts b/packages/ai/typesafe/src/internal/errors.ts similarity index 85% rename from repos/effect/packages/ai/typesafe/src/internal/errors.ts rename to packages/ai/typesafe/src/internal/errors.ts index 388bcdb18..2aa0eb931 100644 --- a/repos/effect/packages/ai/typesafe/src/internal/errors.ts +++ b/packages/ai/typesafe/src/internal/errors.ts @@ -1,11 +1,14 @@ import * as AiError from "effect/ai/AiError" +import * as Clock from "effect/Clock" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" +import * as Headers from "effect/http/Headers" import type * as HttpClientError from "effect/http/HttpClientError" import type * as HttpClientRequest from "effect/http/HttpClientRequest" import * as Num from "effect/Number" import * as Option from "effect/Option" import * as Redactable from "effect/Redactable" +import * as Result from "effect/Result" import * as Schema from "effect/Schema" const requestDetails = (request: HttpClientRequest.HttpClientRequest) => ({ @@ -30,22 +33,22 @@ const nonNegative = (value: string | undefined): number | undefined => { return parsed !== undefined && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined } -const retryAfter = (headers: Readonly>) => { +// `retry-after-ms` is TypeSafe-specific and wins over the standard header; +// an invalid Retry-After is treated as absent +const retryAfter = (headers: Readonly>, now: number) => { const ms = nonNegative(headers["retry-after-ms"]) if (ms !== undefined) return Duration.millis(ms) const raw = headers["retry-after"] - if (raw === undefined || raw.trim() === "") return undefined - const seconds = nonNegative(raw) - if (seconds !== undefined) return Duration.seconds(seconds) - const date = Date.parse(raw) - return Number.isNaN(date) ? undefined : Duration.millis(Math.max(0, date - Date.now())) + if (raw === undefined) return undefined + const retryAfter = Headers.parseRetryAfter(raw) + return Result.isSuccess(retryAfter) ? Headers.retryAfterDelay(retryAfter.success, now) : undefined } export const mapSchemaError = (error: Schema.SchemaError, method: string): AiError.AiError => AiError.make({ module: "TypeSafeClient", method, - reason: AiError.InvalidOutputError.fromSchemaError(error) + reason: AiError.InvalidResponseError.fromSchemaError(error) }) export const mapHttpClientError = Effect.fnUntraced( @@ -90,7 +93,7 @@ export const mapHttpClientError = Effect.fnUntraced( }) if (response.status === 429) { reason = new AiError.RateLimitError({ - retryAfter: retryAfter(response.headers), + retryAfter: retryAfter(response.headers, yield* Clock.currentTimeMillis), http, metadata: { typesafe: { diff --git a/repos/effect/packages/ai/typesafe/test/TypeSafeClient.test.ts b/packages/ai/typesafe/test/TypeSafeClient.test.ts similarity index 95% rename from repos/effect/packages/ai/typesafe/test/TypeSafeClient.test.ts rename to packages/ai/typesafe/test/TypeSafeClient.test.ts index c3340a5a9..e526dbc62 100644 --- a/repos/effect/packages/ai/typesafe/test/TypeSafeClient.test.ts +++ b/packages/ai/typesafe/test/TypeSafeClient.test.ts @@ -6,7 +6,7 @@ import * as HttpClient from "effect/http/HttpClient" import * as HttpClientError from "effect/http/HttpClientError" import * as HttpClientRequest from "effect/http/HttpClientRequest" import * as HttpClientResponse from "effect/http/HttpClientResponse" -import { vi } from "vitest" +import { TestClock } from "effect/testing" const systemOneRequest = { state: "My card was charged twice.", model: "jev-latest", @@ -259,18 +259,14 @@ describe("TypeSafeClient", () => { ) { it.effect("honors a " + name + " HTTP-date Retry-After", () => Effect.gen(function*() { - const now = vi.spyOn(Date, "now").mockReturnValue(Date.parse("2026-09-18T12:00:00Z")) - try { - const client = yield* TypeSafeClient.TypeSafeClient - const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) - - assert.strictEqual(result.reason._tag, "RateLimitError") - if (result.reason._tag === "RateLimitError") { - assert.isDefined(result.reason.retryAfter) - assert.strictEqual(Duration.toMillis(result.reason.retryAfter!), expected) - } - } finally { - now.mockRestore() + yield* TestClock.setTime(Date.parse("2026-09-18T12:00:00Z")) + const client = yield* TypeSafeClient.TypeSafeClient + const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "RateLimitError") + if (result.reason._tag === "RateLimitError") { + assert.isDefined(result.reason.retryAfter) + assert.strictEqual(Duration.toMillis(result.reason.retryAfter!), expected) } }).pipe(Effect.provide(makeTestLayer(undefined, { status: 429, @@ -435,14 +431,14 @@ describe("TypeSafeClient", () => { body: { message: "Internal error" } })))) - it.effect("maps a malformed success body to InvalidOutputError reason", () => + it.effect("maps a malformed success body to a non-retryable InvalidResponseError reason", () => Effect.gen(function*() { const client = yield* TypeSafeClient.TypeSafeClient const result = yield* client.systemOne(systemOneRequest).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.method, "systemOne") - assert.strictEqual(result.reason._tag, "InvalidOutputError") + assert.strictEqual(result.reason._tag, "InvalidResponseError") }).pipe(Effect.provide(makeTestLayer(undefined, { status: 200, body: { model: "jev-latest", answers: { urgent: { type: "noul" } } } @@ -557,12 +553,11 @@ const makeResponse = ( }) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) diff --git a/repos/effect/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts b/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts similarity index 96% rename from repos/effect/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts rename to packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts index 071451bd0..4d1e5d2c4 100644 --- a/repos/effect/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts +++ b/packages/ai/typesafe/test/TypeSafeDecisionModel.test.ts @@ -152,11 +152,12 @@ describe("TypeSafeDecisionModel", () => { })) for ( - const [name, probabilities] of [ - ["out-of-range", { a: 1.01, b: -0.02 }], - ["all-zero", { a: 0, b: 0 }], - ["missing label", { a: 0.5 }], - ["non-finite", { a: Infinity, b: 0.5 }] + const [name, probabilities, reason] of [ + ["out-of-range", { a: 1.01, b: -0.02 }, "InvalidOutputError"], + ["all-zero", { a: 0, b: 0 }, "InvalidOutputError"], + ["missing label", { a: 0.5 }, "InvalidOutputError"], + // `Infinity` serializes as `null`, which the client response schema rejects + ["non-finite", { a: Infinity, b: 0.5 }, "InvalidResponseError"] ] as const ) { it.effect("rejects " + name + " choice probabilities", () => @@ -175,7 +176,7 @@ describe("TypeSafeDecisionModel", () => { )), Effect.flip ) - assert.strictEqual(error.reason._tag, "InvalidOutputError") + assert.strictEqual(error.reason._tag, reason) })) } @@ -603,7 +604,7 @@ describe("TypeSafeDecisionModel", () => { assert.strictEqual(error.reason._tag, "InvalidOutputError") })) - it.effect("fails with InvalidOutputError when a score answer omits probabilities", () => + it.effect("fails with a non-retryable InvalidResponseError when a score answer omits probabilities", () => Effect.gen(function*() { const { probabilities: _probabilities, ...frustration } = systemOneResponse.answers.frustration const error = yield* DecisionModel.decide(TicketTriage, { input: ticket }).pipe( @@ -618,7 +619,7 @@ describe("TypeSafeDecisionModel", () => { ) assert.strictEqual(error._tag, "AiError") - assert.strictEqual(error.reason._tag, "InvalidOutputError") + assert.strictEqual(error.reason._tag, "InvalidResponseError") })) it.effect("fails with InvalidOutputError when a score distribution skips a level index", () => @@ -731,15 +732,14 @@ const jsonResponse = ( }) ) -const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => - Effect.gen(function*() { - const body = request.body - if (body._tag === "Uint8Array") { - const text = new TextDecoder().decode(body.body) - return JSON.parse(text) - } - return yield* Effect.die(new Error("Expected Uint8Array body")) - }) +const getRequestBody = Effect.fnUntraced(function*(request: HttpClientRequest.HttpClientRequest) { + const body = request.body + if (body._tag === "Uint8Array") { + const text = new TextDecoder().decode(body.body) + return JSON.parse(text) + } + return yield* Effect.die(new Error("Expected Uint8Array body")) +}) const noopTypeSafeClient: TypeSafeClient.Service = { client: undefined as unknown as TypeSafeClient.Service["client"], diff --git a/repos/effect/packages/ai/typesafe/test/TypeSafeSchema.test.ts b/packages/ai/typesafe/test/TypeSafeSchema.test.ts similarity index 100% rename from repos/effect/packages/ai/typesafe/test/TypeSafeSchema.test.ts rename to packages/ai/typesafe/test/TypeSafeSchema.test.ts diff --git a/repos/effect/packages/ai/typesafe/tsconfig.json b/packages/ai/typesafe/tsconfig.json similarity index 100% rename from repos/effect/packages/ai/typesafe/tsconfig.json rename to packages/ai/typesafe/tsconfig.json diff --git a/repos/effect/packages/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts b/packages/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts similarity index 100% rename from repos/effect/packages/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts rename to packages/ai/typesafe/typetest/TypeSafeDecisionModel.tst.ts diff --git a/repos/effect/packages/atom/react/CHANGELOG.md b/packages/atom/react/CHANGELOG.md similarity index 88% rename from repos/effect/packages/atom/react/CHANGELOG.md rename to packages/atom/react/CHANGELOG.md index 1d20aef21..7888a0426 100644 --- a/repos/effect/packages/atom/react/CHANGELOG.md +++ b/packages/atom/react/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/atom-react +## 4.0.6 + +### Patch Changes + +- [#9522](https://github.com/Effect-TS/effect/pull/9522) [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0) Thanks @tim-smart! - Add a `skipInitialRun` option to `Atom.serializable` and `Atom.initialValue`. A seeded effect or stream atom then keeps its hydrated or initial value instead of running on mount, until it is refreshed or a dependency changes. Factory `get(...)` calls and `withReactivity` keys are still tracked; dependencies read inside the effect are tracked from its first real run. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/atom/react/LICENSE b/packages/atom/react/LICENSE similarity index 100% rename from repos/effect/packages/atom/react/LICENSE rename to packages/atom/react/LICENSE diff --git a/repos/effect/packages/atom/react/README.md b/packages/atom/react/README.md similarity index 100% rename from repos/effect/packages/atom/react/README.md rename to packages/atom/react/README.md diff --git a/repos/effect/packages/atom/react/package.json b/packages/atom/react/package.json similarity index 96% rename from repos/effect/packages/atom/react/package.json rename to packages/atom/react/package.json index 543733154..1995def59 100644 --- a/repos/effect/packages/atom/react/package.json +++ b/packages/atom/react/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-react", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "React bindings for the Effect Atom modules", @@ -73,8 +73,8 @@ "@types/scheduler": "^0.28.0", "effect": "workspace:^", "jsdom": "~30.0.1", - "react": "^19.3.0", - "react-dom": "^19.3.0", + "react": "^19.3.1", + "react-dom": "^19.3.1", "react-error-boundary": "^6.1.6", "scheduler": "^0.28.0" } diff --git a/repos/effect/packages/atom/react/src/Hooks.ts b/packages/atom/react/src/Hooks.ts similarity index 96% rename from repos/effect/packages/atom/react/src/Hooks.ts rename to packages/atom/react/src/Hooks.ts index 87b753565..1087081a0 100644 --- a/repos/effect/packages/atom/react/src/Hooks.ts +++ b/packages/atom/react/src/Hooks.ts @@ -244,6 +244,13 @@ export const useAtomMount = (atom: Atom.Atom): void => { * accepts a write value or updater function; for `AsyncResult` atoms, `promise` * and `promiseExit` modes return a promise for the success value or full `Exit`. * + * **Gotchas** + * + * In `promise` and `promiseExit` modes, the setter observes the atom's next + * settled result, not a result correlated with that write. Overlapping writes + * can resolve with another write's result. Serialize writes when each caller + * needs its own result. + * * @see {@link useAtom} for reading and updating the same writable atom * * @stability unstable @@ -309,6 +316,13 @@ export const useAtomRefresh = (atom: Atom.Atom): () => void => { * Use when a React component needs both to render the current value of a * writable atom and update it from the same component. * + * **Gotchas** + * + * In `promise` and `promiseExit` modes, the setter observes the atom's next + * settled result, not a result correlated with that write. Overlapping writes + * can resolve with another write's result. Serialize writes when each caller + * needs its own result. + * * @see {@link useAtomValue} for subscribing to an atom without a setter * @see {@link useAtomSet} for updating a writable atom without subscribing to its value * diff --git a/repos/effect/packages/atom/react/src/ReactHydration.ts b/packages/atom/react/src/ReactHydration.ts similarity index 100% rename from repos/effect/packages/atom/react/src/ReactHydration.ts rename to packages/atom/react/src/ReactHydration.ts diff --git a/repos/effect/packages/atom/react/src/RegistryContext.ts b/packages/atom/react/src/RegistryContext.ts similarity index 98% rename from repos/effect/packages/atom/react/src/RegistryContext.ts rename to packages/atom/react/src/RegistryContext.ts index 3509dccff..d6aab5ae0 100644 --- a/repos/effect/packages/atom/react/src/RegistryContext.ts +++ b/packages/atom/react/src/RegistryContext.ts @@ -78,7 +78,7 @@ export const RegistryContext = React.createContext(At */ export const RegistryProvider = (options: { readonly children?: React.ReactNode | undefined - readonly initialValues?: Iterable, any]> | undefined + readonly initialValues?: Iterable, any, Atom.InitialValueOptions?]> | undefined readonly scheduleTask?: ((f: () => void) => () => void) | undefined readonly timeoutResolution?: number | undefined readonly defaultIdleTTL?: number | undefined diff --git a/repos/effect/packages/atom/react/src/ScopedAtom.ts b/packages/atom/react/src/ScopedAtom.ts similarity index 100% rename from repos/effect/packages/atom/react/src/ScopedAtom.ts rename to packages/atom/react/src/ScopedAtom.ts diff --git a/repos/effect/packages/atom/react/src/index.ts b/packages/atom/react/src/index.ts similarity index 100% rename from repos/effect/packages/atom/react/src/index.ts rename to packages/atom/react/src/index.ts diff --git a/repos/effect/packages/atom/react/test/index.test.tsx b/packages/atom/react/test/index.test.tsx similarity index 94% rename from repos/effect/packages/atom/react/test/index.test.tsx rename to packages/atom/react/test/index.test.tsx index aae21c613..bf5ef24e7 100644 --- a/repos/effect/packages/atom/react/test/index.test.tsx +++ b/packages/atom/react/test/index.test.tsx @@ -1,4 +1,5 @@ // +import { assert } from "@effect/vitest" import { act, render, screen, waitFor } from "@testing-library/react" import { Cause, Context, Effect, Latch, Layer } from "effect" import * as AsyncResult from "effect/reactivity/AsyncResult" @@ -482,6 +483,9 @@ describe("atom-react", { concurrent: false }, () => { ) expect(screen.getByTestId("value")).toHaveTextContent("Initial") + // Hydration starts a client effect while the server effect is still pending. + expect(start).toBe(2) + expect(stop).toBe(0) act(() => { Effect.runSync(latch.open) @@ -495,8 +499,44 @@ describe("atom-react", { concurrent: false }, () => { } expect(screen.getByTestId("value")).toHaveTextContent("Success") - expect(start).toBe(1) - expect(stop).toBe(1) + expect(start).toBe(2) + expect(stop).toBe(2) + }) + + test("HydrationBoundary updates existing readers after commit", () => { + const count = Atom.make(0).pipe( + Atom.serializable({ key: "existing-count", schema: Schema.Number }) + ) + const server = AtomRegistry.make() + try { + server.set(count, 5) + const state = Hydration.dehydrate(server) + + function Count({ id }: { id: string }) { + return
{useAtomValue(count)}
+ } + + const { rerender } = render( + + + + ) + assert.strictEqual(screen.getByTestId("header").textContent, "0") + + rerender( + + + + + + + ) + + assert.strictEqual(screen.getByTestId("header").textContent, "5") + assert.strictEqual(screen.getByTestId("page").textContent, "5") + } finally { + server.dispose() + } }) test("HydrationBoundary splits new vs existing atoms", () => { diff --git a/repos/effect/packages/atom/react/tsconfig.json b/packages/atom/react/tsconfig.json similarity index 100% rename from repos/effect/packages/atom/react/tsconfig.json rename to packages/atom/react/tsconfig.json diff --git a/repos/effect/packages/atom/react/vitest.setup.ts b/packages/atom/react/vitest.setup.ts similarity index 100% rename from repos/effect/packages/atom/react/vitest.setup.ts rename to packages/atom/react/vitest.setup.ts diff --git a/repos/effect/packages/atom/solid/CHANGELOG.md b/packages/atom/solid/CHANGELOG.md similarity index 88% rename from repos/effect/packages/atom/solid/CHANGELOG.md rename to packages/atom/solid/CHANGELOG.md index 3b20ba862..43f5b2ebf 100644 --- a/repos/effect/packages/atom/solid/CHANGELOG.md +++ b/packages/atom/solid/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/atom-solid +## 4.0.6 + +### Patch Changes + +- [#9522](https://github.com/Effect-TS/effect/pull/9522) [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0) Thanks @tim-smart! - Add a `skipInitialRun` option to `Atom.serializable` and `Atom.initialValue`. A seeded effect or stream atom then keeps its hydrated or initial value instead of running on mount, until it is refreshed or a dependency changes. Factory `get(...)` calls and `withReactivity` keys are still tracked; dependencies read inside the effect are tracked from its first real run. +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/atom/solid/LICENSE b/packages/atom/solid/LICENSE similarity index 100% rename from repos/effect/packages/atom/solid/LICENSE rename to packages/atom/solid/LICENSE diff --git a/repos/effect/packages/atom/solid/README.md b/packages/atom/solid/README.md similarity index 100% rename from repos/effect/packages/atom/solid/README.md rename to packages/atom/solid/README.md diff --git a/repos/effect/packages/atom/solid/package.json b/packages/atom/solid/package.json similarity index 98% rename from repos/effect/packages/atom/solid/package.json rename to packages/atom/solid/package.json index 9f614f0cd..41f3227d6 100644 --- a/repos/effect/packages/atom/solid/package.json +++ b/packages/atom/solid/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-solid", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "SolidJS bindings for the Effect Atom modules", diff --git a/repos/effect/packages/atom/solid/src/Hooks.ts b/packages/atom/solid/src/Hooks.ts similarity index 95% rename from repos/effect/packages/atom/solid/src/Hooks.ts rename to packages/atom/solid/src/Hooks.ts index 7c0877fa6..97c11d3df 100644 --- a/repos/effect/packages/atom/solid/src/Hooks.ts +++ b/packages/atom/solid/src/Hooks.ts @@ -151,6 +151,13 @@ export const useAtomMount =
(atom: () => Atom.Atom): void => { /** * Returns a setter for a writable atom without subscribing to its value. * + * **Gotchas** + * + * In `promise` and `promiseExit` modes, the setter observes the atom's next + * settled result, not a result correlated with that write. Overlapping writes + * can resolve with another write's result. Serialize writes when each caller + * needs its own result. + * * @stability unstable * @category hooks * @since 4.0.0 @@ -206,6 +213,13 @@ export const useAtomRefresh = (atom: () => Atom.Atom): () => void => { * `AsyncResult` atoms, `promise` and `promiseExit` modes return promises for the * success value or full `Exit`. * + * **Gotchas** + * + * In `promise` and `promiseExit` modes, the setter observes the atom's next + * settled result, not a result correlated with that write. Overlapping writes + * can resolve with another write's result. Serialize writes when each caller + * needs its own result. + * * @see {@link useAtomValue} for subscribing to an atom without a setter * @see {@link useAtomSet} for updating a writable atom without subscribing to its value * diff --git a/repos/effect/packages/atom/solid/src/RegistryContext.ts b/packages/atom/solid/src/RegistryContext.ts similarity index 98% rename from repos/effect/packages/atom/solid/src/RegistryContext.ts rename to packages/atom/solid/src/RegistryContext.ts index ab327f76f..b67adecd6 100644 --- a/repos/effect/packages/atom/solid/src/RegistryContext.ts +++ b/packages/atom/solid/src/RegistryContext.ts @@ -62,7 +62,7 @@ export const RegistryContext = createContext(AtomRegi */ export const RegistryProvider = (options: { readonly children?: JSX.Element | undefined - readonly initialValues?: Iterable, any]> | undefined + readonly initialValues?: Iterable, any, Atom.InitialValueOptions?]> | undefined readonly scheduleTask?: ((f: () => void) => () => void) | undefined readonly timeoutResolution?: number | undefined readonly defaultIdleTTL?: number | undefined diff --git a/repos/effect/packages/atom/solid/src/index.ts b/packages/atom/solid/src/index.ts similarity index 100% rename from repos/effect/packages/atom/solid/src/index.ts rename to packages/atom/solid/src/index.ts diff --git a/repos/effect/packages/atom/solid/test/index.test.tsx b/packages/atom/solid/test/index.test.tsx similarity index 100% rename from repos/effect/packages/atom/solid/test/index.test.tsx rename to packages/atom/solid/test/index.test.tsx diff --git a/repos/effect/packages/atom/solid/tsconfig.json b/packages/atom/solid/tsconfig.json similarity index 100% rename from repos/effect/packages/atom/solid/tsconfig.json rename to packages/atom/solid/tsconfig.json diff --git a/repos/effect/packages/atom/solid/vitest.setup.ts b/packages/atom/solid/vitest.setup.ts similarity index 100% rename from repos/effect/packages/atom/solid/vitest.setup.ts rename to packages/atom/solid/vitest.setup.ts diff --git a/repos/effect/packages/atom/vue/CHANGELOG.md b/packages/atom/vue/CHANGELOG.md similarity index 88% rename from repos/effect/packages/atom/vue/CHANGELOG.md rename to packages/atom/vue/CHANGELOG.md index 1cf87edc1..b7df3fe56 100644 --- a/repos/effect/packages/atom/vue/CHANGELOG.md +++ b/packages/atom/vue/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/atom-vue +## 4.0.6 + +### Patch Changes + +- Updated dependencies [[`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4), [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199), [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87), [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26), [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a), [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613), [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0), [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0), [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e), [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6), [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a), [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca), [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded), [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53), [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f), [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39), [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a), [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19), [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b), [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82), [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67), [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a), [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a), [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2), [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951), [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2), [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770), [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52), [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f), [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd), [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008), [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4), [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b), [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560), [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc), [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4), [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf), [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34), [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7), [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372), [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596), [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a), [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b), [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6), [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3), [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540), [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9), [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d), [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e), [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616), [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b), [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7), [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017), [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c), [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3), [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc), [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1), [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06), [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48), [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484), [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685), [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1), [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10), [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362), [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a), [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad), [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04), [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c), [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6), [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7), [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f), [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6), [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863), [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868), [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a), [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75), [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f), [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85), [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31), [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242), [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4), [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16), [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027), [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1), [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369), [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660), [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3), [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d), [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5), [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18), [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19), [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578), [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8), [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f), [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672), [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd), [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4), [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6), [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624), [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b), [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a), [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692), [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628), [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f), [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36), [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc), [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d), [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f), [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2), [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420), [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65), [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e), [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055), [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956), [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8), [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb), [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623), [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7), [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9), [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f), [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df), [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435), [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817), [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496), [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1), [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de), [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff), [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc), [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47), [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505), [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117), [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe), [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32), [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a), [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799), [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c), [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192), [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd), [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b), [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d), [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b), [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c), [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613), [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013), [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b), [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170), [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04), [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56), [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80), [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2), [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348), [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be), [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922), [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085), [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa), [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e), [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715), [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799), [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310), [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9), [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e), [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c), [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255), [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c), [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219), [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1), [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40), [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853), [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3), [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd), [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df), [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e), [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6), [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e), [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87), [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a), [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071), [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694), [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c), [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00), [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457), [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5), [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64), [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762), [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3), [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a), [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255), [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf), [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705), [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d), [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e), [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead), [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd), [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15), [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087), [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9), [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c), [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84), [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5), [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e), [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d), [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da), [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049), [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307), [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189), [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635), [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea), [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33), [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a), [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4), [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34), [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0), [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3), [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c), [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2), [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec), [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2), [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55), [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7), [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa), [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c), [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247), [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f), [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d), [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2), [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38), [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc), [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6), [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61), [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f), [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c), [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce), [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92), [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d), [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708), [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b), [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd), [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0), [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65), [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724), [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6), [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986)]: + - effect@4.0.6 + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/atom/vue/LICENSE b/packages/atom/vue/LICENSE similarity index 100% rename from repos/effect/packages/atom/vue/LICENSE rename to packages/atom/vue/LICENSE diff --git a/repos/effect/packages/atom/vue/README.md b/packages/atom/vue/README.md similarity index 100% rename from repos/effect/packages/atom/vue/README.md rename to packages/atom/vue/README.md diff --git a/repos/effect/packages/atom/vue/package.json b/packages/atom/vue/package.json similarity index 97% rename from repos/effect/packages/atom/vue/package.json rename to packages/atom/vue/package.json index d983ef8ba..ca77f48e6 100644 --- a/repos/effect/packages/atom/vue/package.json +++ b/packages/atom/vue/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-vue", - "version": "4.0.5", + "version": "4.0.6", "type": "module", "license": "MIT", "description": "Vue bindings for the Effect Atom modules", @@ -61,7 +61,7 @@ }, "devDependencies": { "effect": "workspace:^", - "vue": "^3.5.43" + "vue": "^3.5.44" }, "peerDependencies": { "effect": "workspace:^", diff --git a/repos/effect/packages/atom/vue/src/index.ts b/packages/atom/vue/src/index.ts similarity index 92% rename from repos/effect/packages/atom/vue/src/index.ts rename to packages/atom/vue/src/index.ts index 39e0a210f..ce72794e0 100644 --- a/repos/effect/packages/atom/vue/src/index.ts +++ b/packages/atom/vue/src/index.ts @@ -111,6 +111,13 @@ const useAtomValueRef = >(atom: () => A) => { /** * Returns a reactive atom value and a setter with the requested write mode. * + * **Gotchas** + * + * In `promise` and `promiseExit` modes, the setter observes the atom's next + * settled result, not a result correlated with that write. Overlapping writes + * can resolve with another write's result. Serialize writes when each caller + * needs its own result. + * * @stability unstable * @category composables * @since 4.0.0 @@ -194,6 +201,13 @@ function setAtom /** * Returns a setter for an atom, mounting it for the lifetime of the composable. * + * **Gotchas** + * + * In `promise` and `promiseExit` modes, the setter observes the atom's next + * settled result, not a result correlated with that write. Overlapping writes + * can resolve with another write's result. Serialize writes when each caller + * needs its own result. + * * @stability unstable * @category composables * @since 4.0.0 diff --git a/repos/effect/packages/atom/vue/test/index.test.ts b/packages/atom/vue/test/index.test.ts similarity index 100% rename from repos/effect/packages/atom/vue/test/index.test.ts rename to packages/atom/vue/test/index.test.ts diff --git a/repos/effect/packages/atom/vue/tsconfig.json b/packages/atom/vue/tsconfig.json similarity index 100% rename from repos/effect/packages/atom/vue/tsconfig.json rename to packages/atom/vue/tsconfig.json diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/cli/.madgerc b/packages/cli/.madgerc deleted file mode 100644 index b407c6b4b..000000000 --- a/packages/cli/.madgerc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "detectiveOptions": { - "ts": { - "skipTypeImports": true - } - } -} diff --git a/packages/cli/.npmignore b/packages/cli/.npmignore deleted file mode 100644 index e4c612bb4..000000000 --- a/packages/cli/.npmignore +++ /dev/null @@ -1,14 +0,0 @@ -*.test.js -*.test.d.ts -tsconfig.build.json -*.log -tslint.json -.eslintrc.js -.eslintignore -.madgerc -.swcrc -.cache -coverage/ -.tsbuildinfo -.build/ -!dist/ diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md deleted file mode 100644 index 1195e6840..000000000 --- a/packages/cli/CHANGELOG.md +++ /dev/null @@ -1,1793 +0,0 @@ -# @effect-app/cli - -## 2.1.1 - -### Patch Changes - -- 7a99d1b: Upgrade Effect packages to npm `4.0.2`. - -## 2.1.0 - -### Minor Changes - -- 07dcd75: Add `effa sync` / `effa sync-diff` / `effa sync-push` subcommands for syncing content (architecture docs, e2e helpers, ts-plugins) from `effect-app/shared` into consuming projects per a project-side `.shared.json` lockfile. - - - `effa sync` — clone/checkout the shared repo at the pinned ref into `~/.cache/effa/shared/`, then copy the artifact map's files into the project (honoring `exclude`). - - `effa sync-diff` — sha256 compare each tracked file against the cache copy; reports `M` (modified locally), `D` (missing from project), `E` (excluded). - - `effa sync-push [--pr] [-m msg] [--branch name]` — branch in the cache off the pinned ref, copy modified project files in, commit, push. Optional `--pr` opens a PR via `gh pr create`. - - Lockfile shape: - - ```json - { - "repo": "github.com/effect-app/shared", - "ref": "", - "artifacts": { "": "" }, - "exclude": [""] - } - ``` - -- 97207c0: Add `sync-effect` CLI command to sync the Effect git subtree to the version pinned in package.json manifests. - -### Patch Changes - -- 1f9d7da: fix missing toCodecJson usages -- 32f05c8: Remove unused dependency declarations from package manifests. -- 6cfd83d: update effect to latest beta -- 8434b5f: preserve trailing newline when writing package.json files -- b9312df: Prevent subtree sync commands from importing upstream git tags. -- 68f1b39: Make `effa ue` update Effect and Effect App packages to the highest versions allowed by their existing semver ranges, including compatible prerelease updates. -- 505bfa9: Add concurrent decode helper APIs and migrate decode callsites to use them. - - - Add `withDefaultParseOptions` and keep `DefaultParseOptions` centralized. - - Export `decodeEffectConcurrently` and `decodeUnknownEffectConcurrently` from Schema and SchemaParser modules. - - Update repository, queue, client, form, and CLI decode paths to use concurrent decode helpers. - - Keep schema constructors free of hardcoded parse concurrency overrides. - -- 52b0b01: Beta25 -- 10b55ff: update packages -- 7b28548: Add `effa sync --update [--ref ]` to bump the pinned `ref` in `.shared.json` to the latest sha before syncing. Without `--ref` it resolves the shared repo's default branch HEAD; with `--ref` it resolves that ref (branches resolved against `origin/` so you get the latest remote commit). The resolved sha is written back to the lockfile. Omitting `--update` keeps the existing pin-and-sync behaviour. -- 18bae5b: Upgrade Effect packages to `4.0.0-rc.114` (core `effect` via pkg.pr.new at `cdb3ac91` until npm publishes that version). Also bump independently versioned `@effect/language-service` to `0.87.2` and `@effect/tsgo` to `0.45.0` (requires `oxlint` `1.82.0`). - - Breaking API updates from this RC: - - - 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` - - `SchemaTransformation.transformOrFail` renamed to `transformEffect` - - `Fiber.currentSpan` moved to `fiber.cache.span` - - HTTP server addresses are `InetAddressV4`/`InetAddressV6` instead of `TcpAddress` - - Object JSON Schema now emits `additionalProperties: true` - - Schema `Union` AST stores matching as `options.mode` instead of `mode` - -- e2c9ebd: Require an explicit `EFFA_GIST_CACHE_ID`, remove cache Gist discovery and automatic creation, and initialize missing company pointer files once. -- 55c6572: update packages -- c991be1: update packages -- 8c645d5: update to latest effect -- a37aa38: Update to effect beta 43 -- c1e73de: -- d867272: the return of `Context` -- 50ce7e6: Replace typescript-eslint with oxlint-tsgolint for type-aware lint. Drop ESLint entirely from non-vue packages (cli, effect-app, infra) — they now use only `oxlint --type-aware`. Vue packages keep ESLint to run `@effect-app/no-await-effect` (no tsgolint equivalent) via `@typescript-eslint/parser` + `vue-eslint-parser`. -- 52b0b01: Update Effect dependencies to 4.0.0-beta.31 across workspace packages. -- eb28ea5: bogus -- a354345: Move release tsconfig flattening from publish to pack lifecycle so package configs are restored before registry upload/auth can fail. -- 52b0b01: Update Effect dependencies to 4.0.0-beta.28 across workspace packages. -- d31253f: Refactor eligible schema classes and tagged classes to Opaque schemas, and migrate constructor call sites to use `.make` for those models. -- cec026d: update packages -- 8267e7d: update to latest effect -- 52b0b01: Update Effect dependencies to 4.0.0-beta.29 across workspace packages. -- a69da09: Publish from generated staging directories so release-only files are created outside the source package tree. -- b8b9080: update packages -- e7ad78c: Add `sync-effect-app` to sync vendored Effect App libs by pinned package version. - The command prefers package-version git tags and supports `--ref latest` for main. -- e7ad78c: Sync Effect subtrees from a repository URL without requiring a local git remote. -- 6138bc7: `effa sync-push`: propagate locally-deleted synced files. Previously, files removed from the consuming project were silently skipped (the loop bailed when `destAbs` didn't exist). Now they're recorded as deletions, `git rm`'d in the cache branch, and listed in the PR body alongside modifications. -- 52b0b01: update effect to 4.0.0-beta.37 and drop the Schema Class disableValidation workaround now that the patched effect schema covers it -- fac725d: update effect to latest beta -- 52b0b01: update all teh tings -- c3299f7: update packages -- 6b57330: Upgrade Effect packages to npm `4.0.0`. -- e2d00b5: Upgrade Effect packages to npm `4.0.1`. -- 547e2e1: Update effect packages to `4.0.0-beta.107` (from `beta.90`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`, and `fast-check` to `^4.9.0`. Sync `repos/effect` subtree from `Effect-TS/effect` (effect-smol stopped publishing tags after beta.98). - - API adaptations for beta.107: - - - `concurrency: "inherit"` → `"unbounded"` - - `Schema.ErrorClass` / `TaggedErrorClass` → `Schema.Error` / `TaggedError` - - `Schema.LazyArbitrary` → `Schema.Arbitrary` - - `Schema.DateValid` / `isDateValid` removed (`Schema.Date` rejects invalid dates) - - `SchemaIssue` constructors no longer take `Option` (annotations + input) - - filter meta via `annotations.representation` instead of `annotations.meta` - - `context.defaultValue` → `context.constructorDefault` (single Link) - - Class detection via `~constructor` + static `identifier` - - Redacted detection via `representation.id` - - localized StandardSchema hooks updated for new issue/input model - - provide `NodeCrypto.layer` for cluster sqlite tests - - default `sync-effect` subtree URL → `Effect-TS/effect` - -- 52b0b01: update effect to 4.0.0-beta.36, adapt to Option revert from A | undefined -- Update effect packages to 4.0.0-beta.52 -- 3e855bc: Update Effect packages to `4.0.0-beta.83` (from `beta.74`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/sql-sqlite-node`, `@effect/atom-vue`, `@effect/vitest`. - - Adapt the infra workflow engines to beta.83 API changes: - - - `Schema.Defect` is now a constructor function — use `S.Defect()` when building the deferred-exit codec (the bare constant no longer produces a usable schema and crashed `toType`). - - `Workflow` exposes its name as `_tag` instead of `name`. `WorkflowEngineSqlite`/`WorkflowEngineCosmos` now key the registry, codec caches, and persisted `workflow_name` off `workflow._tag`, fixing crash-recovery (stale-lease re-drive previously registered under an `undefined` key and never matched). - -- 78d076a: Update effect packages to `4.0.0-beta.84` (`effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`). -- ffd140f: Update effect packages to `4.0.0-beta.86` (from `beta.84`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. -- e8842aa: Update effect packages to `4.0.0-beta.88` (from `beta.86`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Also bump `@effect-app/cli` to `2.1.0-beta.35`. No source changes required — typecheck and tests pass unchanged. -- b6dda09: Update effect packages to `4.0.0-beta.90` (from `beta.88`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. -- 8bd5bfe: Update effect packages to `4.0.0-rc.112` (from `beta.107`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Sync `repos/effect` subtree from `Effect-TS/effect` at `effect@4.0.0-rc.112`. - - API adaptations for rc.112: - - - cluster encoded driver `resetAddress` → batched `resetAddresses` - - Cosmos `unprocessedMessages` honors optional `limit` / `addresses` (only claimed rows are returned) - - Service Bus `Runners.make` supplies `codecFor` for schema-aware RPC serialization - - `pnpm subtree:effect` passes `--url https://github.com/Effect-TS/effect.git` (published CLI still defaults to effect-smol) - - JSON Schema check constraints are compacted onto the parent (`minLength`/`maxLength` instead of `allOf`) - -- 2231ef8: Upgrade Effect packages to npm `4.0.0-rc.115`. Drop the pkg.pr.new pin used while `effect@4.0.0-rc.114` was unpublished. - -## 2.1.0-beta.45 - -### Patch Changes - -- e2d00b5: Upgrade Effect packages to npm `4.0.1`. - -## 2.1.0-beta.44 - -### Patch Changes - -- 6b57330: Upgrade Effect packages to npm `4.0.0`. - -## 2.1.0-beta.43 - -### Patch Changes - -- 2231ef8: Upgrade Effect packages to npm `4.0.0-rc.115`. Drop the pkg.pr.new pin used while `effect@4.0.0-rc.114` was unpublished. - -## 2.1.0-beta.42 - -### Patch Changes - -- 18bae5b: Upgrade Effect packages to `4.0.0-rc.114` (core `effect` via pkg.pr.new at `cdb3ac91` until npm publishes that version). Also bump independently versioned `@effect/language-service` to `0.87.2` and `@effect/tsgo` to `0.45.0` (requires `oxlint` `1.82.0`). - - Breaking API updates from this RC: - - - 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` - - `SchemaTransformation.transformOrFail` renamed to `transformEffect` - - `Fiber.currentSpan` moved to `fiber.cache.span` - - HTTP server addresses are `InetAddressV4`/`InetAddressV6` instead of `TcpAddress` - - Object JSON Schema now emits `additionalProperties: true` - - Schema `Union` AST stores matching as `options.mode` instead of `mode` - -## 2.1.0-beta.41 - -### Patch Changes - -- 8bd5bfe: Update effect packages to `4.0.0-rc.112` (from `beta.107`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Sync `repos/effect` subtree from `Effect-TS/effect` at `effect@4.0.0-rc.112`. - - API adaptations for rc.112: - - - cluster encoded driver `resetAddress` → batched `resetAddresses` - - Cosmos `unprocessedMessages` honors optional `limit` / `addresses` (only claimed rows are returned) - - Service Bus `Runners.make` supplies `codecFor` for schema-aware RPC serialization - - `pnpm subtree:effect` passes `--url https://github.com/Effect-TS/effect.git` (published CLI still defaults to effect-smol) - - JSON Schema check constraints are compacted onto the parent (`minLength`/`maxLength` instead of `allOf`) - -## 2.1.0-beta.40 - -### Patch Changes - -- 547e2e1: Update effect packages to `4.0.0-beta.107` (from `beta.90`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`, and `fast-check` to `^4.9.0`. Sync `repos/effect` subtree from `Effect-TS/effect` (effect-smol stopped publishing tags after beta.98). - - API adaptations for beta.107: - - - `concurrency: "inherit"` → `"unbounded"` - - `Schema.ErrorClass` / `TaggedErrorClass` → `Schema.Error` / `TaggedError` - - `Schema.LazyArbitrary` → `Schema.Arbitrary` - - `Schema.DateValid` / `isDateValid` removed (`Schema.Date` rejects invalid dates) - - `SchemaIssue` constructors no longer take `Option` (annotations + input) - - filter meta via `annotations.representation` instead of `annotations.meta` - - `context.defaultValue` → `context.constructorDefault` (single Link) - - Class detection via `~constructor` + static `identifier` - - Redacted detection via `representation.id` - - localized StandardSchema hooks updated for new issue/input model - - provide `NodeCrypto.layer` for cluster sqlite tests - - default `sync-effect` subtree URL → `Effect-TS/effect` - -## 2.1.0-beta.39 - -### Patch Changes - -- 68f1b39: Make `effa ue` update Effect and Effect App packages to the highest versions allowed by their existing semver ranges, including compatible prerelease updates. - -## 2.1.0-beta.38 - -### Patch Changes - -- e2c9ebd: Require an explicit `EFFA_GIST_CACHE_ID`, remove cache Gist discovery and automatic creation, and initialize missing company pointer files once. - -## 2.1.0-beta.37 - -### Patch Changes - -- b6dda09: Update effect packages to `4.0.0-beta.90` (from `beta.88`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. - -## 2.1.0-beta.36 - -### Patch Changes - -- e8842aa: Update effect packages to `4.0.0-beta.88` (from `beta.86`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Also bump `@effect-app/cli` to `2.1.0-beta.35`. No source changes required — typecheck and tests pass unchanged. - -## 2.1.0-beta.35 - -### Patch Changes - -- ffd140f: Update effect packages to `4.0.0-beta.86` (from `beta.84`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. - -## 2.1.0-beta.34 - -### Patch Changes - -- a354345: Move release tsconfig flattening from publish to pack lifecycle so package configs are restored before registry upload/auth can fail. -- a69da09: Publish from generated staging directories so release-only files are created outside the source package tree. - -## 2.1.0-beta.33 - -### Patch Changes - -- 32f05c8: Remove unused dependency declarations from package manifests. - -## 2.1.0-beta.32 - -### Patch Changes - -- b9312df: Prevent subtree sync commands from importing upstream git tags. - -## 2.1.0-beta.31 - -### Patch Changes - -- e7ad78c: Add `sync-effect-app` to sync vendored Effect App libs by pinned package version. - The command prefers package-version git tags and supports `--ref latest` for main. -- e7ad78c: Sync Effect subtrees from a repository URL without requiring a local git remote. - -## 2.1.0-beta.30 - -### Patch Changes - -- 78d076a: Update effect packages to `4.0.0-beta.84` (`effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`). - -## 2.1.0-beta.29 - -### Patch Changes - -- 3e855bc: Update Effect packages to `4.0.0-beta.83` (from `beta.74`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/sql-sqlite-node`, `@effect/atom-vue`, `@effect/vitest`. - - Adapt the infra workflow engines to beta.83 API changes: - - - `Schema.Defect` is now a constructor function — use `S.Defect()` when building the deferred-exit codec (the bare constant no longer produces a usable schema and crashed `toType`). - - `Workflow` exposes its name as `_tag` instead of `name`. `WorkflowEngineSqlite`/`WorkflowEngineCosmos` now key the registry, codec caches, and persisted `workflow_name` off `workflow._tag`, fixing crash-recovery (stale-lease re-drive previously registered under an `undefined` key and never matched). - -## 2.1.0-beta.28 - -### Patch Changes - -- 55c6572: update packages - -## 2.1.0-beta.27 - -### Patch Changes - -- 7b28548: Add `effa sync --update [--ref ]` to bump the pinned `ref` in `.shared.json` to the latest sha before syncing. Without `--ref` it resolves the shared repo's default branch HEAD; with `--ref` it resolves that ref (branches resolved against `origin/` so you get the latest remote commit). The resolved sha is written back to the lockfile. Omitting `--update` keeps the existing pin-and-sync behaviour. - -## 2.1.0-beta.26 - -### Patch Changes - -- bogus - -## 2.1.0-beta.25 - -### Patch Changes - -- 6138bc7: `effa sync-push`: propagate locally-deleted synced files. Previously, files removed from the consuming project were silently skipped (the loop bailed when `destAbs` didn't exist). Now they're recorded as deletions, `git rm`'d in the cache branch, and listed in the PR body alongside modifications. - -## 2.1.0-beta.24 - -### Minor Changes - -- 07dcd75: Add `effa sync` / `effa sync-diff` / `effa sync-push` subcommands for syncing content (architecture docs, e2e helpers, ts-plugins) from `effect-app/shared` into consuming projects per a project-side `.shared.json` lockfile. - - - `effa sync` — clone/checkout the shared repo at the pinned ref into `~/.cache/effa/shared/`, then copy the artifact map's files into the project (honoring `exclude`). - - `effa sync-diff` — sha256 compare each tracked file against the cache copy; reports `M` (modified locally), `D` (missing from project), `E` (excluded). - - `effa sync-push [--pr] [-m msg] [--branch name]` — branch in the cache off the pinned ref, copy modified project files in, commit, push. Optional `--pr` opens a PR via `gh pr create`. - - Lockfile shape: - - ```json - { - "repo": "github.com/effect-app/shared", - "ref": "", - "artifacts": { "": "" }, - "exclude": [""] - } - ``` - -## 2.1.0-beta.23 - -### Patch Changes - -- b8b9080: update packages - -## 2.1.0-beta.22 - -### Patch Changes - -- 6cfd83d: update effect to latest beta - -## 2.1.0-beta.21 - -### Patch Changes - -- c991be1: update packages - -## 2.1.0-beta.20 - -### Patch Changes - -- 50ce7e6: Replace typescript-eslint with oxlint-tsgolint for type-aware lint. Drop ESLint entirely from non-vue packages (cli, effect-app, infra) — they now use only `oxlint --type-aware`. Vue packages keep ESLint to run `@effect-app/no-await-effect` (no tsgolint equivalent) via `@typescript-eslint/parser` + `vue-eslint-parser`. - -## 2.1.0-beta.19 - -### Patch Changes - -- 505bfa9: Add concurrent decode helper APIs and migrate decode callsites to use them. - - - Add `withDefaultParseOptions` and keep `DefaultParseOptions` centralized. - - Export `decodeEffectConcurrently` and `decodeUnknownEffectConcurrently` from Schema and SchemaParser modules. - - Update repository, queue, client, form, and CLI decode paths to use concurrent decode helpers. - - Keep schema constructors free of hardcoded parse concurrency overrides. - -## 2.1.0-beta.18 - -### Patch Changes - -- c1e73de: - -## 2.1.0-beta.17 - -### Patch Changes - -- 8434b5f: preserve trailing newline when writing package.json files - -## 2.1.0-beta.16 - -### Patch Changes - -- d31253f: Refactor eligible schema classes and tagged classes to Opaque schemas, and migrate constructor call sites to use `.make` for those models. - -## 2.1.0-beta.15 - -### Patch Changes - -- c3299f7: update packages - -## 2.1.0-beta.14 - -### Patch Changes - -- Update effect packages to 4.0.0-beta.52 - -## 2.1.0-beta.13 - -### Minor Changes - -- 97207c0: Add `sync-effect` CLI command to sync the Effect git subtree to the version pinned in package.json manifests. - -## 2.0.1-beta.12 - -### Patch Changes - -- fac725d: update effect to latest beta - -## 2.0.1-beta.11 - -### Patch Changes - -- 8267e7d: update to latest effect - -## 2.0.1-beta.10 - -### Patch Changes - -- 1f9d7da: fix missing toCodecJson usages - -## 2.0.1-beta.9 - -### Patch Changes - -- cec026d: update packages - -## 2.0.1-beta.8 - -### Patch Changes - -- d867272: the return of `Context` - -## 2.0.1-beta.7 - -### Patch Changes - -- 10b55ff: update packages - -## 2.0.1-beta.6 - -### Patch Changes - -- a37aa38: Update to effect beta 43 - -## 2.0.1-beta.5 - -### Patch Changes - -- 8c645d5: update to latest effect - -## 2.0.1-beta.4 - -### Patch Changes - -- 601a1ff: update effect to 4.0.0-beta.37 and drop the Schema Class disableValidation workaround now that the patched effect schema covers it - -## 2.0.1-beta.3 - -### Patch Changes - -- 75c72ee: update effect to 4.0.0-beta.36, adapt to Option revert from A | undefined - -## 2.0.1-beta.2 - -### Patch Changes - -- 1c3c541: Update Effect dependencies to 4.0.0-beta.31 across workspace packages. -- de2359d: Update Effect dependencies to 4.0.0-beta.28 across workspace packages. -- 0d72f34: Update Effect dependencies to 4.0.0-beta.29 across workspace packages. - -## 2.0.1-beta.1 - -### Patch Changes - -- 01c70d0: update all teh tings - -## 2.0.1-beta.0 - -### Patch Changes - -- 64786af: Beta25 - -## 2.0.0 - -### Major Changes - -- 7e9e02b: Update to Effect v4 - -## 1.29.2 - -### Patch Changes - -- 5880020: bootstrap - -## 1.29.1 - -### Patch Changes - -- 5d043d3: propagate exit code - -## 1.29.0 - -### Minor Changes - -- 1a102e2: update packages - -## 1.28.1 - -### Patch Changes - -- 358782e: update packages - -## 1.28.0 - -### Minor Changes - -- 07f1de5: update packages and tsconfigs - -## 1.27.0 - -### Minor Changes - -- b8f5e30: update packages - -## 1.26.5 - -### Patch Changes - -- 7da965e: better multi-tenant handling (multi env, different caches) - -## 1.26.4 - -### Patch Changes - -- 36aba87: move gh presence check when logging in because of freakin' services initializations - -## 1.26.3 - -### Patch Changes - -- 25d353c: proper login - -## 1.26.2 - -### Patch Changes - -- 73288cc: move GH check - -## 1.26.1 - -### Patch Changes - -- d1a6901: check gh presence - -## 1.26.0 - -### Minor Changes - -- 2bcc323: add cli gist command to work with github gist files - -## 1.25.0 - -### Minor Changes - -- 6d8c512: update packages - -## 1.24.0 - -### Minor Changes - -- 3d49d81: update packages - -## 1.23.25 - -### Patch Changes - -- e31d54f: remove out of date wiki commands - -## 1.23.24 - -### Patch Changes - -- f35da3c: porting more to eslint shared config package - -## 1.23.23 - -### Patch Changes - -- 8096576: new eslint shared config package - -## 1.23.22 - -### Patch Changes - -- a3ac3bb: fix export (?) - -## 1.23.21 - -### Patch Changes - -- f9aee77: some fixes - -## 1.23.20 - -### Patch Changes - -- f6a3328: export base eslint files so that we import them in projects - -## 1.23.19 - -### Patch Changes - -- 539e2f2: minor wiki command enhancements - -## 1.23.18 - -### Patch Changes - -- 84be367: add command to update all but effect/effect-app packages - -## 1.23.17 - -### Patch Changes - -- d1dd883: other fml fixes - -## 1.23.16 - -### Patch Changes - -- ccbf803: fml - -## 1.23.15 - -### Patch Changes - -- 8994599: fix some wiki commands - -## 1.23.14 - -### Patch Changes - -- 2cad28a: some fixes to wiki commands - -## 1.23.13 - -### Patch Changes - -- d9280c7: fix init wiki - -## 1.23.12 - -### Patch Changes - -- 9ff7964: add wiki-init - -## 1.23.11 - -### Patch Changes - -- 7c2e428: pnpm effa wiki update - -## 1.23.10 - -### Patch Changes - -- 8133014: cli doc always fetch remote to not miss any update (like github wiki interface ones) - -## 1.23.9 - -### Patch Changes - -- 35f8488: fix sync cli command - -## 1.23.8 - -### Patch Changes - -- 5576081: cli command for interacting with wiki (v1) - -## 1.23.7 - -### Patch Changes - -- 8cb893c: dprint rules the world - -## 1.23.6 - -### Patch Changes - -- e81f093: remove watch add nuke - -## 1.23.5 - -### Patch Changes - -- de35a0f: add monitors for packagejson -- 8e1543e: add wrapping of child commands - -## 1.23.4 - -### Patch Changes - -- ca792d7: add some debug prints - -## 1.23.3 - -### Patch Changes - -- 62b36b0: fix monitors blocking next commands by forking them - -## 1.23.2 - -### Patch Changes - -- c12a49b: effect app cli to effect cli - -## 1.23.1 - -### Patch Changes - -- 20293b1: add ncu:effect and ncu:effect-app commands - -## 1.23.0 - -### Minor Changes - -- 0f2b2cf: update packages - -## 1.22.1 - -### Patch Changes - -- b095bb5: tscp - -## 1.22.0 - -### Minor Changes - -- 2c217ce: update packages - -## 1.21.1 - -### Patch Changes - -- b771967: update packages - -## 1.21.0 - -### Minor Changes - -- eac9c67: update packages - -## 1.20.1 - -### Patch Changes - -- 49d93a5: update packages - -## 1.20.0 - -### Minor Changes - -- b1d4af4: housekeeping: update packages - -## 1.19.0 - -### Minor Changes - -- 8cb33d5: update packages - -## 1.18.5 - -### Patch Changes - -- 1e1f32b: housekeeping: update packages - -## 1.18.4 - -### Patch Changes - -- update packages - -## 1.18.3 - -### Patch Changes - -- 0fcab38: update packages - -## 1.18.2 - -### Patch Changes - -- ae71bd9: add missing link for vue-componentsy - -## 1.18.1 - -### Patch Changes - -- fdb1107: filter dir - -## 1.18.0 - -### Minor Changes - -- 14ea804: housekeeping: update packages - -## 1.17.1 - -### Patch Changes - -- 4c8dfb4: update packages - -## 1.17.0 - -### Minor Changes - -- e42d5fb: update to effect 3.14 with native rpc2 - -## 1.16.2 - -### Patch Changes - -- 36996ed: housekeeping: update packages - -## 1.16.1 - -### Patch Changes - -- 63e966a: update packages - -## 1.16.0 - -### Minor Changes - -- 8be2984: housekeeping: update packages - -## 1.15.0 - -### Minor Changes - -- 1721761: improve: only update nearest controllers/routes.ts - -## 1.14.0 - -### Minor Changes - -- 9b2e385: update packages - -## 1.13.2 - -### Patch Changes - -- 9d11dfb: housekeeping: update packages - -## 1.13.1 - -### Patch Changes - -- ab9d1ea: housekeeping: update packages - -## 1.13.0 - -### Minor Changes - -- c8ebfb4: update packages - -## 1.12.0 - -### Minor Changes - -- 67023f8: kill \_cjs - -## 1.11.4 - -### Patch Changes - -- 9f504c6: update packages - -## 1.11.3 - -### Patch Changes - -- 750b46a: update packages - -## 1.11.2 - -### Patch Changes - -- 1bfc3d8: update packages - -## 1.11.1 - -### Patch Changes - -- 5ef6481: update packages - -## 1.11.0 - -### Minor Changes - -- 29a1a65: update packages - -## 1.10.0 - -### Minor Changes - -- d84aef7: update packages - -## 1.9.0 - -### Minor Changes - -- cc2c43e: update packages - -## 1.8.0 - -### Minor Changes - -- bump - -## 1.7.1 - -### Patch Changes - -- c971d73: update packages - -## 1.7.0 - -### Minor Changes - -- update - -## 1.6.0 - -### Minor Changes - -- f19762b: update packages - -## 1.5.1 - -### Patch Changes - -- 31c79d7: exit - -## 1.5.0 - -### Minor Changes - -- 4728346: add 'sync' command - -## 1.4.1 - -### Patch Changes - -- 412cb3d: update packages - -## 1.4.0 - -### Minor Changes - -- 590e5bf: update packages - -## 1.3.0 - -### Minor Changes - -- d4ab73f: update packages - -## 1.2.1 - -### Patch Changes - -- 19ea035: update packages - -## 1.2.0 - -### Minor Changes - -- d5623a6: update packages - -## 1.1.2 - -### Patch Changes - -- f68dbd2: update packages - -## 1.1.1 - -### Patch Changes - -- a66651b: m - -## 1.1.0 - -### Minor Changes - -- 964b0d2: refactor: merge adapters into infra - -## 1.0.0 - -### Major Changes - -- 13abe12: refactor: merge core into prelude - -## 0.90.0 - -### Minor Changes - -- d0471b1: refactor: merge @effect-app/schema into core - -## 0.89.0 - -### Minor Changes - -- 3872061: migrate to effect/Schema - -## 0.88.0 - -### Minor Changes - -- 480f0ef: add new controller setup support - -## 0.87.1 - -### Patch Changes - -- cda1477: update packages - -## 0.87.0 - -### Minor Changes - -- update packages - -## 0.86.0 - -### Minor Changes - -- c56949e: drop fluent extensions - -## 0.85.0 - -### Minor Changes - -- b9b64b8: housekeeping: update packages - -## 0.84.0 - -### Minor Changes - -- 65dcc73: update packages - -## 0.83.0 - -### Minor Changes - -- 4d2b804: housekeeping: update packages - -## 0.82.0 - -### Minor Changes - -- 190b139: update packages - -## 0.81.1 - -### Patch Changes - -- c710953: update packagesy - -## 0.81.0 - -### Minor Changes - -- 1018e58: update packages - -## 0.80.1 - -### Patch Changes - -- 11982eb: fix cli - -## 0.80.0 - -### Minor Changes - -- c5f5223: update packages - -## 0.79.3 - -### Patch Changes - -- e6eecd2: update packages - -## 0.79.2 - -### Patch Changes - -- ff - -## 0.79.1 - -### Patch Changes - -- fix - -## 0.79.0 - -### Minor Changes - -- 2143dd9: update packages - -## 0.78.1 - -### Patch Changes - -- f6066ac: update packages - -## 0.78.0 - -### Minor Changes - -- 79f58af: update packages - -## 0.77.2 - -### Patch Changes - -- b3ebc41: update packages - -## 0.77.1 - -### Patch Changes - -- f569681: update packages - -## 0.77.0 - -### Minor Changes - -- 613c148: update packages - -## 0.76.0 - -### Minor Changes - -- 8d0c511: update packages - -## 0.75.0 - -### Minor Changes - -- 9cc6f86: update packages - -## 0.74.1 - -### Patch Changes - -- 2c661c1: update packages - -## 0.74.0 - -### Minor Changes - -- 9ac9416: update packages - -## 0.73.2 - -### Patch Changes - -- 3411e78: update packages - -## 0.73.1 - -### Patch Changes - -- 128f636: update packages - -## 0.73.0 - -### Minor Changes - -- 2e29520: update packages - -## 0.72.1 - -### Patch Changes - -- c4dbb6c: update packages - -## 0.72.0 - -### Minor Changes - -- 0d1b378: update to latest schema - -## 0.71.0 - -### Minor Changes - -- 290a709: update packages - -## 0.70.0 - -### Minor Changes - -- ecb05f6: update packages - -## 0.69.0 - -### Minor Changes - -- revert form changes - -## 0.68.0 - -### Minor Changes - -- a9a81a1: cleanup ext - -## 0.67.2 - -### Patch Changes - -- fix - -## 0.67.1 - -### Patch Changes - -- 8dc2b62: improve link - -## 0.67.0 - -### Minor Changes - -- 7a3f05c: update packages - -## 0.66.0 - -### Minor Changes - -- bbaa558: bs - -## 0.65.0 - -### Minor Changes - -- e5b0d8e: update packages -- 2e2fa98: update packages - -## 0.64.0 - -### Minor Changes - -- 494842c: update packages - -## 0.63.1 - -### Patch Changes - -- 0334285: update packages - -## 0.63.0 - -### Minor Changes - -- 51253a2: update packages - -## 0.62.0 - -### Minor Changes - -- update packages - -## 0.61.0 - -### Minor Changes - -- d0a1774: update ts to 5.4.1-rc - -## 0.60.0 - -### Minor Changes - -- 7346691: update packages - -## 0.59.0 - -### Minor Changes - -- e8e8ee2: update packages - -## 0.58.0 - -### Minor Changes - -- 117be64: bump - -## 0.57.0 - -### Minor Changes - -- A new day - -## 0.56.0 - -### Minor Changes - -- c070e0d: update effect - -## 0.55.1 - -### Patch Changes - -- b7daa54: improve supported commands prompt - -## 0.55.0 - -### Minor Changes - -- db67c43: update bins - -## 0.54.0 - -### Minor Changes - -- 57fcfef: update packages - -## 0.53.2 - -### Patch Changes - -- 1168eb4: add link/unlink - -## 0.53.1 - -### Patch Changes - -- c550140: fix - -## 0.53.0 - -### Minor Changes - -- 25123dc: improve cli frontend - -## 0.52.0 - -### Minor Changes - -- 6e5c606: cl - -## 0.51.0 - -### Minor Changes - -- de336f6: src - -## 0.50.1 - -### Patch Changes - -- f10e630: update packages - -## 0.50.0 - -### Minor Changes - -- 47f46f0: use actual action name casing so that editor go to works. - -## 0.49.1 - -### Patch Changes - -- 7b40fa7: use full path - -## 0.49.0 - -### Minor Changes - -- m - -## 0.48.0 - -### Minor Changes - -- bs - -## 0.47.0 - -### Minor Changes - -- c8a07bc: fix path - -## 0.46.0 - -### Minor Changes - -- e5cdd42: update packages - -## 0.45.0 - -### Minor Changes - -- update to latest Effect "minor" - -## 0.44.0 - -### Minor Changes - -- c29b6ff: bump - -## 0.43.0 - -### Minor Changes - -- bump - -## 0.42.1 - -### Patch Changes - -- 0a898f8: update packages - -## 0.42.0 - -### Minor Changes - -- 449b343: update packages - -## 0.41.0 - -### Minor Changes - -- 2fd03d3: update packages - -## 0.40.0 - -### Minor Changes - -- a30db88: update packages - -## 0.39.0 - -### Minor Changes - -- 3eb4057: update packages - -## 0.38.0 - -### Minor Changes - -- 4c915ea: update packages - -## 0.37.0 - -### Minor Changes - -- a894a63: update packages - -## 0.36.0 - -### Minor Changes - -- 7945f38: update packages - -## 0.35.0 - -### Minor Changes - -- 63425a7: update packages - -## 0.34.0 - -### Minor Changes - -- 82f0f0d: update packages - -## 0.33.1 - -### Patch Changes - -- cf6ae3c: update packages - -## 0.33.0 - -### Minor Changes - -- 46bd7be: update packages - -## 0.32.0 - -### Minor Changes - -- a681ab6: update packages - -## 0.31.0 - -### Minor Changes - -- a9e8b8f: update packages - -## 0.30.0 - -### Minor Changes - -- align v - -## 0.29.0 - -### Minor Changes - -- align v - -## 0.28.0 - -### Minor Changes - -- 1f6b9a1: update packages - -## 0.27.0 - -### Minor Changes - -- c528c33: initial alignment of Schema with effect/schema - -## 0.26.0 - -### Minor Changes - -- 6c180253: cleanup - -## 0.25.1 - -### Patch Changes - -- 7ff1f6f9: update packages - -## 0.25.0 - -### Minor Changes - -- 0a4bfbc7: update packages - -## 0.24.0 - -### Minor Changes - -- 90af6a41: update packages - -## 0.23.1 - -### Patch Changes - -- aa3c98d1: update packages - -## 0.23.0 - -### Minor Changes - -- update packages - -## 0.22.1 - -### Patch Changes - -- 780a6718: update packages - -## 0.22.0 - -### Minor Changes - -- housekeeping: update packages - -## 0.21.2 - -### Patch Changes - -- 838eeb10: update packages - -## 0.21.1 - -### Patch Changes - -- fix tsplus.config for pnpm - -## 0.21.0 - -### Minor Changes - -- update packages - -## 0.20.0 - -### Minor Changes - -- add match and update packages - -## 0.19.0 - -### Minor Changes - -- update packages - -## 0.18.0 - -### Minor Changes - -- housekeeping: update packages - -## 0.17.0 - -### Minor Changes - -- c80b9e7a: Updated to latest Effect - -## 0.16.0 - -### Minor Changes - -- housekeeping: update packages - -## 0.15.0 - -### Minor Changes - -- Update packages - -## 0.14.0 - -### Minor Changes - -- Update depdendencies - -## 0.13.0 - -### Minor Changes - -- update dependencies - -## 0.12.0 - -### Minor Changes - -- Update dependencies - -## 0.11.0 - -### Minor Changes - -- Update packages - -## 0.10.0 - -### Minor Changes - -- update dependencies - -## 0.9.0 - -### Minor Changes - -- Update packages - -## 0.8.0 - -### Minor Changes - -- Update to latest effect - -## 0.7.0 - -### Minor Changes - -- Update packages - -## 0.6.0 - -### Minor Changes - -- update packages - -## 0.5.0 - -### Minor Changes - -- Update tsplus with companion instance extensions - -## 0.4.0 - -### Minor Changes - -- Update Effect - -## 0.3.1 - -### Patch Changes - -- 02997e8f: FUP - -## 0.3.0 - -### Minor Changes - -- 4d49f303: Upgrade to @effect/data - -## 0.2.7 - -### Patch Changes - -- 82a54d23: Cleanup - -## 0.2.6 - -### Patch Changes - -- 89ee2cc5: Add root index.ts support - -## 0.2.5 - -### Patch Changes - -- 9ae82030: fix index - -## 0.2.4 - -### Patch Changes - -- fe896f8c: Fix m - -## 0.2.3 - -### Patch Changes - -- 5a37df3f: Improve cli - -## 0.2.2 - -### Patch Changes - -- 1cd7bbd9: write package.json at start - -## 0.2.1 - -### Patch Changes - -- 847c1eb0: Improve cli dirs - -## 0.2.0 - -### Minor Changes - -- Update annotations and leverage ForceLazy on Effect \_\_call - -## 0.1.6 - -### Patch Changes - -- 912ba66c: Update packages - -## 0.1.5 - -### Patch Changes - -- 2cc4390c: Fix cli - -## 0.1.4 - -### Patch Changes - -- dccdfe2a: cli: add packagejson levels - -## 0.1.3 - -### Patch Changes - -- 2946e475: Fix - -## 0.1.2 - -### Patch Changes - -- 5fd3e5f5: Add package json exports support - -## 0.1.1 - -### Patch Changes - -- f8d2dea7: Add index commands - -## 0.1.0 - -### Minor Changes - -- 9150d615: Prepare CLI for multi-command - -## 0.0.2 - -### Patch Changes - -- Initial release diff --git a/packages/cli/bin.js b/packages/cli/bin.js deleted file mode 100755 index c37c4113d..000000000 --- a/packages/cli/bin.js +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import "./src/index.ts" diff --git a/packages/cli/package.json b/packages/cli/package.json deleted file mode 100644 index 3c65802e7..000000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "name": "@effect-app/cli", - "version": "2.1.1", - "license": "MIT", - "type": "module", - "repository": { - "type": "git", - "url": "https://github.com/effect-app/libs.git", - "directory": "packages/cli" - }, - "bin": { - "effect-app": "./bin.js", - "effa": "./bin.js", - "effect-app-cli": "./bin.js" - }, - "dependencies": { - "@effect/platform-node": "4.0.5", - "effect": "4.0.5", - "js-yaml": "4.2.0" - }, - "devDependencies": { - "@types/js-yaml": "^4.0.9", - "@types/node": "25.9.1", - "typescript": "npm:@typescript/native-preview@beta", - "vitest": "^5.0.5" - }, - "typesVersions": { - "*": { - "*": [ - "src/*" - ] - } - }, - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - }, - "./argv-patch": { - "types": "./src/argv-patch.ts", - "default": "./src/argv-patch.ts" - }, - "./extract": { - "types": "./src/extract.ts", - "default": "./src/extract.ts" - }, - "./gist": { - "types": "./src/gist.ts", - "default": "./src/gist.ts" - }, - "./os-command": { - "types": "./src/os-command.ts", - "default": "./src/os-command.ts" - }, - "./shared": { - "types": "./src/shared.ts", - "default": "./src/shared.ts" - }, - "./sync-effect-subtree": { - "types": "./src/sync-effect-subtree.ts", - "default": "./src/sync-effect-subtree.ts" - }, - "./sync-effect-app-subtree": { - "types": "./src/sync-effect-app-subtree.ts", - "default": "./src/sync-effect-app-subtree.ts" - }, - "./sync-shared": { - "types": "./src/sync-shared.ts", - "default": "./src/sync-shared.ts" - } - }, - "scripts": { - "watch": "pnpm build:tsc -w", - "build:tsc": "pnpm clean-dist && pnpm check", - "check": "tsgo --build", - "build": "pnpm build:tsc", - "watch2": "pnpm clean-dist && NODE_OPTIONS=--max-old-space-size=6144 tsgo -w", - "clean": "rm -rf dist", - "clean-dist": "sh ../../scripts/clean-dist.sh", - "circular": "pnpm circular:src && pnpm circular:dist", - "circular:src": "madge --circular --ts-config ./tsconfig.json --extensions ts ./src", - "circular:dist": "madge --circular --extensions js ./dist", - "compile": "NODE_OPTIONS=--max-old-space-size=6144 tsgo --noEmit", - "lint": "oxlint --quiet --type-aware ./src && pnpm exec dprint check --config ../../dprint.jsonc .", - "lint-fix": "oxlint --quiet --type-aware --fix ./src && pnpm exec dprint fmt --config ../../dprint.jsonc .", - "test": "vitest", - "test:run": "pnpm run test run --passWithNoTests", - "testsuite": "pnpm lint && pnpm circular && pnpm run test:run", - "ncu": "ncu", - "pub": "pnpm publish --access public", - "prepack": "node ../../scripts/stagePublishPackage.mjs prepack", - "postpack": "node ../../scripts/stagePublishPackage.mjs postpack", - "postpublish": "node ../../scripts/stagePublishPackage.mjs postpublish" - }, - "gitHead": "bd8e27eea3eff97db8739d577d67e7336c078d28", - "sideEffects": [ - "./dist/faker.js", - "./faker", - "./src/faker", - "./dist/faker", - "./dist/src/faker.js", - "./dist/faker.js" - ], - "publishConfig": { - "directory": ".publish", - "linkDirectory": false, - "bin": { - "effect-app": "./dist/index.js", - "effa": "./dist/index.js", - "effect-app-cli": "./dist/index.js" - }, - "typesVersions": { - "*": { - "*": [ - "dist/*" - ] - } - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./argv-patch": { - "types": "./dist/argv-patch.d.ts", - "default": "./dist/argv-patch.js" - }, - "./extract": { - "types": "./dist/extract.d.ts", - "default": "./dist/extract.js" - }, - "./gist": { - "types": "./dist/gist.d.ts", - "default": "./dist/gist.js" - }, - "./os-command": { - "types": "./dist/os-command.d.ts", - "default": "./dist/os-command.js" - }, - "./shared": { - "types": "./dist/shared.d.ts", - "default": "./dist/shared.js" - }, - "./sync-effect-subtree": { - "types": "./dist/sync-effect-subtree.d.ts", - "default": "./dist/sync-effect-subtree.js" - }, - "./sync-effect-app-subtree": { - "types": "./dist/sync-effect-app-subtree.d.ts", - "default": "./dist/sync-effect-app-subtree.js" - }, - "./sync-shared": { - "types": "./dist/sync-shared.d.ts", - "default": "./dist/sync-shared.js" - } - } - } -} diff --git a/packages/cli/src/argv-patch.ts b/packages/cli/src/argv-patch.ts deleted file mode 100644 index 242c56132..000000000 --- a/packages/cli/src/argv-patch.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Join wrap args into a single argv element so that -// `effect-app-cli index-multi tsc --build` works without quoting. -// The new effect/cli lexer classifies --flags as LongOption tokens -// and discards unrecognized ones at the subcommand level. -// By joining all args after the subcommand into one string, the lexer -// sees a single Value token instead of separate LongOption tokens. - -const wrapCommands = new Set(["index-multi", "packagejson", "packagejson-packages"]) - -export const patchArgvForWrapCommands = (argv: Array): void => { - const subIdx = argv.findIndex((a, i) => i >= 2 && wrapCommands.has(a)) - if (subIdx === -1 || subIdx + 1 >= argv.length) return - - const wrapArgs = argv.splice(subIdx + 1) - argv.push(wrapArgs.join(" ")) -} diff --git a/packages/cli/src/extract.ts b/packages/cli/src/extract.ts deleted file mode 100644 index ecc940ae2..000000000 --- a/packages/cli/src/extract.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as EffectArray from "effect/Array" -import * as Effect from "effect/Effect" -import * as FileSystem from "effect/FileSystem" -import { pipe } from "effect/Function" -import * as Order from "effect/Order" -import * as Path from "effect/Path" -import type * as PlatformError from "effect/PlatformError" - -/** - * Generates package.json exports mappings for TypeScript modules - * This function finds all .ts files in src/ (excluding test files) and generates - * JSON export entries that map source files to their compiled .js and .d.ts outputs - * - * Example output: - * "./utils/helper": { "types": "./dist/utils/helper.d.ts", "default": "./dist/utils/helper.js" }, - * - * This allows users to import individual modules instead of the entire package: - * import { helper } from 'package/utils/helper' instead of 'package' - */ -export const ExtractExportMappingsService = Effect.fn("effa-cli.extractExportMappings")(function*() { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - - return Effect.fn("effa-cli.extractExportMappings.service")(function*(cwd: string) { - // @effect-diagnostics-next-line effectFnOpportunity:off - const findTsFiles = (dir: string): Effect.Effect => - Effect.gen(function*() { - const entries = yield* fs.readDirectory(dir) - - const results = yield* Effect.all( - entries.map(Effect.fnUntraced(function*(entry) { - const fullPath = path.join(dir, entry) - const stat = yield* fs.stat(fullPath) - - if (stat.type === "Directory") { - return yield* findTsFiles(fullPath) - } else if (entry.endsWith(".ts") && !entry.includes(".test.")) { - return [fullPath] - } - return [] - })) - ) - - return EffectArray.flatten(results) - }) - - const srcDir = path.join(cwd, "src") - - // Check if src directory exists - const srcExists = yield* fs.exists(srcDir) - if (!srcExists) { - return "" - } - - const tsFiles = yield* findTsFiles(srcDir) - - const exportMappings = tsFiles.map((filePath) => { - // Get relative path from src directory (like bash script's cut -c 5-) - const relativePath = path.relative(srcDir, filePath) - const exportKey = `./${relativePath.replace(/\.ts$/, "")}` - const distPath = `./dist/${relativePath.replace(/\.ts$/, ".js")}` - const typesPath = `./dist/${relativePath.replace(/\.ts$/, ".d.ts")}` - - return `"${exportKey}": { "types": "${typesPath}", "default": "${distPath}" }` - }) - - const sortedMappings = pipe( - exportMappings, - EffectArray.sort(Order.String), - EffectArray.join(",\n") - ) - - return sortedMappings - }) -})() diff --git a/packages/cli/src/gist.ts b/packages/cli/src/gist.ts deleted file mode 100644 index f1c5ca7bb..000000000 --- a/packages/cli/src/gist.ts +++ /dev/null @@ -1,748 +0,0 @@ -/* eslint-disable no-constant-binary-expression */ -/* eslint-disable no-empty-pattern */ -// import necessary modules from the libraries -import * as Array from "effect/Array" -import * as Config from "effect/Config" -import * as Context from "effect/Context" -import * as Data from "effect/Data" -import * as Effect from "effect/Effect" -import * as FileSystem from "effect/FileSystem" -import { pipe } from "effect/Function" -import * as Layer from "effect/Layer" -import * as Option from "effect/Option" -import * as Path from "effect/Path" -import * as Redacted from "effect/Redacted" -import * as Result from "effect/Result" -import * as Schema from "effect/Schema" -import * as SchemaIssue from "effect/SchemaIssue" -import * as SchemaTransformation from "effect/SchemaTransformation" -import * as SynchronizedRef from "effect/SynchronizedRef" - -import * as yaml from "js-yaml" -import path from "path" -import { RunCommandService } from "./os-command.ts" - -export class GistError extends Data.TaggedError("GistError")<{ message: string }> {} - -// -// -// Schemas -// - -export class GistEntry extends Schema.Opaque()(Schema.Struct({ - description: Schema.String, - public: Schema.Boolean, - company: Schema.String, - files: Schema.Array(Schema.String) -})) {} - -/** - * Extended gist entry that validates file_name uniqueness and extracts base filenames. - * - * GitHub Gists have a flat file structure and do not support directories/folders. - * All files within a gist must exist in the same namespace, meaning that files - * with identical names will collide, even if they originate from different local - * directories. When multiple files share the same basename, GitHub will either: - * - Reject the gist creation - * - Silently overwrite files (last one wins) - * - Display unpredictable behavior - * - * This validation prevents such collisions by detecting when multiple file paths - * would result in the same file_name when flattened to the gist structure. - * - * @example - * // These paths would collide in a gist - * ["src/config.json", "dist/config.json"] // Both become "config.json" - * - * @see {@link https://docs.github.com/articles/creating-gists | GitHub Gist Documentation} - * @see {@link https://github.com/orgs/community/discussions/29584 | Community Discussion on Gist Folder Support} - */ -export class GistEntryDecoded extends Schema.Opaque()( - GistEntry.pipe( - Schema.decodeTo( - Schema.Struct({ - description: Schema.String, - public: Schema.Boolean, - company: Schema.String, - files: Schema.Array(Schema.String), - files_with_name: Schema.Array(Schema.Struct({ - path: Schema.String, - name: Schema.String - })) - }), - SchemaTransformation.transformEffect({ - decode: Effect.fnUntraced(function*(entry) { - const files_with_name = entry.files.map((file) => ({ - path: file, - name: path.basename(file) // <-- I'm using Node's path module here so that this schema works without requirements on Effect's Path module - })) - - // check for duplicate file names - const nameMap = new Map() - for (const { name, path: filePath } of files_with_name) { - if (!nameMap.has(name)) { - nameMap.set(name, []) - } - nameMap.get(name)!.push(filePath) - } - - // find duplicates and collect all collision messages - const messages: string[] = [] - for (const [fileName, paths] of nameMap.entries()) { - if (paths.length > 1) { - messages.push( - `Duplicate file name detected: "${fileName}". Colliding paths: ${paths.join(", ")}` - ) - } - } - - // if there are any collisions, fail with a combined message - if (messages.length > 0) { - return yield* Effect.fail( - new SchemaIssue.InvalidValue({ message: messages.join("; ") }, entry.files) - ) - } - - return yield* Effect.succeed({ ...entry, files_with_name }) - }), - encode: ({ files_with_name: _, ...entry }) => Effect.succeed(entry) - }) - ) - ) -) {} - -export class GistYAML extends Schema.Opaque()(Schema.Struct({ - gists: Schema.optional(Schema.NullOr( - Schema.Record(Schema.String, GistEntryDecoded) - )), - settings: Schema.Struct({ - token_env: Schema.String, - base_directory: Schema.String - }) -})) {} - -/** - * Cache entry representing a gist mapping. - * Each entry contains the gist's human-readable name and GitHub ID. - */ -export class GistCacheEntry extends Schema.Opaque()(Schema.Struct({ - name: Schema.String, - id: Schema.String -})) {} - -export const GistCacheEntries = Schema.Array(GistCacheEntry) -export interface GistCacheEntries extends Schema.Schema.Type {} - -/** - * Gist cache mapping YAML configuration names to GitHub gist IDs with company awareness. - * - * Since GitHub gists don't have user-defined names, we maintain a cache - * that maps the human-readable names from our YAML config to actual gist IDs. - * Each cache entry is associated with a company, enabling multi-tenant operations. - * This allows us to: - * - Update existing gists instead of creating duplicates - * - Clean up obsolete entries when gists are removed from config - * - Persist the name->ID mapping across CLI runs - * - Isolate gist operations by company context - * - * The cache itself is stored as a secret GitHub gist for persistence. - */ -export class GistCache { - entries: GistCacheEntries - gist_id: string - - company: string - - constructor({ company, entries, gist_id }: { entries: GistCacheEntries; gist_id: string; company: string }) { - this.entries = entries - this.gist_id = gist_id - this.company = company - } -} - -// -// -// Errors -// -class GistYAMLError extends Data.TaggedError("GistYAMLError")<{ - readonly message: string -}> {} - -// -// -// Services -// - -export class GHGistService extends Context.Service()("GHGistService", { - make: Effect.gen(function*() { - const { runGetExitCode, runGetString } = yield* RunCommandService - - // the client cannot recover from PlatformErrors, so we convert failures into defects to clean up the signatures - const runGetExitCodeSuppressed = (...args: Parameters) => { - return runGetExitCode(...args).pipe( - Effect.mapError((e) => `Command failed: ${args.join(" ")}\nError: ${e.message}`), - Effect.orDie, - Effect.asVoid - ) - } - - // the client cannot recover from PlatformErrors, so we convert failures into defects to clean up the signatures - const runGetStringSuppressed = (...args: Parameters) => { - return runGetString(...args).pipe( - Effect.mapError((e) => `Command failed: ${args.join(" ")}\nError: ${e.message}`), - Effect.orDie - ) - } - - /** - * Extracts the Gist ID from a given GitHub Gist URL: https://gist.github.com/user/ID - * @param url - The full URL of the GitHub Gist. - * @returns An Option containing the Gist ID if extraction is successful, otherwise None. - */ - function extractGistIdFromUrl(url: string) { - const gist_id = url.trim().split("/").pop() - return gist_id && gist_id.length > 0 ? Option.some(gist_id) : Option.none() - } - - const loadGistCache = Effect.fn("effa-cli.gist.loadGistCache")( - function*(cacheGistId: string, company: string) { - const filesInCache = yield* runGetStringSuppressed(`gh gist view ${cacheGistId} --files`).pipe( - Effect.map((files) => - files - .trim() - .split("\n") - .map((f) => f.trim()) - ) - ) - - if (!filesInCache.includes(`${company}.json`)) { - yield* Effect.logInfo(`Cache for company ${company} not found, creating company-specific cache file...`) - yield* runGetStringSuppressed( - `echo "[]" | gh gist edit ${cacheGistId} -a ${company}.json -` - ) - return new GistCache({ entries: [], gist_id: cacheGistId, company }) - } - - const cacheContent = yield* runGetStringSuppressed(`gh gist view ${cacheGistId} -f "${company}.json"`) - - const entries = yield* pipe( - cacheContent, - Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(GistCacheEntries))), - Effect.orDie - ) - - return new GistCache({ entries, gist_id: cacheGistId, company }) - } - ) - - const saveGistCache = Effect.fn("effa-cli.gist.saveGistCache")( - function*(cache: GistCache) { - const cacheJson = yield* pipe( - cache.entries, - Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(GistCacheEntries))), - // cannot recover from parse errors in any case, better to die here instead of cluttering the signature - Effect.orDie - ) - - yield* runGetExitCodeSuppressed( - `echo '${cacheJson}' | gh gist edit ${cache.gist_id} -f ${cache.company}.json -` - ) - } - ) - - const createGistWithFiles = Effect.fn("GHGistService.createGistWithFiles")( - function*({ description, env, files, gist_name, is_public }: { - gist_name: string - description: string - files: { - path: string - name: string - }[] - is_public: boolean - env: string - }) { - yield* Effect.logInfo(`Creating gist ${gist_name} with ${files.length} file(s)`) - - const ghCommand = [ - "gh", - "gist", - "create", - `--desc="${description}"`, - is_public ? "--public" : "", - ...files.map((file) => `"${file.path}"`) - ] - .filter((x) => !!x) - .join(" ") - - // create and capture the created gist URL - const gistUrl = yield* runGetStringSuppressed(ghCommand) - - // extract ID from URL - const gistNameId = yield* pipe( - gistUrl, - extractGistIdFromUrl, - Option.match({ - onNone: () => Effect.die(`Failed to extract gist ID from URL: ${gistUrl}`), - onSome: (id) => - Effect - .succeed( - { name: gist_name, id } - ) - .pipe(Effect.tap(Effect.logInfo(`Created gist with ID ${id}`))) - }) - ) - - // rename all files to include environment prefix for multi-environment support - for (const file of files) { - const originalName = file.name - const name_with_env = `${env}.${originalName}` - const ghRenameCommand = [ - "gh", - "gist", - "rename", - gistNameId.id, - originalName, - name_with_env - ] - .join(" ") - - yield* Effect.logInfo(`Renaming file ${originalName} to ${name_with_env} in gist ${gist_name}`) - yield* runGetStringSuppressed(ghRenameCommand) - } - - return gistNameId - } - ) - - const getGistFileNames = Effect.fn("getGistFileNames")( - function*({ env, gist_id, gist_name }: { - gist_id: string - gist_name: string - env: string - }) { - yield* Effect.logInfo(`Retrieving file names from gist ${gist_name} with ID ${gist_id}`) - const output = yield* runGetStringSuppressed(`gh gist view ${gist_id} --files`) - - // filter file names by environment prefix and remove the prefix - // files in gists are prefixed with "env." to support multiple environments - return Array.filterMap(output.trim().split("\n"), (fn) => { - const fnTrimmed = fn.trim() - if (!fnTrimmed.startsWith(env + ".")) return Result.fail(fn) - return Result.succeed(fnTrimmed.substring(env.length + 1)) // remove env prefix and dot - }) - } - ) - - const removeFileFromGist = Effect.fn("removeFileFromGist")( - function*({ env, file_name, gist_id, gist_name }: { - gist_id: string - gist_name: string - file_name: string - env: string - }) { - const name_with_env = `${env}.${file_name}` - yield* Effect.logInfo(`Removing file ${name_with_env} from gist ${gist_name}`) - return yield* runGetExitCodeSuppressed(`gh gist edit ${gist_id} --remove "${name_with_env}"`) - } - ) - - const updateFileOfGist = Effect.fn("updateFileOfGist")( - function*({ env, file_name, file_path, gist_id, gist_name }: { - gist_id: string - gist_name: string - file_name: string - file_path: string - env: string - }) { - const name_with_env = `${env}.${file_name}` - yield* Effect.logInfo(`Updating file ${name_with_env} located at ${file_path} of gist ${gist_name}`) - - // it seems this does not require renaming the file - const editCommand = [ - "gh", - "gist", - "edit", - gist_id, - "-f", - name_with_env, - `"${file_path}"` - ] - .join(" ") - - return yield* runGetExitCodeSuppressed(editCommand) - } - ) - - const addFileToGist = Effect.fn("addFileToGist")( - function*({ env, file, gist_id, gist_name }: { - gist_id: string - gist_name: string - file: { - path: string - name: string - } - env: string - }) { - yield* Effect.logInfo(`Adding file ${file.path} to gist ${gist_name}`) - const editCommand = [ - "gh", - "gist", - "edit", - gist_id, - "-a", - `"${file.path}"` - ] - .join(" ") - - yield* runGetExitCodeSuppressed(editCommand) - - const renameCommand = [ - "gh", - "gist", - "rename", - gist_id, - file.name, - `${env}.${file.name}` - ] - .join(" ") - - yield* Effect.logInfo(`Renaming file ${file.name} to ${env}.${file.name} in gist ${gist_name}`) - return yield* runGetExitCodeSuppressed(renameCommand) - } - ) - - const deleteGist = Effect.fn("deleteGist")( - function*({ gist_id, gist_name }: { gist_id: string; gist_name: string }) { - yield* Effect.logInfo(`Deleting gist ${gist_name} with ID ${gist_id}`) - return yield* runGetExitCodeSuppressed(`gh gist delete ${gist_id}`) - } - ) - - const login = Effect.fn("GHGistService.login")(function*(token: string) { - if ((yield* runGetExitCode("gh --version").pipe(Effect.orDie)) !== 0) { - return yield* Effect.die( - "GitHub CLI (gh) is not installed or not found in PATH. Please install it to use the gist command." - ) - } - - const isLogged = yield* runGetExitCode(`echo ${token} | gh auth login --with-token`).pipe(Effect.orDie) - if (isLogged !== 0) { - return yield* new GistError({ message: "Failed to log in to GitHub CLI with provided token" }) - } else { - yield* Effect.logInfo("Successfully logged in to GitHub CLI") - } - }) - - return { - /** Logs into GitHub using the GitHub CLI. - * This is a prerequisite for other gist operations. - * @param token - The GitHub personal access token with gist permissions - * - * @returns An Effect that succeeds when login is successful - */ - login, - - /** - * Loads company mappings from the configured cache Gist. - * A missing company file is initialized once with an empty mapping. - * - * @returns An Effect that yields the loaded company cache - */ - loadGistCache, - - /** - * Saves the current gist cache state to the GitHub cache gist. - * Updates the existing cache gist with the current mappings of names to gist IDs. - * - * @param cache - The GistCache instance to save - * @returns An Effect that succeeds when the cache is successfully saved - */ - saveGistCache, - - /** - * Creates a new GitHub gist with the specified files and renames them with environment prefixes. - * Generates a GitHub CLI command to create the gist, extracts the resulting gist ID, - * and renames all files with environment prefixes for multi-environment support. - * - * @param gist_name - The human-readable name for this gist (used in cache mapping) - * @param description - The description for the GitHub gist - * @param files - Array of file objects with path and name properties to include in the gist - * @param is_public - Whether the gist should be public or private - * @param env - Environment prefix to prepend to file names (e.g., "local-dev", "prod") - * @returns An Effect that yields a gist entry object with name and id properties - */ - createGistWithFiles, - - /** - * Retrieves file names from a GitHub gist, filtered by environment prefix. - * Fetches the list of files contained in the specified gist and returns only - * those that match the current environment, with the environment prefix removed. - * - * @param gist_id - The GitHub gist ID to retrieve file names from - * @param gist_name - The human-readable name of the gist (for logging purposes) - * @param env - Environment prefix to filter files by (e.g., "local-dev", "prod") - * @returns An Effect that yields an array of file names with environment prefix removed - */ - getGistFileNames, - - /** - * Removes a file from a specified GitHub gist. - * The file name is automatically prefixed with the environment when removing. - * @param gist_id - The ID of the gist to modify - * @param gist_name - The human-readable name of the gist (for logging purposes) - * @param file_name - The base name of the file to remove (without environment prefix) - * @param env - Environment prefix that was used when the file was added - * @returns An Effect that succeeds when the file is removed - */ - removeFileFromGist, - - /** - * Updates a file in a specified GitHub gist. - * The file name is automatically prefixed with the environment when updating. - * @param gist_id - The ID of the gist to modify - * @param gist_name - The human-readable name of the gist (for logging purposes) - * @param file_name - The base name of the file to update (without environment prefix) - * @param file_path - The local path of the file to update in the gist - * @param env - Environment prefix that was used when the file was added - * @returns An Effect that succeeds when the file is updated - */ - updateFileOfGist, - - /** - * Adds a new file to a specified GitHub gist. - * The file is automatically renamed with an environment prefix for multi-environment support. - * @param gist_id - The ID of the gist to modify - * @param gist_name - The human-readable name of the gist (for logging purposes) - * @param file - The file object containing path and name properties - * @param env - Environment prefix to prepend to the file name - * @returns An Effect that succeeds when the file is added and renamed - */ - addFileToGist, - - /** - * Deletes a specified GitHub gist by its ID. - * @param gist_id - The ID of the gist to delete - * @param gist_name - The human-readable name of the gist (for logging purposes) - * @returns An Effect that succeeds when the gist is deleted - */ - deleteGist - } - }) -}) { - static DefaultWithoutDependencies = Layer.effect(this, this.make) - static Default = this.DefaultWithoutDependencies.pipe( - Layer.provide(RunCommandService.Default) - ) -} - -export class GistHandler extends Context.Service()("GistHandler", { - make: Effect.gen(function*() { - const GH = yield* GHGistService - - // I prefer to provide these two only once during the main CLI pipeline setup - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - - return { - 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") - }) - - yield* Effect.logInfo(`Company: ${CONFIG.company}, ENV: ${CONFIG.env}`) - - yield* Effect.logInfo(`Reading configuration from ${YAMLPath}`) - - const configExists = yield* fs.exists(YAMLPath) - if (!configExists) { - return yield* new GistError({ message: `Configuration file not found: ${YAMLPath}` }) - } - - const configFromYaml = yield* pipe( - YAMLPath, - fs.readFileString, - Effect.andThen((content) => - Effect.try({ - try: () => yaml.load(content), - catch(error) { - return new GistYAMLError({ message: `Failed to parse YAML: ${(error as Error).message}` }) - } - }) - ), - Effect.andThen(Schema.decodeUnknownEffect(GistYAML)) - ) - - // load GitHub token securely from environment variable - 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 - - yield* GH.login(Redacted.value(redactedToken)) - - const cache = yield* SynchronizedRef.make( - yield* GH.loadGistCache(CONFIG.gistCacheId, CONFIG.company) - ) - - // filter YAML gists by company to ensure isolation between different organizations - // this prevents cross-company gist operations and maintains data separation - const thisCompanyGistsFromYaml = Object - .entries(configFromYaml.gists ?? {}) - .filter(([, v]) => v.company === CONFIG.company) - - for ( - const [name, gistConfig] of thisCompanyGistsFromYaml - ) { - const { description, files_with_name, public: is_public } = gistConfig - - yield* Effect.logInfo(`Processing gist ${name}`) - - const filesOnDiskWithFullPath = yield* Effect - .all( - files_with_name.map(Effect.fnUntraced(function*(f) { - const fullPath = path.join(configFromYaml.settings.base_directory, f.path) - const fileExists = yield* fs.exists(fullPath) - - if (!fileExists) { - yield* Effect.logWarning(`File not found: ${fullPath}, skipping...`) - return Option.none() - } - - return Option.some({ - path: fullPath, - name: f.name - }) - })), - { - concurrency: "unbounded" - } - ) - .pipe(Effect.map(Array.getSomes)) - - const gistFromCache = (yield* SynchronizedRef.get(cache)).entries.find((_) => _.name === name) - - // if the gist's name exists in cache, update the existing gist - // otherwise, create a new gist and update the local cache - if (gistFromCache) { - yield* Effect.logInfo(`Updating existing gist ${gistFromCache.name} with ID ${gistFromCache.id}`) - - // get current files in the gist to detect removed files - const gistFileNames = new Set( - yield* GH.getGistFileNames({ - gist_id: gistFromCache.id, - gist_name: gistFromCache.name, - env: CONFIG.env - }) - ) - - const expectedFiles = new Set(filesOnDiskWithFullPath.map(({ name }) => name)) - - // remove files that are no longer in YAML configuration - for (const gf of gistFileNames) { - if (!expectedFiles.has(gf)) { - yield* GH.removeFileFromGist({ - gist_id: gistFromCache.id, - gist_name: gistFromCache.name, - file_name: gf, - env: CONFIG.env - }) - } - } - - // update/add files from configuration - for (const f of filesOnDiskWithFullPath) { - if (gistFileNames.has(f.name)) { - yield* GH.updateFileOfGist({ - gist_id: gistFromCache.id, - gist_name: gistFromCache.name, - file_name: f.name, - file_path: f.path, - env: CONFIG.env - }) - } else { - yield* GH.addFileToGist({ - gist_id: gistFromCache.id, - gist_name: gistFromCache.name, - file: f, - env: CONFIG.env - }) - } - } - } else { - if (filesOnDiskWithFullPath.length !== 0) { - yield* SynchronizedRef.getAndUpdateEffect( - cache, - Effect.fnUntraced(function*(cache) { - return new GistCache({ - gist_id: cache.gist_id, - entries: [ - ...cache.entries, - { - ...(yield* GH.createGistWithFiles({ - gist_name: name, - description, - is_public, - - files: filesOnDiskWithFullPath, - env: CONFIG.env - })) - } - ], - company: cache.company - }) - }) - ) - } else { - yield* Effect.logWarning(`No valid files found for gist ${name}, skipping creation...`) - } - } - } - - // cache cleanup: remove gists that are no longer in YAML configuration - // only affects entries for the current company to maintain isolation - const configGistNames = new Set( - thisCompanyGistsFromYaml - .map(([name]) => name) - ) - - const newCache = yield* SynchronizedRef.updateAndGetEffect( - cache, - Effect.fnUntraced(function*(cache) { - const newEntries = [...cache.entries] - - // remove obsolete cache entries for current company only - // this ensures gists from other companies remain untouched - for (let i = newEntries.length - 1; i >= 0; i--) { - const cacheEntry = newEntries[i] - if (cacheEntry && !configGistNames.has(cacheEntry.name)) { - // delete the actual gist from GitHub - yield* GH.deleteGist({ - gist_id: cacheEntry.id, - gist_name: cacheEntry.name - }) - yield* Effect.logInfo( - `Obsolete gist ${cacheEntry.name} of company ${CONFIG.company} with ID ${cacheEntry.id}) will be removed from cache` - ) - newEntries.splice(i, 1) - } - } - - return { ...cache, entries: newEntries } - }) - ) - - yield* GH.saveGistCache(newCache) - - yield* Effect.logInfo("Gist operations completed") - }) - } - }) -}) { - static DefaultWithoutDependencies = Layer.effect(this, this.make) - static Default = this.DefaultWithoutDependencies.pipe( - Layer.provide(GHGistService.Default) - ) -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts deleted file mode 100644 index 3f5e0177a..000000000 --- a/packages/cli/src/index.ts +++ /dev/null @@ -1,904 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable no-constant-binary-expression */ -/* eslint-disable no-empty-pattern */ -// import necessary modules from the libraries -import { NodeRuntime, NodeServices } from "@effect/platform-node" -import { Argument, Command, Flag, Prompt } from "effect/cli" - -import * as Effect from "effect/Effect" -import * as FileSystem from "effect/FileSystem" -import * as Layer from "effect/Layer" -import * as Option from "effect/Option" -import * as Path from "effect/Path" -import * as Stream from "effect/Stream" -import { ExtractExportMappingsService } from "./extract.ts" -import { GistHandler } from "./gist.ts" -import { RunCommandService } from "./os-command.ts" -import { packages } from "./shared.ts" - -import { patchArgvForWrapCommands } from "./argv-patch.ts" -import { syncEffectAppSubtree } from "./sync-effect-app-subtree.ts" -import { syncEffectSubtree } from "./sync-effect-subtree.ts" -import { syncDiff, syncPush, syncShared } from "./sync-shared.ts" -import { makeNcuUpdateCommand } from "./update-packages.ts" - -patchArgvForWrapCommands(process.argv) - -NodeRuntime.runMain( - Effect - .fn("effa-cli")(function*() { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const extractExportMappings = yield* ExtractExportMappingsService - const { runGetExitCode } = yield* RunCommandService - - yield* Effect.addFinalizer(() => Effect.logInfo(`CLI has finished executing`)) - - /** - * Updates effect-app packages within their existing semver ranges using npm-check-updates. - * Runs both at workspace root and recursively in all workspace packages. - */ - const updateEffectAppPackages = Effect.fn("effa-cli.ue.updateEffectAppPackages")(function*() { - const filters = ["effect-app", "@effect-app/*"] - for (const filter of filters) { - yield* runGetExitCode(makeNcuUpdateCommand(filter)) - yield* runGetExitCode(makeNcuUpdateCommand(filter, { recursive: true })) - } - })() - - /** - * Updates Effect ecosystem packages within their existing semver ranges using npm-check-updates. - * Covers core Effect packages, Effect ecosystem packages, and Effect Atom packages. - * Runs both at workspace root and recursively in all workspace packages. - */ - const updateEffectPackages = Effect.fn("effa-cli.ue.updateEffectPackages")(function*() { - const effectFilters = ["effect", "@effect/*", "@effect-atom/*"] - for (const filter of effectFilters) { - yield* runGetExitCode(makeNcuUpdateCommand(filter)) - yield* runGetExitCode(makeNcuUpdateCommand(filter, { recursive: true })) - } - })() - - /** - * Updates all packages except Effect and Effect-App ecosystem packages to their latest versions using npm-check-updates. - * Excludes core Effect packages, Effect ecosystem packages, Effect Atom packages, and Effect-App packages. - * Preserves existing rejections from .ncurc.json configuration. - * Runs both at workspace root and recursively in all workspace packages. - */ - const updatePackages = Effect.fn("effa-cli.update-packages.updatePackages")(function*() { - const effectFilters = ["effect", "@effect/*", "@effect-atom/*", "effect-app", "@effect-app/*"] - - // read existing .ncurc.json to preserve existing reject patterns - let existingRejects: string[] = [] - const ncurcPath = "./.ncurc.json" - - if (yield* fs.exists(ncurcPath)) { - const ncurcContent = yield* fs.readFileString(ncurcPath) - const ncurc = JSON.parse(ncurcContent) - if (ncurc.reject && Array.isArray(ncurc.reject)) { - existingRejects = ncurc.reject - } - } - - const allRejects = [...existingRejects, ...effectFilters] - yield* Effect.logInfo(`Excluding packages from update: ${allRejects.join(", ")}`) - const rejectArgs = allRejects.map((filter) => `--reject "${filter}"`).join(" ") - - yield* runGetExitCode(`pnpm exec ncu -u ${rejectArgs}`) - yield* runGetExitCode(`pnpm -r exec ncu -u ${rejectArgs}`) - })() - - /** - * Links local effect-app packages by adding file resolutions to package.json. - * Updates the package.json with file: protocol paths pointing to the local effect-app-libs directory, - * then runs pnpm install to apply the changes. - * - * @param effectAppLibsPath - Path to the local effect-app-libs directory - * @returns An Effect that succeeds when linking is complete - */ - const linkPackages = Effect.fnUntraced(function*(effectAppLibsPath: string) { - yield* Effect.logInfo("Linking local effect-app packages...") - - const packageJsonPath = "./package.json" - const packageJsonContent = yield* fs.readFileString(packageJsonPath) - const trailingNewline = packageJsonContent.endsWith("\n") ? "\n" : "" - const pj = JSON.parse(packageJsonContent) - - const resolutions = { - ...pj.resolutions, - "@effect-app/eslint-codegen-model": "file:" + effectAppLibsPath + "/packages/eslint-codegen-model", - "effect-app": "file:" + effectAppLibsPath + "/packages/effect-app", - "@effect-app/infra": "file:" + effectAppLibsPath + "/packages/infra", - "@effect-app/vue": "file:" + effectAppLibsPath + "/packages/vue", - "@effect-app/vue-components": "file:" + effectAppLibsPath + "/packages/vue-components", - "@effect-app/eslint-shared-config": "file:" + effectAppLibsPath + "/packages/eslint-shared-config", - ...packages.reduce((acc, p) => { - acc[p] = `file:${effectAppLibsPath}/node_modules/${p}` - return acc - }, {} as Record) - } - - pj.resolutions = resolutions - - yield* fs.writeFileString(packageJsonPath, JSON.stringify(pj, null, 2) + trailingNewline) - yield* Effect.logInfo("Updated package.json with local file resolutions") - - yield* runGetExitCode("pnpm i") - - yield* Effect.logInfo("Successfully linked local packages") - }) - - /** - * Unlinks local effect-app packages by removing file resolutions from package.json. - * Filters out all effect-app related file: protocol resolutions from package.json, - * then runs pnpm install to restore registry packages. - * - * @returns An Effect that succeeds when unlinking is complete - */ - const unlinkPackages = Effect.fnUntraced(function*() { - yield* Effect.logInfo("Unlinking local effect-app packages...") - - const packageJsonPath = "./package.json" - const packageJsonContent = yield* fs.readFileString(packageJsonPath) - const trailingNewline = packageJsonContent.endsWith("\n") ? "\n" : "" - const pj = JSON.parse(packageJsonContent) - - const filteredResolutions = Object.entries(pj.resolutions as Record).reduce( - (acc, [k, v]) => { - if (k.startsWith("@effect-app/") || k === "effect-app" || packages.includes(k)) return acc - acc[k] = v - return acc - }, - {} as Record - ) - - pj.resolutions = filteredResolutions - - yield* fs.writeFileString(packageJsonPath, JSON.stringify(pj, null, 2) + trailingNewline) - yield* Effect.logInfo("Removed effect-app file resolutions from package.json") - - yield* runGetExitCode("pnpm i") - yield* Effect.logInfo("Successfully unlinked local packages") - })() - - /** - * Monitors controller files for changes and runs eslint on related controllers.ts/routes.ts files. - * Watches for .controllers. files and triggers eslint fixes on parent directory's controller files. - * - * @param watchPath - The path to watch for controller changes - * @param debug - Whether to enable debug logging - * @returns An Effect that sets up controller file monitoring - */ - const monitorChildIndexes = Effect.fn("effa-cli.index-multi.monitorChildIndexes")( - function*(watchPath: string) { - yield* Effect.logInfo(`Starting controller monitoring for: ${watchPath}`) - - const watchStream = fs.watch(watchPath) - - yield* watchStream - .pipe( - Stream.runForEach( - Effect.fn("effa-cli.monitorChildIndexes.handleEvent")(function*(event) { - const pathParts = event.path.split("/") - const fileName = pathParts[pathParts.length - 1] - const isController = fileName?.toLowerCase().includes(".controllers.") - - if (!isController) return - - let i = 1 - const reversedParts = pathParts.toReversed() - - while (i < reversedParts.length) { - const candidateFiles = ["controllers.ts", "routes.ts"] - .map((f) => [...pathParts.slice(0, pathParts.length - i), f].join("/")) - - const existingFiles: string[] = [] - for (const file of candidateFiles) { - const exists = yield* fs.exists(file) - if (exists) existingFiles.push(file) - } - - if (existingFiles.length > 0) { - yield* Effect.logInfo( - `Controller change detected: ${event.path}, fixing files: ${existingFiles.join(", ")}` - ) - - const eslintArgs = existingFiles.map((f) => `"../${f}"`).join(" ") - yield* runGetExitCode(`cd api && pnpm eslint --fix ${eslintArgs}`) - break - } - i++ - } - }) - ), - Effect.andThen( - Effect.addFinalizer(() => Effect.logInfo(`Stopped monitoring child indexes in: ${watchPath}`)) - ), - Effect.forkScoped - ) - } - ) - - /** - * Monitors a directory for changes and runs eslint on the specified index file. - * Triggers eslint fixes when any file in the directory changes (except the index file itself). - * - * @param watchPath - The path to watch for changes - * @param indexFile - The index file to run eslint on when changes occur - * @param debug - Whether to enable debug logging - * @returns An Effect that sets up root index monitoring - */ - const monitorRootIndexes = Effect.fn("effa-cli.index-multi.monitorRootIndexes")( - function*(watchPath: string, indexFile: string) { - yield* Effect.logInfo(`Starting root index monitoring for: ${watchPath} -> ${indexFile}`) - - const watchStream = fs.watch(watchPath) - - yield* watchStream - .pipe( - Stream.runForEach( - Effect.fn("effa-cli.index-multi.monitorRootIndexes.handleEvent")(function*(event) { - if (event.path.endsWith(indexFile)) return - - yield* Effect.logInfo(`Root change detected: ${event.path}, fixing: ${indexFile}`) - - yield* runGetExitCode(`pnpm eslint --fix "${indexFile}"`) - }) - ), - Effect.andThen( - Effect.addFinalizer(() => - Effect.logInfo(`Stopped monitoring root indexes in: ${watchPath} -> ${indexFile}`) - ) - ), - Effect.forkScoped - ) - } - ) - - /** - * Sets up comprehensive index monitoring for a given path. - * Combines both child controller monitoring and root index monitoring. - * - * @param watchPath - The path to monitor - * @param debug - Whether to enable debug logging - * @returns An Effect that sets up all index monitoring for the path - */ - const monitorIndexes = Effect.fn("effa-cli.index-multi.monitorIndexes")( - function*(watchPath: string) { - yield* Effect.logInfo(`Setting up index monitoring for path: ${watchPath}`) - - const indexFile = watchPath + "/index.ts" - - const monitors = [monitorChildIndexes(watchPath)] - - if (yield* fs.exists(indexFile)) { - monitors.push(monitorRootIndexes(watchPath, indexFile)) - } else { - yield* Effect.logWarning(`Index file ${indexFile} does not exist`) - } - - yield* Effect.logInfo(`Starting ${monitors.length} monitor(s) for ${watchPath}`) - - yield* Effect.all(monitors, { concurrency: monitors.length }) - } - ) - - /** - * Updates a package.json file with generated exports mappings for TypeScript modules. - * Scans TypeScript source files and creates export entries that map module paths - * to their compiled JavaScript and TypeScript declaration files. - * - * @param startDir - The starting directory path for resolving relative paths - * @param p - The package directory path to process - * @param levels - Optional depth limit for export filtering (0 = no limit) - * @returns An Effect that succeeds when the package.json is updated - */ - const packagejsonUpdater = Effect.fn("effa-cli.packagejsonUpdater")( - function*(startDir: string, p: string, levels = 0) { - yield* Effect.logInfo(`Generating exports for ${p}`) - - const exportMappings = yield* extractExportMappings(path.resolve(startDir, p)) - - // if exportMappings is empty skip export generation - if (exportMappings === "") { - yield* Effect.logInfo(`No src directory found for ${p}, skipping export generation`) - return - } - - const sortedExportEntries = JSON.parse( - `{ ${exportMappings} }` - ) as Record< - string, - unknown - > - - const filteredExportEntries = levels - ? Object - .keys(sortedExportEntries) - // filter exports by directory depth - only include paths up to specified levels deep - .filter((_) => _.split("/").length <= (levels + 1 /* `./` */)) - .reduce( - (prev, cur) => { - prev[cur] = sortedExportEntries[cur] - return prev - }, - {} as Record - ) - : sortedExportEntries - - const packageExports = { - ...((yield* fs.exists(p + "/src/index.ts")) - && { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }), - ...Object - .keys(filteredExportEntries) - .reduce( - (prev, cur) => { - // exclude index files and internal modules from package exports: - // - skip "./index" to avoid conflicts with the main "." export - // - skip "/internal/" paths to keep internal modules private - if (cur !== "./index" && !cur.includes("/internal/")) { - prev[cur] = filteredExportEntries[cur] - } - return prev - }, - {} as Record - ) - } - - const pkgJsonContent = yield* fs.readFileString(p + "/package.json", "utf-8") - const trailingNewline = pkgJsonContent.endsWith("\n") ? "\n" : "" - const pkgJson = JSON.parse(pkgJsonContent) - - pkgJson.exports = packageExports - - yield* Effect.logInfo(`Writing updated package.json for ${p}`) - - return yield* fs.writeFileString( - p + "/package.json", - JSON.stringify(pkgJson, null, 2) + trailingNewline - ) - } - ) - - /** - * Monitors a directory for TypeScript file changes and automatically updates package.json exports. - * Generates initial package.json exports, then watches the src directory for changes to regenerate exports. - * - * @param watchPath - The directory path containing the package.json and src to monitor - * @param levels - Optional depth limit for export filtering (0 = no limit) - * @returns An Effect that sets up package.json monitoring - */ - const monitorPackageJson = Effect.fn("effa-cli.monitorPackageJson")( - function*(startDir: string, watchPath: string, levels = 0) { - yield* packagejsonUpdater(startDir, watchPath, levels) - - const srcPath = watchPath === "." ? "./src" : `${watchPath}/src` - - if (!(yield* fs.exists(srcPath))) { - yield* Effect.logWarning(`Source directory ${srcPath} does not exist - skipping monitoring`) - return - } - - const watchStream = fs.watch(srcPath) - - yield* watchStream.pipe( - Stream.runForEach( - Effect.fn("effa-cli.monitorPackageJson.handleEvent")(function*(_) { - yield* packagejsonUpdater(startDir, watchPath, levels) - }) - ), - Effect.andThen( - Effect.addFinalizer(() => Effect.logInfo(`Stopped monitoring package.json for: ${watchPath}`)) - ), - Effect.forkScoped - ) - } - ) - - /* - * CLI - */ - - const WrapAsOption = Flag.String("wrap").pipe( - Flag.withAlias("w"), - Flag.optional, - Flag.withDescription( - "Wrap child bash command: the lifetime of the CLI command will be tied to the child process" - ) - ) - - // has prio over WrapAsOption - const WrapAsArg = Argument - .String("wrap") - .pipe( - Argument.atLeast(1), - Argument.optional, - Argument.withDescription( - "Wrap child bash command: the lifetime of the CLI command will be tied to the child process" - ) - ) - - /** - * Creates a command that automatically includes wrap functionality for executing child bash commands. - * Combines both option-based (--wrap) and argument-based wrap parameters, giving priority to arguments. - * If a wrap command is provided, it will be executed **after** the main command handler. - * - * @param name - The command name - * @param config - The command configuration (options, args, etc.) - * @param handler - The main command handler function - * @param completionMessage - Optional message to log when the command completes - * @returns A Command with integrated wrap functionality - */ - const makeCommandWithWrap = ( - name: Name, - config: Config, - handler: (_: any) => Effect.Effect, - completionMessage?: string - ) => - Command.make( - name, - { ...config, wo: WrapAsOption, wa: WrapAsArg }, - Effect.fn("effa-cli.withWrapHandler")(function*(_) { - const { wa, wo, ...cfg } = _ as unknown as { - wo: Option.Option - wa: Option.Option> - } - - if (completionMessage) { - yield* Effect.addFinalizer(() => Effect.logInfo(completionMessage)) - } - - const wrapOption = Option.orElse(wa, () => wo) - - yield* handler(cfg as any) - - if (Option.isSome(wrapOption)) { - const val: string = Array.isArray(wrapOption.value) - ? wrapOption.value.join(" ") - : wrapOption.value as string - - yield* Effect.logInfo(`Spawning child command: ${val}`) - const exitCode = yield* runGetExitCode(val) - if (exitCode !== 0) { - return yield* Effect.sync(() => process.exit(exitCode)) - } - } - - return - }, (_) => Effect.scoped(_)) - ) - - const EffectAppLibsPath = Argument - .Directory("effect-app-libs-path", { mustExist: true }) - .pipe( - Argument.withDefault("../../effect-app/libs"), - Argument.withDescription("Path to the effect-app-libs directory") - ) - - const link = Command - .make( - "link", - { effectAppLibsPath: EffectAppLibsPath }, - Effect.fn("effa-cli.link")(function*({ effectAppLibsPath }) { - return yield* linkPackages(effectAppLibsPath) - }) - ) - .pipe(Command.withDescription("Link local effect-app packages using file resolutions")) - - const unlink = Command - .make( - "unlink", - {}, - Effect.fn("effa-cli.unlink")(function*({}) { - return yield* unlinkPackages - }) - ) - .pipe(Command.withDescription("Remove effect-app file resolutions and restore npm registry packages")) - - const ue = Command - .make( - "ue", - {}, - Effect.fn("effa-cli.ue")(function*({}) { - yield* Effect.logInfo("Update effect-app and/or effect packages") - - const prompted = yield* Prompt.Select({ - choices: [ - { - title: "effect-app", - description: "Update only effect-app packages", - value: "effect-app" - }, - { - title: "effect", - description: "Update only effect packages", - value: "effect" - }, - { - title: "both", - description: "Update both effect-app and effect packages", - value: "both" - } - ], - message: "Select an option" - }) - - switch (prompted) { - case "effect-app": - return yield* updateEffectAppPackages.pipe( - Effect.andThen(runGetExitCode("pnpm i")) - ) - - case "effect": - return yield* updateEffectPackages.pipe( - Effect.andThen(runGetExitCode("pnpm i")) - ) - case "both": - return yield* updateEffectPackages.pipe( - Effect.andThen(updateEffectAppPackages), - Effect.andThen(runGetExitCode("pnpm i")) - ) - } - }) - ) - .pipe(Command.withDescription("Update effect-app and/or effect packages")) - - const up = Command - .make( - "up", - {}, - Effect.fn("effa-cli.update-packages")(function*({}) { - yield* Effect.logInfo("Updating all packages except Effect/Effect-App ecosystem packages...") - - return yield* updatePackages.pipe( - Effect.andThen(runGetExitCode("pnpm i")) - ) - }) - ) - .pipe(Command.withDescription("Update all packages except Effect/Effect-App ecosystem packages")) - - const indexMulti = makeCommandWithWrap( - "index-multi", - {}, - Effect.fn("effa-cli.index-multi")(function*({}) { - yield* Effect.logInfo("Starting multi-index monitoring") - - const dirs = ["./api/src"] - - const existingDirs: string[] = [] - for (const dir of dirs) { - const dirExists = yield* fs.exists(dir) - if (dirExists) { - existingDirs.push(dir) - } else { - yield* Effect.logWarning(`Directory ${dir} does not exist - skipping`) - } - } - - const monitors = existingDirs.map((dir) => monitorIndexes(dir)) - yield* Effect.all(monitors, { concurrency: monitors.length }) - }), - "Stopped multi-index monitoring" - ) - .pipe( - Command.withDescription( - "Monitor multiple directories for index and controller file changes" - ) - ) - - const packagejson = makeCommandWithWrap( - "packagejson", - {}, - Effect.fn("effa-cli.packagejson")(function*({}) { - // https://nodejs.org/api/path.html#pathresolvepaths - const startDir = path.resolve() - - return yield* monitorPackageJson(startDir, ".") - }), - "Stopped monitoring root package.json exports" - ) - .pipe( - Command.withDescription("Generate and update root-level package.json exports mappings for TypeScript modules") - ) - - const packagejsonPackages = makeCommandWithWrap( - "packagejson-packages", - {}, - Effect.fn("effa-cli.packagejson-packages")(function*({}) { - // https://nodejs.org/api/path.html#pathresolvepaths - const startDir = path.resolve() - - const packagesDir = path.join(startDir, "packages") - - const packagesExists = yield* fs.exists(packagesDir) - if (!packagesExists) { - return yield* Effect.logWarning("No packages directory found") - } - - // get all package directories - const packageDirs = yield* fs.readDirectory(packagesDir) - - const validPackages: string[] = [] - - // filter packages that have package.json and src directory - for (const packageName of packageDirs) { - const packagePath = path.join(packagesDir, packageName) - const packageJsonExists = yield* fs.exists(path.join(packagePath, "package.json")) - const srcExists = yield* fs.exists(path.join(packagePath, "src")) - - const shouldExclude = false - || packageName.endsWith("eslint-codegen-model") - || packageName.endsWith("eslint-shared-config") - || packageName.endsWith("vue-components") - - if (packageJsonExists && srcExists && !shouldExclude) { - validPackages.push(packagePath) - } - } - - yield* Effect.logInfo(`Found ${validPackages.length} packages to update`) - - // update each package sequentially - yield* Effect.all( - validPackages.map( - Effect.fnUntraced(function*(packagePath) { - const relativePackagePath = path.relative(startDir, packagePath) - yield* Effect.logInfo(`Updating ${relativePackagePath}`) - return yield* monitorPackageJson(startDir, relativePackagePath) - }) - ) - ) - - yield* Effect.logInfo("All packages updated successfully") - }), - "Stopped monitoring package.json exports for all packages" - ) - .pipe( - Command.withDescription("Generate and update package.json exports mappings for all packages in monorepo") - ) - - const gist = Command - .make( - "gist", - { - config: Flag.File("config").pipe( - Flag.withDefault("gists.yaml"), - Flag.withDescription("Path to YAML configuration file") - ) - }, - Effect.fn("effa-cli.gist")(function*({ config }) { - const gh = yield* GistHandler - return yield* gh.handler({ - YAMLPath: config - }) - }) - ) - .pipe(Command.withDescription("Create GitHub gists from files specified in YAML configuration")) - - const nuke = Command - .make( - "nuke", - { - dryRun: Flag.Boolean("dry-run").pipe( - Flag.withDescription("Show what would be done without making changes") - ), - storePrune: Flag.Boolean("store-prune").pipe( - Flag.withDescription("Prune the package manager store") - ) - }, - Effect.fn("effa-cli.nuke")(function*({ dryRun, storePrune }) { - yield* Effect.logInfo(dryRun ? "Performing dry run cleanup..." : "Performing nuclear cleanup...") - - if (dryRun) { - yield* runGetExitCode( - "find . -depth \\( -type d \\( -name 'node_modules' -o -name '.nuxt' -o -name 'dist' -o -name '.output' -o -name '.nitro' -o -name '.cache' -o -name 'test-results' -o -name 'test-out' -o -name 'coverage' \\) -print \\) -o \\( -type f \\( -name '*.log' -o -name '*.tsbuildinfo' \\) -print \\)" - ) - } else { - yield* runGetExitCode( - "find . -depth \\( -type d \\( -name 'node_modules' -o -name '.nuxt' -o -name 'dist' -o -name '.output' -o -name '.nitro' -o -name '.cache' -o -name 'test-results' -o -name 'test-out' -o -name 'coverage' \\) -exec rm -rf -- {} + \\) -o \\( -type f \\( -name '*.log' -o -name '*.tsbuildinfo' \\) -delete \\)" - ) - - if (storePrune) { - yield* runGetExitCode( - "pnpm store prune" - ) - } - } - - yield* Effect.logInfo("Cleanup operation completed") - }) - ) - .pipe(Command.withDescription("Nuclear cleanup command: removes all generated files and cleans the workspace")) - - const syncEffect = Command - .make( - "sync-effect", - { - 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( - Flag.optional, - Flag.withDescription("Subtree prefix (default: repos/effect)") - ), - url: Flag.String("url").pipe( - Flag.optional, - Flag.withDescription( - "Git repository URL (default: https://github.com/Effect-TS/effect.git)" - ) - ) - }, - Effect.fn("effa-cli.sync-effect")(function*({ manifests, prefix, url }) { - yield* syncEffectSubtree({ - manifestPaths: Option.match(manifests, { - onNone: () => ["package.json"], - onSome: (m) => m.split(",").map((p) => p.trim()) - }), - subtreePrefix: Option.getOrElse(prefix, () => "repos/effect"), - url: Option.getOrElse(url, () => "https://github.com/Effect-TS/effect.git") - }) - }) - ) - .pipe(Command.withDescription("Sync the Effect subtree to the version pinned in package.json")) - - const syncEffectApp = Command - .make( - "sync-effect-app", - { - 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( - Flag.optional, - Flag.withDescription("Subtree prefix (default: repos/libs)") - ), - 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( - Flag.optional, - Flag.withDescription("Ref escape hatch (branch/tag/sha/latest); latest means main") - ) - }, - Effect.fn("effa-cli.sync-effect-app")(function*({ manifests, prefix, ref, url }) { - yield* syncEffectAppSubtree({ - manifestPaths: Option.match(manifests, { - onNone: () => ["package.json"], - onSome: (m) => m.split(",").map((p) => p.trim()) - }), - ...Option.match(ref, { - onNone: () => ({}), - onSome: (ref) => ({ ref }) - }), - subtreePrefix: Option.getOrElse(prefix, () => "repos/libs"), - url: Option.getOrElse(url, () => "https://github.com/effect-app/libs.git") - }) - }) - ) - .pipe(Command.withDescription("Sync the Effect App libs subtree to the version pinned in package.json")) - - const SharedLockfileFlag = Flag.File("lockfile").pipe( - Flag.optional, - Flag.withDescription("Path to lockfile (default: .shared.json)") - ) - - const sync = Command - .make( - "sync", - { - lockfile: SharedLockfileFlag, - update: Flag.Boolean("update").pipe( - Flag.withDescription("Bump the pinned ref to the latest sha before syncing") - ), - ref: Flag.String("ref").pipe( - Flag.optional, - Flag.withDescription("Ref (branch/tag/sha) to update to; default: remote default branch HEAD") - ) - }, - Effect.fn("effa-cli.sync")(function*({ lockfile, ref, update }) { - yield* syncShared({ - lockfilePath: Option.getOrElse(lockfile, () => ".shared.json"), - update, - ref: Option.getOrUndefined(ref) - }) - }) - ) - .pipe( - Command.withDescription("Sync shared docs / e2e helpers / plugins from effect-app/shared per .shared.json") - ) - - const syncDiffCmd = Command - .make( - "sync-diff", - { lockfile: SharedLockfileFlag }, - Effect.fn("effa-cli.sync-diff")(function*({ lockfile }) { - yield* syncDiff({ - lockfilePath: Option.getOrElse(lockfile, () => ".shared.json") - }) - }) - ) - .pipe(Command.withDescription("Report drift between local synced files and the pinned shared ref")) - - const syncPushCmd = Command - .make( - "sync-push", - { - lockfile: SharedLockfileFlag, - message: Flag.String("message").pipe( - Flag.withAlias("m"), - Flag.optional, - Flag.withDescription("Commit message for the push") - ), - branch: Flag.String("branch").pipe( - Flag.optional, - Flag.withDescription("Branch name in shared repo (default: auto-generated)") - ), - pr: Flag.Boolean("pr").pipe( - Flag.withDescription("Open a PR via `gh pr create` after pushing") - ) - }, - Effect.fn("effa-cli.sync-push")(function*({ branch, lockfile, message, pr }) { - yield* syncPush({ - lockfilePath: Option.getOrElse(lockfile, () => ".shared.json"), - message: Option.getOrUndefined(message), - branch: Option.getOrUndefined(branch), - pr - }) - }) - ) - .pipe(Command.withDescription("Push locally-modified synced files to the shared repo on a new branch")) - - // configure CLI - return yield* Command.run( - Command - .make("effa") - .pipe(Command.withSubcommands([ - ue, - up, - link, - unlink, - indexMulti, - packagejson, - packagejsonPackages, - gist, - nuke, - syncEffect, - syncEffectApp, - sync, - syncDiffCmd, - syncPushCmd - ])), - { - version: "v1.0.0" - } - ) - })() - .pipe( - Effect.scoped, - Effect.provide( - Layer.provideMerge( - Layer.merge( - GistHandler.Default, - RunCommandService.Default - ), - NodeServices.layer - ) - ) - ) -) diff --git a/packages/cli/src/os-command.ts b/packages/cli/src/os-command.ts deleted file mode 100644 index 9982a34cc..000000000 --- a/packages/cli/src/os-command.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable no-constant-binary-expression */ -/* eslint-disable no-empty-pattern */ -import * as Context from "effect/Context" -import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" -import { ChildProcess } from "effect/process" -import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner" - -/** - * Service for executing shell commands using the Effect platform's Command API. - * Provides methods to run shell commands with different output handling strategies. - * All commands are executed through the system shell (/bin/sh) for proper command parsing. - */ -export class RunCommandService extends Context.Service()("RunCommandService", { - make: Effect.gen(function*() { - // will be provided by the main CLI pipeline setup - const spawner = yield* ChildProcessSpawner - - /** - * Executes a shell command using Command API with inherited stdio streams. - * The command is run through the system shell (/bin/sh) for proper command parsing. - * - * @param cmd - The shell command to execute - * @param cwd - Optional working directory to execute the command in - * @returns An Effect that succeeds with the exit code or fails with a PlatformError - */ - const runGetExitCode = (cmd: string, cwd?: string) => - spawner - .exitCode( - ChildProcess.make("sh", ["-c", cmd], { stdout: "inherit", stderr: "inherit", cwd }) - ) - - /** - * Executes a shell command using Command API and returns the output as a string. - * The command is run through the system shell (/bin/sh) for proper command parsing. - * - * @param cmd - The shell command to execute - * @param cwd - Optional working directory to execute the command in - * @returns An Effect that succeeds with the command's stdout output as string or fails with a PlatformError - */ - const runGetString = (cmd: string, cwd?: string) => - spawner - .string( - ChildProcess.make("sh", ["-c", cmd], { cwd }) - ) - - return { - runGetExitCode, - runGetString - } - }) -}) { - static Default = Layer.effect(this, this.make) -} diff --git a/packages/cli/src/shared.ts b/packages/cli/src/shared.ts deleted file mode 100644 index 6aade80bb..000000000 --- a/packages/cli/src/shared.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const packages = ["effect", "@effect/platform", "@effect/platform-node"] - -export const EFFECT_APP_LIBS = process.env["EFFECT_APP_LIBS"] || "../../effect-app/libs" diff --git a/packages/cli/src/sync-effect-app-subtree.ts b/packages/cli/src/sync-effect-app-subtree.ts deleted file mode 100644 index 00aa657ec..000000000 --- a/packages/cli/src/sync-effect-app-subtree.ts +++ /dev/null @@ -1,307 +0,0 @@ -import * as Arr from "effect/Array" -import * as Effect from "effect/Effect" -import * as FileSystem from "effect/FileSystem" -import * as Option from "effect/Option" -import * as Path from "node:path" -import { RunCommandService } from "./os-command.ts" - -export interface SyncEffectAppConfig { - readonly manifestPaths: ReadonlyArray - readonly subtreePrefix: string - readonly url: string - readonly ref?: string -} - -interface VersionMention { - readonly manifestPath: string - readonly section: string - readonly name: string - readonly range: string - readonly normalized: string -} - -const depSections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const - -const packageManifestPaths: Record = { - "effect-app": "packages/effect-app/package.json", - "@effect-app/infra": "packages/infra/package.json", - "@effect-app/vue": "packages/vue/package.json", - "@effect-app/vue-components": "packages/vue-components/package.json", - "@effect-app/cli": "packages/cli/package.json", - "@effect-app/eslint-codegen-model": "packages/eslint-codegen-model/package.json", - "@effect-app/eslint-shared-config": "packages/eslint-shared-config/package.json" -} - -const packagePreference = Object.keys(packageManifestPaths) - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value) - -const isEffectAppPackage = (name: string) => name === "effect-app" || name.startsWith("@effect-app/") - -const isNonEmptyString = (value: string) => value !== "" - -const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'` - -const normalizeVersion = (range: string) => { - const match = range.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/) - return match?.[0] -} - -const isFullSha = (ref: string) => /^[0-9a-f]{40}$/i.test(ref) - -const resolveRequestedRef = (ref: string) => ref === "latest" ? "main" : ref - -const parsePackageVersion = (source: string, packagePath: string) => { - const parsed: unknown = JSON.parse(source) - if (!isRecord(parsed) || typeof parsed["version"] !== "string") { - throw new Error(`Could not read version from ${packagePath}`) - } - return parsed["version"] -} - -const parseDefaultBranch = (source: string) => { - const line = source - .split("\n") - .find((line) => line.startsWith("ref: refs/heads/") && line.endsWith("\tHEAD")) - - if (!line) { - throw new Error("Could not resolve default branch for effect-app libs repository") - } - - return line.slice("ref: refs/heads/".length, -"\tHEAD".length) -} - -const selectVersionMentions = (mentions: Arr.NonEmptyArray) => { - for (const packageName of packagePreference) { - const selected = mentions.filter((mention) => mention.name === packageName) - if (Arr.isArrayNonEmpty(selected)) return selected - } - - return mentions -} - -const describeMentions = (mentions: ReadonlyArray) => - mentions - .map((mention) => `${mention.manifestPath} (${mention.section}) ${mention.name}: ${mention.range}`) - .join("\n") - -export const syncEffectAppSubtree = Effect.fnUntraced(function*(config: SyncEffectAppConfig) { - const fs = yield* FileSystem.FileSystem - const { runGetExitCode, runGetString } = yield* RunCommandService - - const run = Effect.fnUntraced(function*(command: string) { - const exitCode = yield* runGetExitCode(command) - if (exitCode !== 0) { - return yield* Effect.fail(new Error(`Command failed with exit code ${exitCode}: ${command}`)) - } - }) - - const fetchRemoteRef = Effect.fnUntraced(function*(remoteRef: string, localRef: string) { - const rawRef = `${localRef}-raw` - - yield* run( - `git fetch --no-tags ${shellQuote(config.url)} ${shellQuote(`+${remoteRef}:${rawRef}`)}` - ) - - const commit = (yield* runGetString(`git rev-list -n1 ${shellQuote(rawRef)}`)).trim() - yield* run(`git update-ref ${shellQuote(localRef)} ${shellQuote(commit)}`) - }) - - const fetchRequestedRef = Effect.fnUntraced(function*(requestedRef: string, localRef: string) { - if (isFullSha(requestedRef)) { - const localCommit = yield* runGetString( - `git rev-parse --verify ${shellQuote(`${requestedRef}^{commit}`)}` - ) - .pipe(Effect.option) - - if (Option.isSome(localCommit)) { - return yield* run(`git update-ref ${shellQuote(localRef)} ${shellQuote(localCommit.value.trim())}`) - } - } - - const remoteRefs = requestedRef.startsWith("refs/") - ? [requestedRef] - : [`refs/heads/${requestedRef}`, `refs/tags/${requestedRef}`, requestedRef] - - for (const remoteRef of remoteRefs) { - const exists = yield* runGetString( - `git ls-remote --exit-code ${shellQuote(config.url)} ${shellQuote(remoteRef)}` - ) - .pipe(Effect.option) - - if (Option.isSome(exists)) { - return yield* fetchRemoteRef(remoteRef, localRef) - } - } - - return yield* Effect.fail(new Error(`Could not resolve effect-app ref: ${requestedRef}`)) - }) - - if (config.ref) { - const ref = resolveRequestedRef(config.ref) - const syncRef = "refs/effa-sync/effect-app-requested" - yield* Effect.logInfo(`Using effect-app subtree ref: ${ref}`) - yield* Effect.logInfo(`Using effect-app subtree url: ${config.url}`) - yield* fetchRequestedRef(ref, syncRef) - return yield* run( - `git -c status.showUntrackedFiles=no subtree pull --prefix=${shellQuote(config.subtreePrefix)} . ${ - shellQuote(syncRef) - } --squash` - ) - } - - const versionMentions: Array = [] - - for (const manifestPath of config.manifestPaths) { - if (!(yield* fs.exists(manifestPath))) continue - - const manifest: unknown = JSON.parse(yield* fs.readFileString(manifestPath)) - if (!isRecord(manifest)) continue - - for (const section of depSections) { - const deps = manifest[section] - if (!isRecord(deps)) continue - - for (const [name, range] of Object.entries(deps)) { - if (!isEffectAppPackage(name) || typeof range !== "string") continue - - const normalized = normalizeVersion(range) - if (!normalized) continue - - versionMentions.push({ manifestPath, section, name, range, normalized }) - } - } - } - - if (!Arr.isArrayNonEmpty(versionMentions)) { - return yield* Effect.fail( - new Error("Could not find any effect-app/@effect-app dependency versions in manifests") - ) - } - - const selectedMentions = selectVersionMentions(versionMentions) - const versions = [...new Set(selectedMentions.map((mention) => mention.normalized))] - - if (Arr.isArrayNonEmpty(versions.slice(1))) { - return yield* Effect.fail( - new Error(`Found multiple Effect App versions. Please align first:\n${describeMentions(selectedMentions)}`) - ) - } - - const version = selectedMentions[0].normalized - const packageName = selectedMentions[0].name - const packagePath = packageManifestPaths[packageName] - - if (!packagePath) { - return yield* Effect.fail( - new Error(`No effect-app libs package manifest mapping for ${packageName}`) - ) - } - - const tagCandidates = [ - `${packageName}@${version}`, - `v${version}`, - version - ] - - let remoteRef: string | undefined - for (const candidate of tagCandidates) { - const tagResult = yield* runGetString( - `git ls-remote --exit-code --tags ${shellQuote(config.url)} ${shellQuote(`refs/tags/${candidate}`)}` - ) - .pipe(Effect.option) - - if (Option.isSome(tagResult)) { - remoteRef = candidate - break - } - } - - if (remoteRef) { - const syncRef = "refs/effa-sync/effect-app-tag" - yield* Effect.logInfo(`Using effect-app package: ${packageName}@${version}`) - yield* Effect.logInfo(`Using effect-app subtree ref: ${remoteRef}`) - yield* Effect.logInfo(`Using effect-app subtree url: ${config.url}`) - yield* fetchRemoteRef(`refs/tags/${remoteRef}`, syncRef) - return yield* run( - `git -c status.showUntrackedFiles=no subtree pull --prefix=${shellQuote(config.subtreePrefix)} . ${ - shellQuote(syncRef) - } --squash` - ) - } - - const defaultBranchOutput = yield* runGetString(`git ls-remote --symref ${shellQuote(config.url)} HEAD`) - const defaultBranch = yield* Effect.try({ - try: () => parseDefaultBranch(defaultBranchOutput), - catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)) - }) - - const cachePath = (yield* runGetString("git rev-parse --git-path effa-sync/effect-app-libs.git")).trim() - yield* run(`mkdir -p ${shellQuote(Path.dirname(cachePath))}`) - - if (!(yield* fs.exists(cachePath))) { - yield* run(`git init --bare ${shellQuote(cachePath)}`) - } - - yield* run( - `git --git-dir=${shellQuote(cachePath)} fetch --no-tags --prune ${shellQuote(config.url)} ${ - shellQuote(`+refs/heads/${defaultBranch}:refs/heads/${defaultBranch}`) - }` - ) - - const commits = (yield* runGetString( - `git --git-dir=${shellQuote(cachePath)} rev-list ${shellQuote(`refs/heads/${defaultBranch}`)}` - )) - .trim() - .split("\n") - .filter(isNonEmptyString) - - if (!Arr.isArrayNonEmpty(commits)) { - return yield* Effect.fail( - new Error(`No commits found on ${defaultBranch} for ${config.url}`) - ) - } - - let commit: string | undefined - for (const candidate of commits) { - const packageJsonResult = yield* runGetString( - `git --git-dir=${shellQuote(cachePath)} show ${shellQuote(`${candidate}:${packagePath}`)}` - ) - .pipe(Effect.option) - - if (Option.isNone(packageJsonResult)) continue - - const candidateVersion = yield* Effect.try({ - try: () => parsePackageVersion(packageJsonResult.value, packagePath), - catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)) - }) - - if (candidateVersion === version) { - commit = candidate - break - } - } - - if (!commit) { - return yield* Effect.fail( - new Error( - `No commit found on ${defaultBranch} where ${packagePath} has version ${version}. Tried tags: ${ - tagCandidates.join(", ") - }` - ) - ) - } - - const syncRef = "refs/effa-sync/effect-app-selected" - yield* run(`git --git-dir=${shellQuote(cachePath)} update-ref ${shellQuote(syncRef)} ${shellQuote(commit)}`) - - yield* Effect.logInfo(`Using effect-app package: ${packageName}@${version}`) - yield* Effect.logInfo(`Using effect-app subtree commit: ${commit}`) - yield* Effect.logInfo(`Using effect-app subtree url: ${config.url}`) - yield* run( - `git -c status.showUntrackedFiles=no subtree pull --prefix=${shellQuote(config.subtreePrefix)} ${ - shellQuote(cachePath) - } ${shellQuote(syncRef)} --squash` - ) -}) diff --git a/packages/cli/src/sync-effect-subtree.ts b/packages/cli/src/sync-effect-subtree.ts deleted file mode 100644 index a87d823d3..000000000 --- a/packages/cli/src/sync-effect-subtree.ts +++ /dev/null @@ -1,140 +0,0 @@ -import * as Effect from "effect/Effect" -import * as FileSystem from "effect/FileSystem" -import * as Option from "effect/Option" -import { RunCommandService } from "./os-command.ts" - -export interface SyncEffectConfig { - readonly manifestPaths: ReadonlyArray - readonly subtreePrefix: string - readonly url: string -} - -const depSections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const - -const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'` - -const normalizeVersion = (range: string) => { - const match = range.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/) - return match?.[0] -} - -export const syncEffectSubtree = Effect.fnUntraced(function*(config: SyncEffectConfig) { - const fs = yield* FileSystem.FileSystem - const { runGetExitCode, runGetString } = yield* RunCommandService - - const run = Effect.fnUntraced(function*(command: string) { - const exitCode = yield* runGetExitCode(command) - if (exitCode !== 0) { - return yield* Effect.fail(new Error(`Command failed with exit code ${exitCode}: ${command}`)) - } - }) - - const fetchRemoteRef = Effect.fnUntraced(function*(remoteRef: string, localRef: string) { - const rawRef = `${localRef}-raw` - - yield* run( - `git fetch --no-tags ${shellQuote(config.url)} ${shellQuote(`+${remoteRef}:${rawRef}`)}` - ) - - const commit = (yield* runGetString(`git rev-list -n1 ${shellQuote(rawRef)}`)).trim() - yield* run(`git update-ref ${shellQuote(localRef)} ${shellQuote(commit)}`) - }) - - const versionMentions: Array<{ - manifestPath: string - section: string - name: string - range: string - normalized: string - }> = [] - - for (const manifestPath of config.manifestPaths) { - if (!(yield* fs.exists(manifestPath))) continue - - const manifest = JSON.parse(yield* fs.readFileString(manifestPath)) - - for (const section of depSections) { - const deps = (manifest[section] ?? {}) as Record - - for (const [name, range] of Object.entries(deps)) { - if (name !== "effect" && !name.startsWith("@effect/")) continue - - const normalized = normalizeVersion(String(range)) - if (!normalized) continue - - versionMentions.push({ manifestPath, section, name, range: String(range), normalized }) - } - } - } - - const effectPackageMentions = versionMentions.filter((e) => e.name === "effect") - - const versionsFromEffectPackage = [...new Set(effectPackageMentions.map((e) => e.normalized))] - - const versionsFromEffectScope = [ - ...new Set( - versionMentions - .filter((e) => e.name.startsWith("@effect/") && e.normalized.startsWith("4.")) - .map((e) => e.normalized) - ) - ] - - const versions = versionsFromEffectPackage.length > 0 - ? versionsFromEffectPackage - : versionsFromEffectScope - - if (versions.length === 0) { - return yield* Effect.fail( - new Error("Could not find any effect/@effect dependency versions in manifests") - ) - } - - if (versions.length > 1) { - const details = (effectPackageMentions.length > 0 ? effectPackageMentions : versionMentions) - .map((e) => `${e.manifestPath} (${e.section}) ${e.name}: ${e.range}`) - .join("\n") - - return yield* Effect.fail( - new Error(`Found multiple Effect versions. Please align first:\n${details}`) - ) - } - - const version = versions[0] - const candidateRefs = [ - `effect@${version}`, - `@effect/effect@${version}`, - `@effect/ai-anthropic@${version}` - ] - - const url = shellQuote(config.url) - - let ref: string | undefined - for (const candidate of candidateRefs) { - const tagResult = yield* runGetString( - `git ls-remote --exit-code --tags ${url} ${shellQuote(`refs/tags/${candidate}`)}` - ) - .pipe(Effect.option) - - if (Option.isSome(tagResult)) { - ref = candidate - break - } - } - - if (!ref) { - return yield* Effect.fail( - new Error(`No matching tag found for version ${version}. Tried: ${candidateRefs.join(", ")}`) - ) - } - - yield* Effect.logInfo(`Using effect version: ${version}`) - yield* Effect.logInfo(`Using subtree ref: ${ref}`) - yield* Effect.logInfo(`Using subtree url: ${config.url}`) - const syncRef = "refs/effa-sync/effect-selected" - yield* fetchRemoteRef(`refs/tags/${ref}`, syncRef) - yield* run( - `git -c status.showUntrackedFiles=no subtree pull --prefix=${shellQuote(config.subtreePrefix)} . ${ - shellQuote(syncRef) - } --squash` - ) -}) diff --git a/packages/cli/src/sync-shared.ts b/packages/cli/src/sync-shared.ts deleted file mode 100644 index 2ad967a93..000000000 --- a/packages/cli/src/sync-shared.ts +++ /dev/null @@ -1,349 +0,0 @@ -import * as Effect from "effect/Effect" -import * as FileSystem from "effect/FileSystem" -import * as Path from "effect/Path" -import { RunCommandService } from "./os-command.ts" - -/** - * Project-side lockfile shape (`.shared.json`). - * - * `artifacts` maps SOURCE path (inside the shared repo) to DEST path (inside the - * consuming project). `exclude` lists source-relative paths to skip during sync. - */ -export interface SharedLockfile { - readonly repo: string - readonly ref: string - readonly artifacts: Record - readonly exclude?: ReadonlyArray - readonly synced_at?: string -} - -const sanitizeRepoSlug = (repo: string) => repo.replace(/[^A-Za-z0-9_.-]+/g, "_") - -const repoCloneUrl = (repo: string) => { - if (repo.startsWith("http") || repo.startsWith("git@")) return repo - // "github.com/effect-app/shared" → "git@github.com:effect-app/shared.git" - const m = repo.match(/^github\.com\/(.+?)\/(.+?)$/) - if (m) return `git@github.com:${m[1]}/${m[2]}.git` - return repo -} - -const joinPosix = (a: string, b: string) => a.endsWith("/") ? a + b : a + "/" + b - -const readLockfile = Effect.fnUntraced(function*(lockfilePath: string) { - const fs = yield* FileSystem.FileSystem - if (!(yield* fs.exists(lockfilePath))) { - return yield* Effect.fail(new Error(`No ${lockfilePath} found in current directory.`)) - } - const content = yield* fs.readFileString(lockfilePath) - return { content, lockfile: JSON.parse(content) as SharedLockfile } -}) - -const ensureCache = Effect.fnUntraced(function*(lockfile: SharedLockfile) { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const { runGetExitCode } = yield* RunCommandService - - const home = process.env["HOME"] ?? process.env["USERPROFILE"] - if (!home) return yield* Effect.fail(new Error("Cannot resolve home directory.")) - - const cacheRoot = path.join(home, ".cache", "effa", "shared") - const cachePath = path.join(cacheRoot, sanitizeRepoSlug(lockfile.repo)) - const cloneUrl = repoCloneUrl(lockfile.repo) - - if (!(yield* fs.exists(cachePath))) { - yield* runGetExitCode(`mkdir -p ${JSON.stringify(cacheRoot)}`) - yield* runGetExitCode(`git clone ${JSON.stringify(cloneUrl)} ${JSON.stringify(cachePath)}`) - } else { - yield* runGetExitCode("git fetch --all --tags --prune", cachePath) - } - - yield* runGetExitCode(`git checkout ${JSON.stringify(lockfile.ref)}`, cachePath) - - return cachePath -}) - -/** - * Resolve `requestedRef` (or the remote default branch HEAD if omitted) to a - * concrete commit sha in the fetched cache. Accepts branch names, tags, or - * shas; bare branch names are resolved against `origin/` first so the result - * is the latest remote commit, not a stale local ref. - */ -const resolveLatestSha = Effect.fnUntraced(function*(cachePath: string, requestedRef?: string) { - const { runGetExitCode, runGetString } = yield* RunCommandService - - const target = requestedRef - ?? (yield* runGetString("git rev-parse --abbrev-ref origin/HEAD", cachePath)).trim() - - const candidates = target.includes("/") ? [target] : [`origin/${target}`, target] - for (const candidate of candidates) { - const rev = JSON.stringify(`${candidate}^{commit}`) - const code = yield* runGetExitCode(`git rev-parse --verify --quiet ${rev}`, cachePath) - if (code === 0) { - return (yield* runGetString(`git rev-parse ${rev}`, cachePath)).trim() - } - } - - return yield* Effect.fail(new Error(`Cannot resolve ref: ${target}`)) -}) - -/** - * Walk artifact files. For each file in the artifact map (respecting excludes), - * yields `{ srcRel, srcAbs, destAbs }`. - */ -const walkArtifacts = Effect.fnUntraced(function*( - lockfile: SharedLockfile, - cachePath: string -) { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const { runGetString } = yield* RunCommandService - - const excludeSet = new Set(lockfile.exclude ?? []) - const results: Array<{ srcRel: string; srcAbs: string; destAbs: string; excluded: boolean }> = [] - - for (const [srcRel, destRel] of Object.entries(lockfile.artifacts)) { - const srcAbs = path.join(cachePath, srcRel) - const destAbs = path.resolve(destRel) - - if (!(yield* fs.exists(srcAbs))) continue - - const stat = yield* fs.stat(srcAbs) - - if (stat.type === "File") { - results.push({ srcRel, srcAbs, destAbs, excluded: excludeSet.has(srcRel) }) - continue - } - - const fileList = yield* runGetString(`find ${JSON.stringify(srcAbs)} -type f`) - for (const fileAbs of fileList.split("\n").filter((l) => l.trim() !== "")) { - const relInArtifact = path.relative(srcAbs, fileAbs) - const srcRelFull = joinPosix(srcRel, relInArtifact) - const destFileAbs = path.join(destAbs, relInArtifact) - results.push({ - srcRel: srcRelFull, - srcAbs: fileAbs, - destAbs: destFileAbs, - excluded: excludeSet.has(srcRelFull) - }) - } - } - - return results -}) - -/** - * Pull artifacts from the shared repo into the consuming project according to - * `.shared.json`. Idempotent — running twice without changes is a no-op. - * - * MVP: overwrites destination files. Caller is expected to inspect `git status` - * after sync to review local changes. Conflict handling lands in a follow-up. - */ -export const syncShared = Effect.fnUntraced(function*( - opts: { lockfilePath?: string; update?: boolean; ref?: string | undefined } = {} -) { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const { runGetExitCode } = yield* RunCommandService - - const lockfilePath = opts.lockfilePath ?? ".shared.json" - const { content: lockfileContent, lockfile: pinned } = yield* readLockfile(lockfilePath) - - yield* Effect.logInfo(`Syncing from ${pinned.repo} @ ${pinned.ref}`) - - const cachePath = yield* ensureCache(pinned) - yield* Effect.logInfo(`Cache: ${cachePath}`) - - let lockfile = pinned - if (opts.update) { - const newRef = yield* resolveLatestSha(cachePath, opts.ref) - if (newRef === pinned.ref) { - yield* Effect.logInfo(`Already at latest: ${newRef.slice(0, 12)}`) - } else { - yield* Effect.logInfo(`Updating ref ${pinned.ref.slice(0, 12)} → ${newRef.slice(0, 12)}`) - yield* runGetExitCode(`git checkout ${JSON.stringify(newRef)}`, cachePath) - } - lockfile = { ...pinned, ref: newRef } - } - - const files = yield* walkArtifacts(lockfile, cachePath) - - let copied = 0 - let skipped = 0 - - for (const { srcRel, srcAbs, destAbs, excluded } of files) { - if (excluded) { - yield* Effect.logInfo(` exclude ${srcRel}`) - skipped++ - continue - } - yield* runGetExitCode(`mkdir -p ${JSON.stringify(path.dirname(destAbs))}`) - yield* runGetExitCode(`cp ${JSON.stringify(srcAbs)} ${JSON.stringify(destAbs)}`) - copied++ - } - - const today = new Date().toISOString().slice(0, 10) - const trailingNewline = lockfileContent.endsWith("\n") ? "\n" : "" - const updated = { ...lockfile, synced_at: today } - yield* fs.writeFileString(lockfilePath, JSON.stringify(updated, null, 2) + trailingNewline) - - yield* Effect.logInfo(`Sync complete: ${copied} copied, ${skipped} excluded.`) - yield* Effect.logInfo("Review changes with: git status") -}) - -/** - * Compare project files to cache files. Reports each file as one of: - * M modified locally (project differs from cache) - * A added locally (project has file, cache does not — for tracked artifacts) - * D deleted locally (cache has file, project does not) - * E excluded (skipped by lockfile) - * - * Files that match cache exactly are not listed. - */ -export const syncDiff = Effect.fnUntraced(function*(opts: { lockfilePath?: string } = {}) { - const fs = yield* FileSystem.FileSystem - const { runGetString } = yield* RunCommandService - - const lockfilePath = opts.lockfilePath ?? ".shared.json" - const { lockfile } = yield* readLockfile(lockfilePath) - - yield* Effect.logInfo(`Diffing against ${lockfile.repo} @ ${lockfile.ref}`) - - const cachePath = yield* ensureCache(lockfile) - const files = yield* walkArtifacts(lockfile, cachePath) - - const changes: Array<{ kind: "M" | "D" | "E"; srcRel: string; destPath: string }> = [] - - for (const { srcRel, srcAbs, destAbs, excluded } of files) { - if (excluded) { - changes.push({ kind: "E", srcRel, destPath: destAbs }) - continue - } - - if (!(yield* fs.exists(destAbs))) { - changes.push({ kind: "D", srcRel, destPath: destAbs }) - continue - } - - const srcHash = (yield* runGetString(`sha256sum ${JSON.stringify(srcAbs)}`)).split(" ")[0] - const destHash = (yield* runGetString(`sha256sum ${JSON.stringify(destAbs)}`)).split(" ")[0] - - if (srcHash !== destHash) { - changes.push({ kind: "M", srcRel, destPath: destAbs }) - } - } - - if (changes.length === 0) { - yield* Effect.logInfo("In sync. No diff.") - return - } - - for (const { kind, srcRel, destPath } of changes) { - yield* Effect.logInfo(`${kind} ${srcRel} -> ${destPath}`) - } - yield* Effect.logInfo("") - yield* Effect.logInfo( - `Summary: ${changes.filter((c) => c.kind === "M").length} modified, ${ - changes - .filter((c) => c.kind === "D") - .length - } missing in project, ${changes.filter((c) => c.kind === "E").length} excluded.` - ) -}) - -/** - * Push locally-modified synced files back to the shared repo on a new branch. - * Optionally opens a PR via `gh pr create`. - * - * Workflow: - * 1. Ensure cache fresh at pinned ref. - * 2. Detect modified files via hash compare (project vs cache). - * 3. Create new branch in cache, copy modified files in, commit, push. - * 4. Optionally run `gh pr create` if `--pr` set. - */ -export const syncPush = Effect.fnUntraced(function*(opts: { - lockfilePath?: string | undefined - message?: string | undefined - branch?: string | undefined - pr?: boolean | undefined -} = {}) { - const { runGetExitCode, runGetString } = yield* RunCommandService - - const lockfilePath = opts.lockfilePath ?? ".shared.json" - const { lockfile } = yield* readLockfile(lockfilePath) - - const cachePath = yield* ensureCache(lockfile) - const files = yield* walkArtifacts(lockfile, cachePath) - - const fs = yield* FileSystem.FileSystem - const modified: Array<{ srcRel: string; srcAbs: string; destAbs: string }> = [] - const deleted: Array<{ srcRel: string; srcAbs: string }> = [] - - for (const { srcRel, srcAbs, destAbs, excluded } of files) { - if (excluded) continue - if (!(yield* fs.exists(destAbs))) { - deleted.push({ srcRel, srcAbs }) - continue - } - const srcHash = (yield* runGetString(`sha256sum ${JSON.stringify(srcAbs)}`)).split(" ")[0] - const destHash = (yield* runGetString(`sha256sum ${JSON.stringify(destAbs)}`)).split(" ")[0] - if (srcHash !== destHash) { - modified.push({ srcRel, srcAbs, destAbs }) - } - } - - if (modified.length === 0 && deleted.length === 0) { - yield* Effect.logInfo("No local modifications to push.") - return - } - - yield* Effect.logInfo(`Pushing ${modified.length} modified, ${deleted.length} deleted file(s):`) - for (const { srcRel } of modified) { - yield* Effect.logInfo(` M ${srcRel}`) - } - for (const { srcRel } of deleted) { - yield* Effect.logInfo(` D ${srcRel}`) - } - - const branch = opts.branch - ?? `sync/from-${sanitizeRepoSlug(process.cwd().split("/").pop() ?? "project")}-${ - new Date().toISOString().slice(0, 10) - }` - - const message = opts.message ?? "sync: propagate local edits" - - // Stash anything in cache (defensive), create branch from pinned ref. - yield* runGetExitCode(`git stash --include-untracked || true`, cachePath) - yield* runGetExitCode(`git checkout -B ${JSON.stringify(branch)} ${JSON.stringify(lockfile.ref)}`, cachePath) - - for (const { srcAbs, destAbs } of modified) { - yield* runGetExitCode(`cp ${JSON.stringify(destAbs)} ${JSON.stringify(srcAbs)}`) - } - for (const { srcAbs } of deleted) { - yield* runGetExitCode(`git rm -f ${JSON.stringify(srcAbs)}`, cachePath) - } - - yield* runGetExitCode(`git add -A`, cachePath) - yield* runGetExitCode( - `git -c commit.gpgsign=false commit -m ${JSON.stringify(message)}`, - cachePath - ) - yield* runGetExitCode(`git push -u origin ${JSON.stringify(branch)}`, cachePath) - - if (opts.pr) { - yield* runGetExitCode( - `gh pr create --title ${JSON.stringify(message)} --body ${ - JSON.stringify(`Propagated from project at ${process.cwd()}.\n\nFiles:\n${ - [ - ...modified.map((m) => `- M ${m.srcRel}`), - ...deleted.map((d) => `- D ${d.srcRel}`) - ] - .join("\n") - }`) - } --head ${JSON.stringify(branch)}`, - cachePath - ) - } - - yield* Effect.logInfo(`Pushed branch ${branch} to shared repo.`) - yield* Effect.logInfo(opts.pr ? "PR opened." : "Open a PR with: gh pr create (from the cache dir)") -}) diff --git a/packages/cli/src/update-packages.ts b/packages/cli/src/update-packages.ts deleted file mode 100644 index ae2c84386..000000000 --- a/packages/cli/src/update-packages.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const makeNcuUpdateCommand = ( - filter: string, - options?: { readonly recursive?: boolean } -) => `pnpm ${options?.recursive ? "-r " : ""}exec ncu -u --target semver --filter "${filter}"` diff --git a/packages/cli/test.gists.yaml b/packages/cli/test.gists.yaml deleted file mode 100644 index 41b4b992b..000000000 --- a/packages/cli/test.gists.yaml +++ /dev/null @@ -1,27 +0,0 @@ -gists: - # "cli-config-banana": - # company: "banana" # acts like a namespace - # description: "Package configuration and main source for Effect-App CLI" - # public: false - # files: - # - "package.json" - # # - "packages/cli/src/index.ts" - "cli-config-mela": - company: "mela" # acts like a namespace - description: "Package configuration and main source for Effect-App CLI" - public: false - files: - - "package.json" - - # "test-files": - # description: "Test YAML files for gist functionality" - # public: false - # files: - # - "gists.yaml" - -settings: - # GitHub token environment variable name - token_env: "GIST_GITHUB_TOKEN" - - # Base directory for files (relative to where the CLI is run) - base_directory: "." diff --git a/packages/cli/test/argv-patch.test.ts b/packages/cli/test/argv-patch.test.ts deleted file mode 100644 index 11c0ea30c..000000000 --- a/packages/cli/test/argv-patch.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { execFileSync } from "node:child_process" -import path from "node:path" -import url from "node:url" -import { describe, expect, it } from "vitest" -import { patchArgvForWrapCommands } from "../src/argv-patch.js" - -describe("patchArgvForWrapCommands", () => { - const make = (...args: Array) => ["node", "effect-app-cli", ...args] - - describe("joins wrap args into a single element", () => { - it("index-multi with tsc --build", () => { - const argv = make("index-multi", "tsc", "--build") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("index-multi", "tsc --build")) - }) - - it("packagejson with tsc --build and tsconfig path", () => { - const argv = make("packagejson", "tsc", "--build", "./tsconfig.src.json") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("packagejson", "tsc --build ./tsconfig.src.json")) - }) - - it("packagejson-packages with pnpm check", () => { - const argv = make("packagejson-packages", "pnpm", "check") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("packagejson-packages", "pnpm check")) - }) - - it("single wrap arg stays as-is", () => { - const argv = make("index-multi", "tsc") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("index-multi", "tsc")) - }) - - it("wrap args with multiple --flags", () => { - const argv = make("index-multi", "tsc", "--build", "--watch", "--verbose") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("index-multi", "tsc --build --watch --verbose")) - }) - - it("wrap args with short flags", () => { - const argv = make("packagejson", "pnpm", "-r", "check") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("packagejson", "pnpm -r check")) - }) - - it("wrap args with --flag=value syntax", () => { - const argv = make("index-multi", "tsc", "--build=./tsconfig.json") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("index-multi", "tsc --build=./tsconfig.json")) - }) - - it("wrap args with --flag=\"quoted value\" syntax", () => { - const argv = make("index-multi", "tsc", "--outDir=\"dist/build\"") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("index-multi", "tsc --outDir=\"dist/build\"")) - }) - - it("wrap args with mixed quoted and unquoted flags", () => { - const argv = make("packagejson", "cmd", "--x=\"abc\"", "--y", "plain") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("packagejson", "cmd --x=\"abc\" --y plain")) - }) - - it("wrap args with single-quoted value in flag", () => { - const argv = make("index-multi", "cmd", "--config='my config.json'") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("index-multi", "cmd --config='my config.json'")) - }) - - it("wrap args with spaces inside quoted flag value", () => { - const argv = make("packagejson-packages", "cmd", "--msg=\"hello world\"", "--verbose") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("packagejson-packages", "cmd --msg=\"hello world\" --verbose")) - }) - }) - - describe("does nothing for non-wrap subcommands", () => { - it("nuke with flags", () => { - const argv = make("nuke", "--dry-run") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("nuke", "--dry-run")) - }) - - it("gist with --config flag", () => { - const argv = make("gist", "--config", "gists.yaml") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("gist", "--config", "gists.yaml")) - }) - - it("ue", () => { - const argv = make("ue") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("ue")) - }) - - it("link with path arg", () => { - const argv = make("link", "../../effect-app/libs") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("link", "../../effect-app/libs")) - }) - - it("unrecognized command", () => { - const argv = make("unknown", "tsc", "--build") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("unknown", "tsc", "--build")) - }) - }) - - describe("edge cases", () => { - it("no subcommand", () => { - const argv = ["node", "effect-app-cli"] - patchArgvForWrapCommands(argv) - expect(argv).toEqual(["node", "effect-app-cli"]) - }) - - it("subcommand with no trailing args", () => { - const argv = make("index-multi") - patchArgvForWrapCommands(argv) - expect(argv).toEqual(make("index-multi")) - }) - - it("empty argv", () => { - const argv: Array = [] - patchArgvForWrapCommands(argv) - expect(argv).toEqual([]) - }) - - it("single element argv", () => { - const argv = ["node"] - patchArgvForWrapCommands(argv) - expect(argv).toEqual(["node"]) - }) - - it("mutates the original array", () => { - const argv = make("index-multi", "tsc", "--build") - const ref = argv - patchArgvForWrapCommands(argv) - expect(ref).toBe(argv) - expect(ref).toEqual(make("index-multi", "tsc --build")) - }) - - it("different node/script paths", () => { - const argv = ["/usr/local/bin/node", "/home/user/.npm/bin/effa", "packagejson", "pnpm", "check"] - patchArgvForWrapCommands(argv) - expect(argv).toEqual(["/usr/local/bin/node", "/home/user/.npm/bin/effa", "packagejson", "pnpm check"]) - }) - }) -}) - -describe.skip("e2e: CLI spawns wrap command correctly", () => { - const binPath = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "../bin.js") - - const run = (...args: Array) => - execFileSync("node", [binPath, ...args], { - encoding: "utf-8", - timeout: 10_000, - env: { ...process.env, NO_COLOR: "1" } - }) - - it("packagejson spawns 'echo hello' and captures output", () => { - const out = run("packagejson", "echo", "hello") - expect(out).toContain("Spawning child command: echo hello") - expect(out).toContain("hello") - }) - - it("packagejson spawns command with --flags without quoting", () => { - const out = run("packagejson", "echo", "--build-test", "extra") - expect(out).toContain("Spawning child command: echo --build-test extra") - expect(out).toContain("--build-test extra") - }) - - it("packagejson with single quoted arg still works", () => { - const out = run("packagejson", "echo from-quoted") - expect(out).toContain("Spawning child command: echo from-quoted") - expect(out).toContain("from-quoted") - }) - - it("propagates non-zero exit code", () => { - try { - run("packagejson", "exit 42") - expect.unreachable("should have thrown") - } catch (e: any) { - expect(e.status).toBe(42) - } - }) - - it("index-multi spawns wrap command with --flags", () => { - const out = run("index-multi", "echo", "--build", "done") - expect(out).toContain("Spawning child command: echo --build done") - expect(out).toContain("--build done") - }) - - it("packagejson spawns command with --flag=\"value\" syntax", () => { - const out = run("packagejson", "echo", "--x=\"abc\"", "--y") - expect(out).toContain("Spawning child command: echo --x=\"abc\" --y") - }) -}) diff --git a/packages/cli/test/gist-cache.test.ts b/packages/cli/test/gist-cache.test.ts deleted file mode 100644 index 902e71424..000000000 --- a/packages/cli/test/gist-cache.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Effect, Layer } from "effect" -import { describe, expect, it } from "vitest" -import { GHGistService } from "../src/gist.js" -import { RunCommandService } from "../src/os-command.js" - -describe("GHGistService.loadGistCache", () => { - it("loads the company cache from the configured Gist ID without discovery", async () => { - const commands: Array = [] - const runCommand = Layer.succeed(RunCommandService)({ - runGetExitCode: (command) => { - commands.push(command) - return Effect.die(`Unexpected command: ${command}`) - }, - runGetString: (command) => { - commands.push(command) - - switch (command) { - case "gh gist view configured-cache-id --files": - return Effect.succeed("effa-gist.cache\nmako.json\n") - case "gh gist view configured-cache-id -f \"mako.json\"": - return Effect.succeed("[{\"name\":\"mako\",\"id\":\"content-gist-id\"}]") - default: - return Effect.die(`Unexpected command: ${command}`) - } - } - }) - - const cache = await Effect.runPromise( - Effect - .gen(function*() { - const gh = yield* GHGistService - return yield* gh.loadGistCache("configured-cache-id", "mako") - }) - .pipe( - Effect.provide(GHGistService.DefaultWithoutDependencies), - Effect.provide(runCommand) - ) - ) - - expect(cache.gist_id).toBe("configured-cache-id") - expect(cache.company).toBe("mako") - expect(cache.entries.map(({ id, name }) => ({ id, name }))).toEqual([ - { id: "content-gist-id", name: "mako" } - ]) - expect(commands).toEqual([ - "gh gist view configured-cache-id --files", - "gh gist view configured-cache-id -f \"mako.json\"" - ]) - }) - - it("creates a missing company cache file once without retrying", async () => { - const commands: Array = [] - const runCommand = Layer.succeed(RunCommandService)({ - runGetExitCode: (command) => { - commands.push(command) - return Effect.die(`Unexpected command: ${command}`) - }, - runGetString: (command) => { - commands.push(command) - - switch (command) { - case "gh gist view configured-cache-id --files": - return Effect.succeed("effa-gist.cache\n") - case "echo \"[]\" | gh gist edit configured-cache-id -a mako.json -": - return Effect.succeed("") - default: - return Effect.die(`Unexpected command: ${command}`) - } - } - }) - - const cache = await Effect.runPromise( - Effect - .gen(function*() { - const gh = yield* GHGistService - return yield* gh.loadGistCache("configured-cache-id", "mako") - }) - .pipe( - Effect.provide(GHGistService.DefaultWithoutDependencies), - Effect.provide(runCommand) - ) - ) - - expect(cache.gist_id).toBe("configured-cache-id") - expect(cache.company).toBe("mako") - expect(cache.entries).toEqual([]) - expect(commands).toEqual([ - "gh gist view configured-cache-id --files", - "echo \"[]\" | gh gist edit configured-cache-id -a mako.json -" - ]) - }) -}) diff --git a/packages/cli/test/update-packages.test.ts b/packages/cli/test/update-packages.test.ts deleted file mode 100644 index 9cca50be2..000000000 --- a/packages/cli/test/update-packages.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, expect, it } from "vitest" -import { makeNcuUpdateCommand } from "../src/update-packages.js" - -describe("makeNcuUpdateCommand", () => { - it("targets the highest version allowed by the existing semver range", () => { - expect(makeNcuUpdateCommand("@effect-app/*")).toBe( - "pnpm exec ncu -u --target semver --filter \"@effect-app/*\"" - ) - expect(makeNcuUpdateCommand("@effect/*", { recursive: true })).toBe( - "pnpm -r exec ncu -u --target semver --filter \"@effect/*\"" - ) - }) -}) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json deleted file mode 100644 index 6326ef26c..000000000 --- a/packages/cli/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "lib": [ - "esnext" - ], - "moduleResolution": "Node16", - "outDir": "./dist", - "rootDir": "./src", - "verbatimModuleSyntax": true, - "tsBuildInfoFile": "./dist/.tsbuildinfo" - }, - "include": [ - "./src/**/*.ts" - ], - "references": [] -} diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts deleted file mode 100644 index 85903fa64..000000000 --- a/packages/cli/vitest.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -import { defineConfig } from "vitest/config" -import makeConfig from "../../vite.config.base" - -export default defineConfig(makeConfig(__dirname)) diff --git a/packages/e2e/CHANGELOG.md b/packages/e2e/CHANGELOG.md deleted file mode 100644 index 68c9cc0c2..000000000 --- a/packages/e2e/CHANGELOG.md +++ /dev/null @@ -1,891 +0,0 @@ -# @effect-app/e2e - -## 4.0.2 - -### Patch Changes - -- Updated dependencies [76911ed] -- Updated dependencies [7a99d1b] - - effect-app@4.0.2 - - @effect-app/vue@4.0.2 - - @effect-app/infra@4.0.2 - -## 4.0.1 - -### Patch Changes - -- Updated dependencies [222e619] - - effect-app@4.0.1 - - @effect-app/infra@4.0.1 - - @effect-app/vue@4.0.1 - -## 4.0.0 - -### Patch Changes - -- Updated dependencies [eddda2e] -- Updated dependencies [07dd7b9] -- Updated dependencies [439cbeb] -- Updated dependencies [da83c1f] -- Updated dependencies [b2df3fa] -- Updated dependencies [199e9a5] -- Updated dependencies [b035b1c] -- Updated dependencies [664e83d] -- Updated dependencies [abb9c99] -- Updated dependencies [71a0719] -- Updated dependencies [aa436d3] -- Updated dependencies [a4dff57] -- Updated dependencies [ba4bdc3] -- Updated dependencies [52b0b01] -- Updated dependencies [52b0b01] -- Updated dependencies [eaf0115] -- Updated dependencies [eceb3a3] -- Updated dependencies [9d3495e] -- Updated dependencies [3436d44] -- Updated dependencies [52b0b01] -- Updated dependencies [f317c5e] -- Updated dependencies [e585c9c] -- Updated dependencies [e4ff9a6] -- Updated dependencies [f4b58cd] -- Updated dependencies [33b0544] -- Updated dependencies [6821fc5] -- Updated dependencies [38289e0] -- Updated dependencies [99c43c4] -- Updated dependencies [52b0b01] -- Updated dependencies [08d092c] -- Updated dependencies [947fe20] -- Updated dependencies [1f9d7da] -- Updated dependencies [21ac90a] -- Updated dependencies [a74a894] -- Updated dependencies [d1fc90a] -- Updated dependencies [32f05c8] -- Updated dependencies [50ce7e6] -- Updated dependencies [6cfd83d] -- Updated dependencies [1c858d3] -- Updated dependencies [52b0b01] -- Updated dependencies [6a3d364] -- Updated dependencies [6884a32] -- Updated dependencies [aa5ef5c] -- Updated dependencies [82f66f1] -- Updated dependencies [08d30af] -- Updated dependencies [8ff0bf9] -- Updated dependencies [992d9fa] -- Updated dependencies [fb93244] -- Updated dependencies [aac6308] -- Updated dependencies [505bfa9] -- Updated dependencies [939bebc] -- Updated dependencies [2b4c324] -- Updated dependencies [fd67f0b] -- Updated dependencies [ba789a2] -- Updated dependencies [ab289d4] -- Updated dependencies [c31c673] -- Updated dependencies [bac9f1a] -- Updated dependencies [3eb66e9] -- Updated dependencies [5aed2e3] -- Updated dependencies [48f1457] -- Updated dependencies [de9fb83] -- Updated dependencies [0520dd4] -- Updated dependencies [13c2fd9] -- Updated dependencies [52b0b01] -- Updated dependencies [fdb9cb3] -- Updated dependencies [c5e348f] -- Updated dependencies [99a2e9b] -- Updated dependencies [8eb6737] -- Updated dependencies [4a7d95a] -- Updated dependencies [e0c4835] -- Updated dependencies [27bf9b6] -- Updated dependencies [8753c52] -- Updated dependencies [32dbc54] -- Updated dependencies [0a0030f] -- Updated dependencies [0263827] -- Updated dependencies [0263827] -- Updated dependencies [d23e3f6] -- Updated dependencies [aeb17bc] -- Updated dependencies [6613f3d] -- Updated dependencies [c0e5a1b] -- Updated dependencies [10b55ff] -- Updated dependencies [14aba14] -- Updated dependencies [a0075b8] -- Updated dependencies [f052d38] -- Updated dependencies [f233f3d] -- Updated dependencies [18bae5b] -- Updated dependencies [f313973] -- Updated dependencies [edc52e4] -- Updated dependencies [ea1bd46] -- Updated dependencies [85a8275] -- Updated dependencies [50b022e] -- Updated dependencies [52b0b01] -- Updated dependencies [47e3742] -- Updated dependencies [52b0b01] -- Updated dependencies [4149577] -- Updated dependencies [04fc985] -- Updated dependencies [458bb1b] -- Updated dependencies [3365758] -- Updated dependencies [54ec1ef] -- Updated dependencies [f21190c] -- Updated dependencies [1176240] -- Updated dependencies [3cf0c42] -- Updated dependencies [b6cb25b] -- Updated dependencies [fce6ee9] -- Updated dependencies [65835a6] -- Updated dependencies [be3300d] -- Updated dependencies [0d4e0b8] -- Updated dependencies [31739d7] -- Updated dependencies [0b21a02] -- Updated dependencies [eed57c6] -- Updated dependencies [bbaa67e] -- Updated dependencies [1015582] -- Updated dependencies [a211c12] -- Updated dependencies [ebffba8] -- Updated dependencies [29c39d2] -- Updated dependencies [fbf47b8] -- Updated dependencies [47e53f5] -- Updated dependencies [347af48] -- Updated dependencies [70f1f27] -- Updated dependencies [d786b91] -- Updated dependencies [dfd5562] -- Updated dependencies [2d85646] -- Updated dependencies [2f862a4] -- Updated dependencies [c80e781] -- Updated dependencies [fe2caba] -- Updated dependencies [9a9e46c] -- Updated dependencies [5a709d1] -- Updated dependencies [b4ffee0] -- Updated dependencies [1ab3cf7] -- Updated dependencies [d195003] -- Updated dependencies [7519318] -- Updated dependencies [1b38043] -- Updated dependencies [07a57b6] -- Updated dependencies [bd26832] -- Updated dependencies [3053760] -- Updated dependencies [f28fce5] -- Updated dependencies [c9e0c44] -- Updated dependencies [52b0b01] -- Updated dependencies [55c6572] -- Updated dependencies [f88ea34] -- Updated dependencies [dd239fa] -- Updated dependencies [66fd718] -- Updated dependencies [52b0b01] -- Updated dependencies [c991be1] -- Updated dependencies [d738811] -- Updated dependencies [702d51c] -- Updated dependencies [fc41dcf] -- Updated dependencies [8fffc3c] -- Updated dependencies [52b0b01] -- Updated dependencies [34f6a97] -- Updated dependencies [a788432] -- Updated dependencies [10e90d5] -- Updated dependencies [4b95009] -- Updated dependencies [52b0b01] -- Updated dependencies [178480a] -- Updated dependencies [2e4c018] -- Updated dependencies [e02a258] -- Updated dependencies [c7fbd58] -- Updated dependencies [985176b] -- Updated dependencies [50d7fc1] -- Updated dependencies [f7290aa] -- Updated dependencies [2871841] -- Updated dependencies [dc465e3] -- Updated dependencies [b4d5f55] -- Updated dependencies [f44800c] -- Updated dependencies [a0075b8] -- Updated dependencies [f052d38] -- Updated dependencies [186de3a] -- Updated dependencies [21017d5] -- Updated dependencies [0541f0d] -- Updated dependencies [2ebf8ae] -- Updated dependencies [8c645d5] -- Updated dependencies [8cb3de4] -- Updated dependencies [30c512d] -- Updated dependencies [50b022e] -- Updated dependencies [52b0b01] -- Updated dependencies [b952f19] -- Updated dependencies [a37aa38] -- Updated dependencies [99c43c4] -- Updated dependencies [52b0b01] -- Updated dependencies [28a0b29] -- Updated dependencies [c1e73de] -- Updated dependencies [d867272] -- Updated dependencies [1b57aa4] -- Updated dependencies [24f5dd0] -- Updated dependencies [0c42d67] -- Updated dependencies [774a9b3] -- Updated dependencies [d67d17a] -- Updated dependencies [8f09f77] -- Updated dependencies [50ce7e6] -- Updated dependencies [d71d976] -- Updated dependencies [2aa8e5e] -- Updated dependencies [ed1b8a9] -- Updated dependencies [29a1e57] -- Updated dependencies [186de3a] -- Updated dependencies [52b0b01] -- Updated dependencies [e3ceaba] -- Updated dependencies [52b0b01] -- Updated dependencies [6fff09c] -- Updated dependencies [dab6992] -- Updated dependencies [11422f8] -- Updated dependencies [eb28ea5] -- Updated dependencies [52b0b01] -- Updated dependencies [7d5cefc] -- Updated dependencies [8f1cf6a] -- Updated dependencies [ee9694e] -- Updated dependencies [52a31dd] -- Updated dependencies [3a588e4] -- Updated dependencies [848d26d] -- Updated dependencies [db285e9] -- Updated dependencies [52b0b01] -- Updated dependencies [ca94edf] -- Updated dependencies [d1c15d3] -- Updated dependencies [4bc4a27] -- Updated dependencies [a1b59bc] -- Updated dependencies [dc07df5] -- Updated dependencies [0fe925d] -- Updated dependencies [40585ca] -- Updated dependencies [5ac46cb] -- Updated dependencies [a354345] -- Updated dependencies [52b0b01] -- Updated dependencies [1cee480] -- Updated dependencies [186de3a] -- Updated dependencies [1e4c989] -- Updated dependencies [aeb17bc] -- Updated dependencies [48d9f36] -- Updated dependencies [52b0b01] -- Updated dependencies [52b0b01] -- Updated dependencies [d31253f] -- Updated dependencies [5615e47] -- Updated dependencies [3e46e7b] -- Updated dependencies [18fd1df] -- Updated dependencies [cec026d] -- Updated dependencies [902ca1b] -- Updated dependencies [4622a75] -- Updated dependencies [925214b] -- Updated dependencies [95995d5] -- Updated dependencies [7191fc1] -- Updated dependencies [88838fb] -- Updated dependencies [b3ed68a] -- Updated dependencies [f16e766] -- Updated dependencies [3bae238] -- Updated dependencies [b2e438f] -- Updated dependencies [f150cf9] -- Updated dependencies [0c88f78] -- Updated dependencies [d16845e] -- Updated dependencies [261470f] -- Updated dependencies [beae3a0] -- Updated dependencies [ddd9505] -- Updated dependencies [8792221] -- Updated dependencies [1f103b2] -- Updated dependencies [0054611] -- Updated dependencies [4bc3d05] -- Updated dependencies [ebbb799] -- Updated dependencies [5773159] -- Updated dependencies [ddc5492] -- Updated dependencies [7bd8234] -- Updated dependencies [54bfc59] -- Updated dependencies [3d732b1] -- Updated dependencies [ad0ede6] -- Updated dependencies [818e047] -- Updated dependencies [89d8b3a] -- Updated dependencies [52b0b01] -- Updated dependencies [08d2e70] -- Updated dependencies [9ea024d] -- Updated dependencies [5f9cd6a] -- Updated dependencies [6252808] -- Updated dependencies [e6f2341] -- Updated dependencies [28777c1] -- Updated dependencies [1576688] -- Updated dependencies [821468d] -- Updated dependencies [2495ace] -- Updated dependencies [e33f1eb] -- Updated dependencies [0b3e00e] -- Updated dependencies [52b0b01] -- Updated dependencies [c215db8] -- Updated dependencies [12abb55] -- Updated dependencies [0cff7c1] -- Updated dependencies [ba61aad] -- Updated dependencies [c1a6fdc] -- Updated dependencies [8ae8b53] -- Updated dependencies [a5248a9] -- Updated dependencies [37089ea] -- Updated dependencies [0e824ef] -- Updated dependencies [62627e9] -- Updated dependencies [52b0b01] -- Updated dependencies [eb06b32] -- Updated dependencies [52b0b01] -- Updated dependencies [52b0b01] -- Updated dependencies [52b0b01] -- Updated dependencies [dd73a4a] -- Updated dependencies [8a4a6d2] -- Updated dependencies [dba5779] -- Updated dependencies [025de47] -- Updated dependencies [0fa0e80] -- Updated dependencies [acfdc9e] -- Updated dependencies [1186b09] -- Updated dependencies [9be71e1] -- Updated dependencies [48f1457] -- Updated dependencies [48f1457] -- Updated dependencies [48f1457] -- Updated dependencies [c313c07] -- Updated dependencies [0ddda6f] -- Updated dependencies [bb3f51d] -- Updated dependencies [db7ba34] -- Updated dependencies [a69da09] -- Updated dependencies [3c1f52d] -- Updated dependencies [3a8c710] -- Updated dependencies [f22a026] -- Updated dependencies [186de3a] -- Updated dependencies [1d85785] -- Updated dependencies [b9586f8] -- Updated dependencies [0054611] -- Updated dependencies [9569911] -- Updated dependencies [583393f] -- Updated dependencies [828d264] -- Updated dependencies [61a3931] -- Updated dependencies [8ff0bf9] -- Updated dependencies [7fa3045] -- Updated dependencies [3dc0d2a] -- Updated dependencies [d4bf24a] -- Updated dependencies [b0db40f] -- Updated dependencies [459697f] -- Updated dependencies [738b482] -- Updated dependencies [3eda52e] -- Updated dependencies [fa3533e] -- Updated dependencies [b53c59e] -- Updated dependencies [b52b424] -- Updated dependencies [7c25dbb] -- Updated dependencies [b8b9080] -- Updated dependencies [b90fa30] -- Updated dependencies [57db551] -- Updated dependencies [3613e87] -- Updated dependencies [52b0b01] -- Updated dependencies [7119320] -- Updated dependencies [e944bca] -- Updated dependencies [ffbff5d] -- Updated dependencies [dbcc53b] -- Updated dependencies [18b915f] -- Updated dependencies [74e6d40] -- Updated dependencies [b63f10f] -- Updated dependencies [52b0b01] -- Updated dependencies [6a0e008] -- Updated dependencies [140e192] -- Updated dependencies [9992e70] -- Updated dependencies [43611c7] -- Updated dependencies [24f0a5a] -- Updated dependencies [256ae85] -- Updated dependencies [ac62e48] -- Updated dependencies [1587bfc] -- Updated dependencies [8bd9a11] -- Updated dependencies [52b0b01] -- Updated dependencies [fac725d] -- Updated dependencies [3200fa8] -- Updated dependencies [412f08b] -- Updated dependencies [79eb019] -- Updated dependencies [6ae3050] -- Updated dependencies [f353d48] -- Updated dependencies [0d7d197] -- Updated dependencies [52b0b01] -- Updated dependencies [01bab22] -- Updated dependencies [2a86a17] -- Updated dependencies [52b0b01] -- Updated dependencies [c3299f7] -- Updated dependencies [23a7167] -- Updated dependencies [e71eb78] -- Updated dependencies [8062ad3] -- Updated dependencies [39996f7] -- Updated dependencies [6b57330] -- Updated dependencies [e2d00b5] -- Updated dependencies [547e2e1] -- Updated dependencies [52b0b01] -- Updated dependencies -- Updated dependencies [ea32222] -- Updated dependencies [7ca66ce] -- Updated dependencies [57a1862] -- Updated dependencies [3e855bc] -- Updated dependencies [78d076a] -- Updated dependencies [ffd140f] -- Updated dependencies [e8842aa] -- Updated dependencies [b6dda09] -- Updated dependencies [0263827] -- Updated dependencies [8bd5bfe] -- Updated dependencies [2231ef8] -- Updated dependencies [724da29] -- Updated dependencies [413022d] -- Updated dependencies [f052d38] -- Updated dependencies [fc98fb7] -- Updated dependencies [1df5cf5] -- Updated dependencies [b241ae5] -- Updated dependencies [677821a] -- Updated dependencies [c7bbc41] -- Updated dependencies [52b0b01] -- Updated dependencies [52b0b01] -- Updated dependencies [7328c76] -- Updated dependencies [52b0b01] -- Updated dependencies [7fd35e4] -- Updated dependencies [89b7d2f] -- Updated dependencies [52b0b01] -- Updated dependencies [4bbeb19] - - @effect-app/vue@4.0.0 - - effect-app@4.0.0 - - @effect-app/infra@4.0.0 - -## 4.0.0-beta.333 - -### Patch Changes - -- Updated dependencies [e2d00b5] - - effect-app@4.0.0-beta.333 - - @effect-app/infra@4.0.0-beta.333 - - @effect-app/vue@4.0.0-beta.333 - -## 4.0.0-beta.332 - -### Patch Changes - -- Updated dependencies [6b57330] - - effect-app@4.0.0-beta.332 - - @effect-app/infra@4.0.0-beta.332 - - @effect-app/vue@4.0.0-beta.332 - -## 4.0.0-beta.331 - -### Patch Changes - -- Updated dependencies [65835a6] - - @effect-app/infra@4.0.0-beta.331 - - effect-app@4.0.0-beta.331 - - @effect-app/vue@4.0.0-beta.331 - -## 4.0.0-beta.330 - -### Patch Changes - -- Updated dependencies [48d9f36] - - effect-app@4.0.0-beta.330 - - @effect-app/infra@4.0.0-beta.330 - - @effect-app/vue@4.0.0-beta.330 - -## 4.0.0-beta.329 - -### Patch Changes - -- Updated dependencies [459697f] - - effect-app@4.0.0-beta.329 - - @effect-app/infra@4.0.0-beta.329 - - @effect-app/vue@4.0.0-beta.329 - -## 4.0.0-beta.328 - -### Patch Changes - -- Updated dependencies [3a8c710] - - effect-app@4.0.0-beta.328 - - @effect-app/infra@4.0.0-beta.328 - - @effect-app/vue@4.0.0-beta.328 - -## 4.0.0-beta.327 - -### Patch Changes - -- Updated dependencies [ebffba8] - - @effect-app/infra@4.0.0-beta.327 - - effect-app@4.0.0-beta.327 - - @effect-app/vue@4.0.0-beta.327 - -## 4.0.0-beta.326 - -### Patch Changes - -- Updated dependencies [2ebf8ae] - - effect-app@4.0.0-beta.326 - - @effect-app/infra@4.0.0-beta.326 - - @effect-app/vue@4.0.0-beta.326 - -## 4.0.0-beta.325 - -### Patch Changes - -- Updated dependencies [f233f3d] - - effect-app@4.0.0-beta.325 - - @effect-app/infra@4.0.0-beta.325 - - @effect-app/vue@4.0.0-beta.325 - -## 4.0.0-beta.324 - -### Patch Changes - -- Updated dependencies [a0075b8] -- Updated dependencies [a0075b8] - - effect-app@4.0.0-beta.324 - - @effect-app/infra@4.0.0-beta.324 - - @effect-app/vue@4.0.0-beta.324 - -## 4.0.0-beta.323 - -### Patch Changes - -- Updated dependencies [ab289d4] - - effect-app@4.0.0-beta.323 - - @effect-app/infra@4.0.0-beta.323 - - @effect-app/vue@4.0.0-beta.323 - -## 4.0.0-beta.322 - -### Patch Changes - -- Updated dependencies [2231ef8] - - effect-app@4.0.0-beta.322 - - @effect-app/infra@4.0.0-beta.322 - - @effect-app/vue@4.0.0-beta.322 - -## 4.0.0-beta.321 - -### Patch Changes - -- Updated dependencies [18bae5b] - - effect-app@4.0.0-beta.321 - - @effect-app/infra@4.0.0-beta.321 - - @effect-app/vue@4.0.0-beta.321 - -## 4.0.0-beta.320 - -### Patch Changes - -- effect-app@4.0.0-beta.320 -- @effect-app/infra@4.0.0-beta.320 -- @effect-app/vue@4.0.0-beta.320 - -## 4.0.0-beta.319 - -### Patch Changes - -- effect-app@4.0.0-beta.319 -- @effect-app/infra@4.0.0-beta.319 -- @effect-app/vue@4.0.0-beta.319 - -## 4.0.0-beta.318 - -### Patch Changes - -- Updated dependencies [8bd5bfe] - - effect-app@4.0.0-beta.318 - - @effect-app/infra@4.0.0-beta.318 - - @effect-app/vue@4.0.0-beta.318 - -## 4.0.0-beta.317 - -### Patch Changes - -- Updated dependencies [186de3a] -- Updated dependencies [186de3a] -- Updated dependencies [186de3a] -- Updated dependencies [186de3a] - - @effect-app/infra@4.0.0-beta.317 - - effect-app@4.0.0-beta.317 - - @effect-app/vue@4.0.0-beta.317 - -## 4.0.0-beta.316 - -### Patch Changes - -- effect-app@4.0.0-beta.316 -- @effect-app/infra@4.0.0-beta.316 -- @effect-app/vue@4.0.0-beta.316 - -## 4.0.0-beta.315 - -### Patch Changes - -- effect-app@4.0.0-beta.315 -- @effect-app/infra@4.0.0-beta.315 -- @effect-app/vue@4.0.0-beta.315 - -## 4.0.0-beta.314 - -### Patch Changes - -- Updated dependencies [61a3931] - - @effect-app/vue@4.0.0-beta.314 - - effect-app@4.0.0-beta.314 - - @effect-app/infra@4.0.0-beta.314 - -## 4.0.0-beta.313 - -### Patch Changes - -- Updated dependencies [eddda2e] - - @effect-app/vue@4.0.0-beta.313 - - effect-app@4.0.0-beta.313 - - @effect-app/infra@4.0.0-beta.313 - -## 4.0.0-beta.312 - -### Patch Changes - -- Updated dependencies [9ea024d] - - effect-app@4.0.0-beta.312 - - @effect-app/infra@4.0.0-beta.312 - - @effect-app/vue@4.0.0-beta.312 - -## 4.0.0-beta.311 - -### Patch Changes - -- Updated dependencies [547e2e1] - - effect-app@4.0.0-beta.311 - - @effect-app/infra@4.0.0-beta.311 - - @effect-app/vue@4.0.0-beta.311 - -## 4.0.0-beta.310 - -### Patch Changes - -- Updated dependencies [0fa0e80] - - @effect-app/vue@4.0.0-beta.310 - - effect-app@4.0.0-beta.310 - - @effect-app/infra@4.0.0-beta.310 - -## 4.0.0-beta.309 - -### Patch Changes - -- Updated dependencies [99c43c4] -- Updated dependencies [99c43c4] - - effect-app@4.0.0-beta.309 - - @effect-app/vue@4.0.0-beta.309 - - @effect-app/infra@4.0.0-beta.309 - -## 4.0.0-beta.308 - -### Patch Changes - -- Updated dependencies [b4d5f55] - - @effect-app/infra@4.0.0-beta.308 - - effect-app@4.0.0-beta.308 - - @effect-app/vue@4.0.0-beta.308 - -## 4.0.0-beta.307 - -### Patch Changes - -- Updated dependencies [40585ca] -- Updated dependencies [ad0ede6] - - effect-app@4.0.0-beta.307 - - @effect-app/infra@4.0.0-beta.307 - - @effect-app/vue@4.0.0-beta.307 - -## 4.0.0-beta.306 - -### Patch Changes - -- Updated dependencies [0fe925d] - - effect-app@4.0.0-beta.306 - - @effect-app/infra@4.0.0-beta.306 - - @effect-app/vue@4.0.0-beta.306 - -## 4.0.0-beta.305 - -### Patch Changes - -- Updated dependencies [8bd9a11] - - effect-app@4.0.0-beta.305 - - @effect-app/infra@4.0.0-beta.305 - - @effect-app/vue@4.0.0-beta.305 - -## 4.0.0-beta.304 - -### Patch Changes - -- Updated dependencies [412f08b] - - effect-app@4.0.0-beta.304 - - @effect-app/infra@4.0.0-beta.304 - - @effect-app/vue@4.0.0-beta.304 - -## 4.0.0-beta.303 - -### Patch Changes - -- Updated dependencies [33b0544] - - effect-app@4.0.0-beta.303 - - @effect-app/infra@4.0.0-beta.303 - - @effect-app/vue@4.0.0-beta.303 - -## 4.0.0-beta.302 - -### Patch Changes - -- Updated dependencies [c0e5a1b] - - effect-app@4.0.0-beta.302 - - @effect-app/infra@4.0.0-beta.302 - - @effect-app/vue@4.0.0-beta.302 - -## 4.0.0-beta.301 - -### Patch Changes - -- Updated dependencies [413022d] - - @effect-app/vue@4.0.0-beta.301 - - effect-app@4.0.0-beta.301 - - @effect-app/infra@4.0.0-beta.301 - -## 4.0.0-beta.300 - -### Patch Changes - -- effect-app@4.0.0-beta.300 -- @effect-app/infra@4.0.0-beta.300 -- @effect-app/vue@4.0.0-beta.300 - -## 4.0.0-beta.299 - -### Patch Changes - -- effect-app@4.0.0-beta.299 -- @effect-app/infra@4.0.0-beta.299 -- @effect-app/vue@4.0.0-beta.299 - -## 4.0.0-beta.298 - -### Patch Changes - -- Updated dependencies [08d092c] - - effect-app@4.0.0-beta.298 - - @effect-app/vue@4.0.0-beta.298 - - @effect-app/infra@4.0.0-beta.298 - -## 4.0.0-beta.297 - -### Patch Changes - -- Updated dependencies [30c512d] - - effect-app@4.0.0-beta.297 - - @effect-app/infra@4.0.0-beta.297 - - @effect-app/vue@4.0.0-beta.297 - -## 4.0.0-beta.296 - -### Patch Changes - -- Updated dependencies [e0c4835] - - effect-app@4.0.0-beta.296 - - @effect-app/infra@4.0.0-beta.296 - - @effect-app/vue@4.0.0-beta.296 - -## 4.0.0-beta.295 - -### Patch Changes - -- Updated dependencies [eb06b32] - - effect-app@4.0.0-beta.295 - - @effect-app/infra@4.0.0-beta.295 - - @effect-app/vue@4.0.0-beta.295 - -## 4.0.0-beta.294 - -### Patch Changes - -- Updated dependencies [a211c12] -- Updated dependencies [7519318] - - effect-app@4.0.0-beta.294 - - @effect-app/vue@4.0.0-beta.294 - - @effect-app/infra@4.0.0-beta.294 - -## 4.0.0-beta.293 - -### Patch Changes - -- Updated dependencies [d1c15d3] - - effect-app@4.0.0-beta.293 - - @effect-app/infra@4.0.0-beta.293 - - @effect-app/vue@4.0.0-beta.293 - -## 4.0.0-beta.292 - -### Patch Changes - -- Updated dependencies [4622a75] - - @effect-app/vue@4.0.0-beta.292 - - effect-app@4.0.0-beta.292 - - @effect-app/infra@4.0.0-beta.292 - -## 4.0.0-beta.291 - -### Patch Changes - -- Updated dependencies [848d26d] -- Updated dependencies [fa3533e] - - @effect-app/infra@4.0.0-beta.291 - - @effect-app/vue@4.0.0-beta.291 - - effect-app@4.0.0-beta.291 - -## 4.0.0-beta.290 - -### Patch Changes - -- Updated dependencies [dc07df5] - - effect-app@4.0.0-beta.290 - - @effect-app/infra@4.0.0-beta.290 - - @effect-app/vue@4.0.0-beta.290 - -## 4.0.0-beta.289 - -### Patch Changes - -- Updated dependencies [a1b59bc] - - effect-app@4.0.0-beta.289 - - @effect-app/infra@4.0.0-beta.289 - - @effect-app/vue@4.0.0-beta.289 - -## 4.0.0-beta.288 - -### Patch Changes - -- Updated dependencies [aeb17bc] -- Updated dependencies [aeb17bc] - - effect-app@4.0.0-beta.288 - - @effect-app/vue@4.0.0-beta.288 - - @effect-app/infra@4.0.0-beta.288 - -## 4.0.0-beta.287 - -### Patch Changes - -- Updated dependencies [261470f] - - effect-app@4.0.0-beta.287 - - @effect-app/infra@4.0.0-beta.287 - - @effect-app/vue@4.0.0-beta.287 - -## 4.0.0-beta.286 - -### Patch Changes - -- Updated dependencies [b6dda09] - - effect-app@4.0.0-beta.286 - - @effect-app/infra@4.0.0-beta.286 - - @effect-app/vue@4.0.0-beta.286 - -## 4.0.0-beta.285 - -### Patch Changes - -- Updated dependencies [7bd8234] - - effect-app@4.0.0-beta.285 - - @effect-app/infra@4.0.0-beta.285 - - @effect-app/vue@4.0.0-beta.285 - -## 4.0.0-beta.284 - -### Patch Changes - -- Updated dependencies [dd239fa] - - @effect-app/infra@4.0.0-beta.284 - - @effect-app/vue@4.0.0-beta.284 - - effect-app@4.0.0-beta.284 - -## 4.0.0-beta.283 - -### Patch Changes - -- Updated dependencies [0054611] -- Updated dependencies [0054611] - - effect-app@4.0.0-beta.283 - - @effect-app/infra@4.0.0-beta.283 - - @effect-app/vue@4.0.0-beta.283 diff --git a/packages/e2e/package.json b/packages/e2e/package.json deleted file mode 100644 index 8f1ef7d0c..000000000 --- a/packages/e2e/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@effect-app/e2e", - "version": "4.0.2", - "license": "MIT", - "type": "module", - "private": true, - "dependencies": { - "@effect-app/infra": "workspace:*", - "@effect-app/vue": "workspace:*", - "effect-app": "workspace:*" - }, - "devDependencies": { - "@effect/atom-vue": "^4.0.5", - "@effect/platform-node": "4.0.5", - "@effect/vitest": "4.0.5", - "@tanstack/vue-query": "5.96.2", - "@types/node": "25.9.1", - "@vitejs/plugin-vue": "^6.0.7", - "effect": "^4.0.5", - "typescript": "~6.0.3", - "vitest": "^5.0.5", - "vue": "^3.5.35" - }, - "scripts": { - "check": "tsgo --noEmit", - "test": "vitest", - "test:run": "vitest run --passWithNoTests", - "lint": "oxlint --quiet --type-aware ./test && pnpm exec dprint check --config ../../dprint.jsonc .", - "lint-fix": "oxlint --quiet --type-aware --fix ./test && pnpm exec dprint fmt --config ../../dprint.jsonc .", - "testsuite": "pnpm lint && pnpm run test:run", - "clean": "rm -rf dist" - }, - "sideEffects": false -} diff --git a/packages/e2e/test/repoInvalidation.e2e.test.ts b/packages/e2e/test/repoInvalidation.e2e.test.ts deleted file mode 100644 index 5ca7ef1bd..000000000 --- a/packages/e2e/test/repoInvalidation.e2e.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { awaitAtomResult, buildQueryFamily, invalidateAndAwait, makeAtomClientRuntime } from "@effect-app/vue/atomQuery" -import { invalidateQueries } from "@effect-app/vue/mutate" -import { defaultRegistry } from "@effect/atom-vue" -import { NodeHttpServer } from "@effect/platform-node" -import { expect, it } from "@effect/vitest" -import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query" -import { ApiClientFactory, InvalidStateError, makeRpcClient, NotLoggedInError, OptimisticConcurrencyException } from "effect-app/client" -import * as Context from "effect-app/Context" -import * as Effect from "effect-app/Effect" -import { HttpRouter, HttpServer } from "effect-app/http" -import * as Layer from "effect-app/Layer" -import { DefaultGenericMiddlewares } from "effect-app/middleware" -import { makeRepo, RepositoryRegistryLive } from "effect-app/Model" -import { MiddlewareMaker, RpcContextMap, RpcMiddleware } from "effect-app/rpc" -import * as S from "effect-app/Schema" -import { FetchHttpClient } from "effect/http" -import * as ManagedRuntime from "effect/ManagedRuntime" -import * as NetAddress from "effect/net/NetAddress" -import * as Option from "effect/Option" -import * as Reactivity from "effect/reactivity/Reactivity" -import { RpcSerialization } from "effect/rpc" -import * as Scope from "effect/Scope" -import * as Tuple from "effect/Tuple" -import { createServer } from "http" -import { createApp, effectScope, ref } from "vue" -import { RequestContextMiddleware } from "../../infra/src/internal/RequestContextMiddleware.ts" -import { makeRouter } from "../../infra/src/routing.ts" -import { DefaultGenericMiddlewaresLive } from "../../infra/src/routing/middleware.ts" -import { MemoryStoreLive } from "../../infra/src/Store/Memory.ts" -import { makeTanstackQuery, makeTanstackQueryInvalidator } from "../../vue/src/internal/tanstackQuery.ts" - -class RequestContextMap extends RpcContextMap.makeMap({ - allowAnonymous: RpcContextMap.makeInverted()(NotLoggedInError) -}) {} - -class AllowAnonymous extends RpcMiddleware.Tag()("AllowAnonymous", { - dynamic: RequestContextMap.get("allowAnonymous") -}) {} - -const AllowAnonymousLive = Layer.effect( - AllowAnonymous, - Effect.succeed(Effect.fnUntraced(function*(effect, { rpc }) { - yield* Scope.Scope - if (!RequestContextMap.getConfig(rpc).allowAnonymous) { - return yield* new NotLoggedInError({ message: "Not logged in" }) - } - return yield* effect - })) -) - -class AppMiddleware extends MiddlewareMaker - .Tag()("AppMiddleware", RequestContextMap) - .middleware(AllowAnonymous) - .middleware(...DefaultGenericMiddlewares) -{ - static Default = this.layer.pipe( - Layer.provide([AllowAnonymousLive, DefaultGenericMiddlewaresLive] as const) - ) -} - -const { Router, matchAll } = makeRouter(AppMiddleware.Default) -const { TaggedRequestFor } = makeRpcClient(AppMiddleware) -const Req = TaggedRequestFor("RepoInvalidation") - -class RepoItem extends S.Class("RepoItem")({ id: S.String, label: S.String }) {} -class OtherItem extends S.Class("OtherItem")({ id: S.String, label: S.String }) {} - -class RepoItems extends Context.Service()("RepoItems", { - make: makeRepo("RepoItem", RepoItem, {}) -}) { - static Default = Layer.effect(this, this.make).pipe( - Layer.provide(Layer.merge(MemoryStoreLive, RepositoryRegistryLive)) - ) -} - -class OtherItems extends Context.Service()("OtherItems", { - make: makeRepo("OtherItem", OtherItem, {}) -}) { - static Default = Layer.effect(this, this.make).pipe( - Layer.provide(Layer.merge(MemoryStoreLive, RepositoryRegistryLive)) - ) -} - -class GetRepoCount extends Req.Query()("GetRepoCount", {}, { - allowAnonymous: true, - success: S.Number -}) {} - -class GetRepoCountRuns extends Req.Query()("GetRepoCountRuns", {}, { - allowAnonymous: true, - success: S.Number -}) {} - -class SaveRepoItem extends Req.Command()("SaveRepoItem", { - id: S.String, - label: S.String -}, { - allowAnonymous: true, - error: S.Union([InvalidStateError, OptimisticConcurrencyException]), - success: S.Void -}) {} - -class SaveOtherItem extends Req.Command()("SaveOtherItem", { - id: S.String, - label: S.String -}, { - allowAnonymous: true, - success: S.Void -}) {} - -const RepoRpcs = { GetRepoCount, GetRepoCountRuns, SaveRepoItem, SaveOtherItem } -let repoCountRuns = 0 - -const router = Router(RepoRpcs)({ - dependencies: [RepoItems.Default, OtherItems.Default], - *effect(match) { - const repo = yield* RepoItems - const otherRepo = yield* OtherItems - return match({ - GetRepoCount: () => - Effect - .sync(() => repoCountRuns++) - .pipe(Effect.andThen(repo.all), Effect.map((items) => items.length), Effect.orDie), - GetRepoCountRuns: () => Effect.sync(() => repoCountRuns), - SaveRepoItem: ({ id, label }) => repo.save(new RepoItem({ id, label })).pipe(Effect.orDie), - SaveOtherItem: ({ id, label }) => otherRepo.save(new OtherItem({ id, label })).pipe(Effect.orDie) - }) - } -}) - -const RpcRouterLayer = matchAll({ router }) -const NodeServerLayer = NodeHttpServer.layer(() => createServer(), { port: 0 }) - -const ServerLayer = HttpRouter - .serve(RpcRouterLayer.pipe(Layer.provide(HttpRouter.middleware(RequestContextMiddleware()).layer))) - .pipe( - Layer.provide(NodeServerLayer), - Layer.provide(RpcSerialization.layerNdjson) - ) - -const ClientLayer = Layer - .unwrap( - Effect.gen(function*() { - const server = yield* HttpServer.HttpServer - const addr = server.address - 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)) - }) - ) - .pipe(Layer.provide(NodeServerLayer)) - -const TestLayer = Layer.mergeAll(ServerLayer, ClientLayer, Reactivity.layer) - -const setup = () => { - repoCountRuns = 0 - const runtime = ManagedRuntime.make(TestLayer) - return Effect - .all( - [ - ApiClientFactory.makeFor(Layer.empty)(RepoRpcs), - Effect.context() - ] as const - ) - .pipe(Effect.map(([client, context]) => Tuple.make(runtime, client, context)), runtime.runPromise) -} - -it("atom engine: rpc repo write invalidates and refetches; unrelated rpc write does not", async () => { - const [runtime, client, context] = await setup() - const reactivity = Context.get(context, Reactivity.Reactivity) - const rt = makeAtomClientRuntime(() => Layer.succeedContext(context), runtime.memoMap) - const invalidator = { - invalidateAndAwait: (keys: ReadonlyArray>) => - invalidateAndAwait(keys).pipe(Effect.provideService(Reactivity.Reactivity, reactivity)) - } - const atom = buildQueryFamily(rt, client.GetRepoCount as any)(undefined) - const unmount = defaultRegistry.mount(atom) - const run = (effect: Effect.Effect) => Effect.runPromise(effect) - const save = (id: string) => - run( - invalidateQueries(client.SaveRepoItem, undefined, invalidator)(client.SaveRepoItem.handler({ id, label: id }), { - id, - label: id - }) - .pipe(Effect.provide(context)) as any - ) - const saveOther = (id: string) => - run( - invalidateQueries(client.SaveOtherItem, undefined, invalidator)(client.SaveOtherItem.handler({ id, label: id }), { - id, - label: id - }) - .pipe(Effect.provide(context)) as any - ) - - try { - expect(await run(awaitAtomResult(defaultRegistry, atom) as any)).toBe(0) - - await save("1") - expect(await run(awaitAtomResult(defaultRegistry, atom) as any)).toBe(1) - - const before = await run(client.GetRepoCountRuns.handler().pipe(Effect.provide(context)) as any) - await saveOther("x") - const after = await run(client.GetRepoCountRuns.handler().pipe(Effect.provide(context)) as any) - expect(after).toBe(before) - } finally { - unmount() - await runtime.dispose() - } -}, 10_000) - -it("tanstack engine: rpc repo write invalidates and refetches; unrelated rpc write does not", async () => { - const [runtime, client, context] = await setup() - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: 0 } } - }) - const useQuery = makeTanstackQuery(() => context, queryClient) - const invalidator = makeTanstackQueryInvalidator(queryClient) - const app = createApp({ render: () => null }) - app.use(VueQueryPlugin, { queryClient }) - const scope = effectScope(true) - let handle: any - let data: any - const run = (effect: Effect.Effect) => Effect.runPromise(effect) - const save = (id: string) => - run( - invalidateQueries(client.SaveRepoItem, undefined, invalidator)(client.SaveRepoItem.handler({ id, label: id }), { - id, - label: id - }) - .pipe(Effect.provide(context)) as any - ) - const saveOther = (id: string) => - run( - invalidateQueries(client.SaveOtherItem, undefined, invalidator)(client.SaveOtherItem.handler({ id, label: id }), { - id, - label: id - }) - .pipe(Effect.provide(context)) as any - ) - - app.runWithContext(() => - scope.run(() => { - const tuple = useQuery(client.GetRepoCount as any)(ref(undefined) as any, {} as any) as any - data = tuple[1] - handle = tuple[3] - }) - ) - - try { - expect(await run(handle.refetch())).toBe(0) - - await save("1") - expect(data.value).toBe(1) - - const before = await run(client.GetRepoCountRuns.handler().pipe(Effect.provide(context)) as any) - await saveOther("x") - const after = await run(client.GetRepoCountRuns.handler().pipe(Effect.provide(context)) as any) - expect(after).toBe(before) - } finally { - scope.stop() - queryClient.clear() - await runtime.dispose() - } -}, 10_000) diff --git a/packages/e2e/tsconfig.json b/packages/e2e/tsconfig.json deleted file mode 100644 index d51e90c5f..000000000 --- a/packages/e2e/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "lib": [ - "esnext", - "DOM" - ], - "moduleResolution": "Node16", - "esModuleInterop": true, - "noEmit": true, - // Leaf test package: it imports workspace packages by source, so disable project-mode (which - // would demand every transitively-imported source file be listed) and emit nothing. - "composite": false, - "incremental": false, - "declarationMap": false, - "rootDir": "../..", - "types": [ - "node", - "vitest/globals" - ] - }, - "include": [ - "./test/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/packages/e2e/vitest.config.ts b/packages/e2e/vitest.config.ts deleted file mode 100644 index 852ed8889..000000000 --- a/packages/e2e/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -/// -import { defineConfig } from "vitest/config" -import makeConfig from "../../vite.config.base" - -export default defineConfig({ - ...makeConfig(__dirname), - test: { - environment: "node", - include: ["**/test/**/*.test.{ts,tsx}"], - exclude: ["node_modules/**", "dist/**"], - globals: true - } -}) diff --git a/packages/effect-app/.madgerc b/packages/effect-app/.madgerc deleted file mode 100644 index b407c6b4b..000000000 --- a/packages/effect-app/.madgerc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "detectiveOptions": { - "ts": { - "skipTypeImports": true - } - } -} diff --git a/packages/effect-app/.npmignore b/packages/effect-app/.npmignore deleted file mode 100644 index 9e4be5957..000000000 --- a/packages/effect-app/.npmignore +++ /dev/null @@ -1,14 +0,0 @@ -*.test.js -*.test.d.ts -tsconfig.build.json -*.log -tslint.json -.eslintrc.js -.eslintignore -.madgerc -.swcrc -.cache -coverage/ -.tsbuildinfo -.build/ -!dist/ \ No newline at end of file diff --git a/packages/effect-app/CHANGELOG.md b/packages/effect-app/CHANGELOG.md deleted file mode 100644 index d3baa3a56..000000000 --- a/packages/effect-app/CHANGELOG.md +++ /dev/null @@ -1,9773 +0,0 @@ -# @effect-app/prelude - -## 4.0.2 - -### Patch Changes - -- 76911ed: Add `suspendWaiting` so a command can park on a dialog without the spinner or the waiting toast. The button stays disabled. Both come back when the effect finishes and the rest of the command runs. -- 7a99d1b: Upgrade Effect packages to npm `4.0.2`. - -## 4.0.1 - -### Patch Changes - -- 222e619: Remove effect-app helpers that Effect already provides, and add `migrateEffectAppSource` to rewrite call sites. - - Deleted aliases and wrappers now live in Effect: `String.capitalize` / `uncapitalize`, `Struct.keys`, `Record.values`, `absurd`, `Tuple.make`, `Option.isSome`, `Option.liftPredicate` for non-empty arrays, `Array.chunksOf`, `Chunk.findFirst` / `findLast` / `partition` / `containsWith`, `Array.dedupeWith`, `Effect.annotateLogsScoped`, `Deferred.await`, `Effect.fromOption`, `Result.fromOption`, and `DateTime.add` / `subtract`. `Schema.DateValid` was an alias of `Schema.Date`. The date-fns calendar wrappers and the `date-fns` dependency are gone. - - `effect-app/migration/nativeEffect` exports the replacement table and `migrateEffectAppSource(source)`, which rewrites those imports in one file. `dropUndefined` and `fromBool` stay, along with `groupByT` (any key, entry list), `randomElement`, `findFirstMap`, and the schema constructors that add construction defaults. - - `effect-app/DateTime` adds `makeZonedLocal` and `pipeZonedLocal`. `makeZonedLocal` attaches the process timezone to a `Date` without moving the instant. `pipeZonedLocal` runs the functions passed to it and returns that instant with `DateTime.toDateUtc`. `DateAdd*` and `DateSub*` migrate to `pipeZonedLocal(date, DateTime.add({ days }))`, which keeps the local calendar. - - `Chunk.containsWith` calls the equivalence as `(needle, element)`. The old `elem` called it as `(element, needle)`, so only a symmetric equivalence is unchanged. `Record.values` expects one value type, so a mapped type whose value depends on its key should use `Object.values`. - -## 4.0.0 - -### Major Changes - -- 52b0b01: Fix Schema->Codec -- 52b0b01: Effect v4 beta - -### Minor Changes - -- b2df3fa: Align `Schema.Void` with TypeScript `void` return-value semantics (effect-smol PR #2475 / `b7d46ab`). - - `S.Void` now accepts **any present value** at runtime and discards it to `undefined`, while keeping the decoded/encoded type as `void` — matching a `void` return whose result callers never observe. This is implemented as an override of the AST node in a new `effect-app/SchemaAST` module (`export * from "effect/SchemaAST"` plus a `Void` subclass whose parser mirrors the PR's `fromAnyToConst(undefined)`), so there is a single canonical `Void` used everywhere, including RPC success schemas. - - - New `effect-app/SchemaAST` module; internal `SchemaAST` imports across the libs now route through it. - - Removed `ForceVoid` from `effect-app/client/makeClient` — use `S.Void` directly, which now carries this behaviour. - - `S.Void_` remains available as effect's original (`undefined`-only) Void. - -- ba789a2: Move core service contracts and runtime-agnostic modules into `effect-app`, keep `infra` and `vue` focused on adapters, and drop the temporary `infra` compatibility re-export paths in favor of the new canonical imports. - - `@effect-app/infra` no longer re-exports moved core modules such as `./Model`, `./Emailer/service`, `./QueueMaker/service`, `./Store/service`, `./adapters/*`, or `./api/*` entrypoints. - -- ab289d4: Add opt-in `autoscaleMaxThroughput` for Cosmos containers created on demand (`StorageConfig`, `ClusterCosmosConfig`, `WorkflowEngineCosmosConfig`). When set, a missing container is created with autoscale at that max RU/s, unless its database has shared throughput (then it keeps sharing the database pool). Existing containers are untouched; unset keeps the Cosmos default of manual 400 RU/s per container. -- aeb17bc: Add `disableQueryInvalidation` flag to Command config for background saves. - - Set `disableQueryInvalidation: true` in a `Req.Command` config (3rd argument) - to suppress all client-side query invalidation for that command — client - `invalidatesQueries` callbacks, server-returned `metadata.invalidateQueries`, - and repository-derived write-dependency matching are all skipped. Use for - background saves (e.g. debounced auto-save) whose writes should not trigger - query refetches. - - - `InvalidationConfig` gains `disableQueryInvalidation?: boolean`. - - `RequestHandlerWithInput` gains `disableQueryInvalidation?: boolean`, - propagated from `Request.config` by `ApiClientFactory.makeFor`. - - `invalidateCache` early-returns `Effect.void` when the flag is set, - applying to both regular and stream mutations. - -- f21190c: Add `copy` property to `Class` and `TaggedClass` for creating modified instances with updated fields. The `copy` method is cached per-class and supports both object and function-based updates, with pipeline support. -- f88ea34: Move `makeQueryKey` into `effect-app/client` and update Vue source and tests to import it from the shared client module. Vue still re-exports `makeQueryKey` from `src/lib` for compatibility. -- 2ebf8ae: `jitM` runs at the store boundary as JSON → JSON, before the `toCodecJson(toEncoded(schema))` decode; repositories decode the Encoded shape only. - - `jitM` used to be applied by the repository _after_ the store had already decoded the document, and was typed `(pm: Encoded) => Encoded` - a lie, since jitMs are written against the stored JSON and `Encoded` holds native `Date`/`Map`/`Set` values. - - The read pipeline is now: raw JSON document → merge `defaultValues` (unchanged: they only fill _absent_ keys, so an explicitly stored `null` reaches `jitM`) → `jitM` (JSON → JSON) → decode `Schema.toCodecJson(Schema.toEncoded(schema))` → Encoded. The repository then decodes Encoded → the domain type with `schema` and no longer applies `jitM` at all. Because `jitM` now runs before any schema decode sees the document, it can repair legacy shapes including explicit `null`s. - - - `StoreConfig.jitM?: (json: JsonRecord) => JsonRecord` is new, where `JsonRecord` (exported from `effect-app/Store`) is `{ readonly [key: string]: Schema.Json }`. It is not applied on the write/encode path, and never receives `_etag`. - - `RepositoryOptions.jitM` changes type from `(pm: Encoded) => Encoded` to `(json: JsonRecord) => JsonRecord` and is forwarded into the store config. - - **Migrating a jitM:** it now receives and must return JSON. A `Date` field arrives as an ISO string (return a string, not a `Date`), a `ReadonlySet` as an array, a `ReadonlyMap` as an array of `[key, value]` pairs. Field access is index-signature based (`json["x"]`) rather than typed property access. - - The JSON→Encoded decode stays strict: a document `jitM` does not repair fails at the store boundary instead of being read back half-decoded. - - `ValidationError.jitMResult` is deprecated: `validateSample` only ever sees the store's output, so it is identical to `rawData`. - -- 8cb3de4: Add command invalidation helpers that preserve query-only resource types and pass mutation input and `Exit` results into invalidation callbacks. Update Vue `clientFor` to merge request-level invalidation config with call-site invalidation and require matching invalidation resources. -- 186de3a: Stop forcing Date/Map/Set Encoded shapes to JSON. - - `Schema.Date` / `ReadonlySet` / `ReadonlyMap` now keep native Encoded types (`Date`, `Set`, `Map`). Use `DateFromString`, `ReadonlySetFromArray`, and `ReadonlyMapFromArray` when the Encoded form must be JSON. The query DSL accepts those native values, including array ops (`includes` / `in` / `includes-any`) on `Date[]` and `ReadonlySet` fields. Memory, Disk, SQL, and Cosmos convert Encoded Date/Map/Set through `Schema.toCodecJson` on write/read; query parameters and defaults lower the same way from the store schema. App types such as DateOnly stay native Encoded and JSON-lower via that schema — not a type registry. - -- 186de3a: Query maps as JSON arrays of `[key, value]` tuples. - - `where("meta", "hasKey" | "hasValue" | "hasKeyValue", ...)` (and `not*` / `*-any` / `*-all` variants) filter `ReadonlyMap` fields. Memory, Disk, SQLite, Postgres, and Cosmos compile those ops against the encoded tuple array. - -- d16845e: Remove `TaggedRequest` from `makeRpcClient`, now only `TaggedRequestFor` is returned. Remove all legacy `meta.moduleName` support — `id` and `moduleName` are now required on `Req` type. Remove `makeRpcGroup` (use `makeRpcGroupFromRequestsAndModuleName` instead). -- beae3a0: Remove `withDefaultConstructor` wrapper, use `S.withConstructorDefault` directly with `Effect.succeed`/`Effect.sync`. -- 1f103b2: Replace `proxify` with explicit service accessor helpers: `accessFn`, `accessEffectFn`, `accessCn`, `accessEffectCn`. -- 0054611: Derive query invalidation from repository read/write dependencies and propagate dependency metadata through RPC clients. -- 89d8b3a: Add Effect RPC `Stream` support to the wrapper. - - - New `Stream` request constructor on `TaggedRequestFor` parallel to `Query`/`Command`. Emits resources with `type: "stream"`. - - Server router (`@effect-app/infra` `routing.ts`) accepts stream resources whose handlers return a `Stream.Stream` (or a function from input to one). Forwards `stream: true` to `Rpc.make` so `RpcSchema.Stream` wrapping is applied. Streams bypass `applyRequestTypeInterruptibility` and the `Effect.withSpan` wrapping (the RPC server adds its own span). - - Client (`apiClientFactory.ts`) detects stream resources, forwards `stream: true` when constructing `RpcGroup`, and exposes the per-request `handler` as a `Stream.Stream` (via `Stream.unwrap` over the `ManagedRuntime` context) instead of an `Effect`. `Invalidation.CommandResponseWithMetaData` continues to apply only to commands. - - New `RequestStreamHandler` / `RequestStreamHandlerWithInput` shapes in `clientFor.ts`; `RequestHandlers` dispatches on `type: "stream"`. - -- 08d2e70: Add `concurrency: "unbounded"` parseOptions annotation to Schema constructors (Struct, Array, NonEmptyArray, Record, TaggedStruct, ReadonlySet, ReadonlyMap, Class, TaggedClass) so all encode/decode operations automatically run with unbounded concurrency. Also override `mapFields` on Struct and Class/TaggedClass to preserve the annotation. -- 5f9cd6a: Schema/Class facades: add `StructFacade` and carry `identifier` on the class facades. - - - Add `StructFacade` = `Omit, "Type" | "Encoded" | "~type.make.in" | "DecodingServices" | "EncodingServices"> & { pinned type-level members }`. A top-level `const X = S.Struct(...)` / `S.TaggedStruct(...)` model faceted by the `.d.ts`-emit compiler is retyped to it: still a real `S.Struct` (so `Workflow.AnyStructSchema`, the `Struct` reconstruction, `Union`, `.fields.x` keep working) while `Type`/`Encoded`/make/services resolve to the named namespace interfaces. - - Add `readonly identifier: string` to `OpaqueClassFacade` and `OpaqueErrorFacadeClass` (an `S.Class` static lost from the bare emitted interface, which the compiler otherwise had to hand-emit per model). `OpaqueFacade` is intentionally left untouched — `S.Opaque` and requests build on `S.Bottom`, not `S.Class`, so they have no `identifier`. - -- 6252808: Add `ReadonlySetFromArray` and `ReadonlyMapFromArray` schema transformations that decode from annotated arrays to Set/Map. Update `ReadonlySet` and `ReadonlyMap` to use these internally. -- 821468d: Add server-driven cache invalidation via RPC response headers. - - - `effect-app/rpc`: new `Invalidation` module with `InvalidationKey` / `InvalidationKeys` schemas, `Invalidates` annotation (for declaring static invalidation on Rpc definitions), `InvalidationSet` reference (request-scoped accumulator), and `makeInvalidationSet` helper. - - `effect-app/middleware`: new `InvalidationMiddleware` RPC middleware tag; included in `DefaultGenericMiddlewares`. - - `effect-app/client`: new `InvalidationKeys` module with `InvalidationKeysFromServer` reference and `makeInvalidationKeysService` helper; `apiClientFactory` now taps HTTP responses to read the `x-invalidate` header and forward keys to `InvalidationKeysFromServer`. - - `@effect-app/infra`: new `InvalidationMiddlewareLive` RPC middleware implementation that owns the full lifecycle — creates a request-scoped `InvalidationSet` (backed by a `Ref`), pre-populates it from the `Invalidates` annotation, provides it to the handler, and after the handler completes registers an HTTP pre-response handler (via `appendPreResponseHandlerUnsafe`) to write the accumulated keys as an `x-invalidate` response header. No separate HTTP middleware is needed. - - `@effect-app/vue`: `invalidateQueries` / `useMutation` now reads server-provided invalidation keys from `InvalidationKeysFromServer` after each mutation and applies them alongside the client-side invalidation. - -- 025de47: Fold the encoded-override support from `ExtendedClass` and `ExtendedTaggedClass` into `Class`, `TaggedClass`, `ErrorClass`, and `TaggedErrorClass`, and update model codegen to detect the new second-generic form. -- 3a8c710: Store document decode fails as a typed `SchemaError` instead of a defect; repositories keep their public error channels by dying at the boundary; `validateSample` reports documents that fail at the store boundary. -- 186de3a: JSON stores lower native Encoded values (Date, Map, Set, and app types such as DateOnly) through the store's document schema. - - `makeRepo` already passes that schema. Adapters encode documents, query parameters, and defaults with `Schema.toCodecJson(toEncoded(schema))` at the field path. No type registry. Schemaless stores still lower Date/Map/Set structurally. - -- 828d264: Stream requests now support an optional `final` schema that models the final success type of the stream. When declared, `mutateStream`'s execute effect resolves with the last emitted value typed as `Final` instead of `void`. - - ```ts - class MyStream extends SomethingStream()( - "MyStream", - { id: S.String }, - { - success: S.Union([OperationProgress, ExportComplete]), - final: ExportComplete, // execute now resolves with ExportComplete - } - ) {} - ``` - -- 7fa3045: V1/V2/V3: stream and command requests carry invalidation metadata - - **V1** – stream final response includes metadata - - - `Invalidation.StreamResponseChunk` wraps each stream item as `{ _tag: "value", value }` and appends `{ _tag: "done", metadata }` at the end carrying all accumulated invalidation keys. - - **V2** – invalidation keys included in failures - - - `Invalidation.CommandFailureWithMetaData` and `Invalidation.StreamFailureChunk` carry keys accumulated up to the point of failure, so clients can invalidate queries even when a command or stream errors. - - `InvalidationMiddlewareLive` wraps command failures; `routing.ts` wraps stream failures. - - `apiClientFactory.ts` unwraps both on the client side, forwarding keys before re-failing with the original error. - - **V3** – mid-stream metadata chunks - - - `Invalidation.StreamResponseChunk` now also includes `{ _tag: "metadata", metadata }` for mid-stream invalidation. - - After each emitted value, the server drains accumulated keys and emits a "metadata" chunk if any keys were collected since the last drain (bucket reset via `InvalidationSet.drain`). - - `apiClientFactory.ts` processes "metadata" chunks the same as "done" chunks, forwarding keys to `InvalidationKeysFromServer` immediately. - - `makeInvalidationKeysService` accepts an optional `onAdded` callback that fires after each key addition, enabling `mutate.ts` to trigger query invalidation mid-stream without waiting for the stream to complete. - -- 3dc0d2a: Add streaming as a `stream: true` config option on `Query` / `Command` instead of a separate request type. - - `TaggedRequestFor` now exposes only `Query` and `Command` factories — the standalone `Stream` factory is removed. To produce a Stream of `success` values, pass `stream: true` in the request config. The request `type` field stays `"command" | "query"`; a new `stream: boolean` field carries the streaming flag (stripped from the stored handler config). - - ```ts - // Query that streams results - Req.Query()("Tag", {}, { stream: true, success: ... }) - - // Command that streams results - Req.Command()("Tag", {}, { stream: true, success: ... }) - ``` - - Vue client mapping (per-handler properties mirror the non-stream API — `.query`, `.fn`, `.mutate`): - - - `query` + `stream: true` → exposes `.query` (read-only streaming, tracked Vue Query). Helper map key: `${name}Query`. - - `command` + `stream: true` → exposes `.fn` and `.mutate` (mutating streaming). - - Plain `query` / `command` unchanged. - - Server routing dispatches via the new `stream` flag (`makeStreamRpc` for streaming commands/queries, `makeCommandRpc` / `Rpc.make` otherwise). - - Also lifts the `Struct` / `TaggedStruct` and `Opaque` definitions in `effect-app/Schema` to use `S.Bottom` / `S.Opaque` directly, exposing `fields`, `mapFields`, and a `MakeIn` that allows `void` when all fields are optional. `TaggedRequestFor` request classes now use `Opaque(TaggedStruct(...))` instead of `TaggedClass`, and decoding/encoding services are derived from `success` / `error` rather than stored on the request. - - **Migration**: replace `Req.Stream` with `Req.Query` or `Req.Command` and add `stream: true` to the config — `Query` for read-only streams, `Command` for mutating streams. - -- 738b482: Add pick/omit to Struct and TaggedStruct -- 3613e87: Add `TaggedRequestFor` helper to `makeRpcClient` that curries a `moduleName`, producing request classes with static `id` and `moduleName` properties. This enables passing request classes directly to `makeQueryKey` without going through `clientFor` first. The `clientFor` function no longer requires a `meta` property on the module when requests carry `moduleName`. The meta codegen preset now generates `Req = TaggedRequestFor(moduleName)`. Original `TaggedRequest` remains for backwards compatibility. -- 7119320: Add `generateGuards` and `generateGuardsFor` to `TaggedUnion` / `ExtendTaggedUnion` for property-scoped type guards. - - - `generateGuards("key")` — generic per-guard, no need to specify the container type - - `generateGuardsFor()("key")` — curried, fixes `A` for concrete guard signatures - - Both return `{ is{Tag}, isAnyOf }` guards that narrow the container type by its tagged union property. - -- e944bca: Add `generateGuards` and `generateGuardsFor` to `TaggedUnion` / `ExtendTaggedUnion` for property-scoped type guards. - - - `generateGuards("key")` — generic per-guard, no need to specify the container type - - `generateGuardsFor()("key")` — curried, fixes `A` for concrete guard signatures - - Both return `{ is{Tag}, isAnyOf }` guards that narrow the container type by its tagged union property. - -- 8bd9a11: Support ID-scoped signal dependencies and additive repository dependencies derived from previous and current entities. -- 23a7167: Add a typed, serializable `DatabaseError { message, transient, cause }` for store adapter failures. - - Store adapters wrapped DB calls in bare `Effect.promise` / `.orDie`, so a transient infra failure (request timeout, throttle, 5xx, dropped socket) became a raw-`Error` defect — which `Effect.retry` can't retry and which breaks JSON encoding of a workflow exit cause ("Expected JSON value, got Error"). `DatabaseError` is now exposed on all `Store` and `Repository` method error channels (writes: `OptimisticConcurrencyException | DatabaseError`) and added to `SupportedErrors` so the api/client/FE treat it as a 500-class error. Each adapter wraps its db-call failures into `DatabaseError` with a `transient` flag (timeout/throttle/5xx ⇒ retryable); the `cause` serializes via `Schema.Defect`. Construction/seed/DDL paths stay `orDie`. - -- e71eb78: Tighten typing of query DSL builders so field/path parameters are constrained to actual field paths instead of bare `string`: - - - `relation(path)` now infers the relation's element type and constrains `distinctCount`/`sum`/`collect`/`collectDistinct`/`collectFields`/`collectDistinctFields` and the `unit` of `sumExprBy`/`sumExprNormalized` to `FieldPath`. - - `relation(path).expr` exposes a scope-bound math-expression builder so `expr.field(...)` inside `sumExpr`/`sumExprBy`/`sumExprNormalized` is typed against the relation element. - - Top-level `expr.field` accepts an optional generic for opt-in tightening (`expr.field("x")`). - - `aggregate(schema, build)` accepts a builder callback whose `agg` argument is bound to the source row inferred from the pipe — paths are checked without any explicit generic: `make().pipe(aggregate(schema, ($) => ({ city: $.field("address.city") })))`. Plain `AggregateMap` form still accepted. - - `projectComputed(schema, build, mode?)` accepts a builder callback whose `{ relation }` argument is bound to the source row inferred from the pipe: `make().pipe(projectComputed(out, ({ relation }) => ({ total: relation("items").sum("qty") })))`. Plain `ComputedProjectionMap` form still accepted. - - `agg()` factory remains as an escape hatch when the builder is built outside a pipe. - -### Patch Changes - -- 439cbeb: Adopt module system from effect-smol: replace barrel imports with specific submodule imports (`import * as X from "effect-app/X"` / `import * as X from "effect/X"`). -- da83c1f: Align `InvalidationEntry` (vue) with `InvalidateQueryInstruction` (effect-app). - - `InvalidateQueryInstruction` is now parametrized over `Filters` / `Options` - (defaulting to `Record` so the core stays framework-agnostic). - `@effect-app/vue` exposes `InvalidationEntry` as a narrowed alias substituting - `@tanstack/vue-query`'s `InvalidateQueryFilters` and `InvalidateOptions`. Single - source of truth for the union shape across both packages. - -- 199e9a5: Allow Struct and TaggedStruct make helpers to omit input when every constructor field is optional or defaulted, and preserve widening copy typings through a lighter named public type to improve TypeScript editor responsiveness. -- b035b1c: Isolate per-call `Effect.provide(layers)` / `Stream.provide(layers)` in the - RPC api client factory via `{ local: true }`. - - `layers` here is built per RPC invocation from a caller-supplied - `requestLevelLayers` plus a fresh `RequestName` layer. Today both pieces are - stateless, so no observable bug — but `Effect.provide(layer)` without - `local: true` resolves its `MemoMap` from the ambient fiber context, and on a - long-lived runtime (browser app, server fiber) any stateful layer slipped into - `requestLevelLayers` by a caller would be memoized and shared across every - subsequent RPC call. `{ local: true }` builds the layer fresh per call and - skips the ambient MemoMap entirely. - - Also documents the rule in `AGENTS.md` ("Per-request `Effect.provide(layer)` - must isolate its MemoMap") so the pattern is enforceable in code review and - caught early. Companion to the `provideOnRequestScope` MemoMap fix in - `@effect-app/infra`. - -- 664e83d: Add Atom-native Vue query APIs, stream query pull atoms, and a TanStack-backed legacy query engine toggle. -- a4dff57: Adjust Struct and TaggedStruct copy typing to follow class-style widening constraints while keeping structural copy runtime behavior. -- ba4bdc3: improve: support .ts extension -- 52b0b01: remove hopefully obsolete Struct overrides -- 9d3495e: Preserve field-level schema decode errors for relaxed Class and TaggedClass declarations so decode failures report nested constraints (for example min-length violations) instead of only a generic class-type mismatch. -- 3436d44: Extend `Schema.Opaque` in `effect-app/Schema` to support an optional encoded-type generic while preserving the original single-generic behavior. -- 52b0b01: Update `Effect.allLower` to call `svc.asEffect()` when available, ensuring service entries are normalized before `Effect.all` evaluation. -- f317c5e: Preserve omitted-input make helpers through Schema.Opaque when the wrapped schema allows optional or default-only constructor input. -- e585c9c: fix config nested -- e4ff9a6: Fix the global `Array`/`ReadonlyArray.map` override breaking union-of-array receivers. The previous `this: NonEmptyArray`/`NonEmptyReadonlyArray` overload was selected-then-rejected on a union receiver (e.g. `(readonly A[] | readonly B[]).map(...)`), raising TS2684. Replaced with a single conditional-return signature (`this extends NonEmpty… ? NonEmpty : U[]`) that preserves NonEmpty refinement without a `this` parameter, so union-array `.map` calls type-check again. -- 33b0544: Replace repository codec child spans with parent-span timing attributes and bounded metrics, and record database operation timing for instrumented stores. -- 99c43c4: Derive scoped dependency reads and writes from typed query filters and annotated model relationships, including previous aliases. -- 52b0b01: Fix `Schema.TaggedUnion(...).tags` extraction for class-based members (for example `TaggedClass`) by using a local AST sentinel walker instead of relying on internal effect APIs. - - Add tests covering: - - - `TaggedUnion` with `encodeKeys`-wrapped members - - `TaggedUnion` with `TaggedClass` members - -- 08d092c: Update Atom query caches in memory and accumulate stream invalidations so live events do not cause repeated RPC refetches. -- 947fe20: fix input date -- 21ac90a: Unify generated model facades on `OpaqueFacade` and support class facades through encoded-key schema transforms. -- 32f05c8: Remove unused dependency declarations from package manifests. -- 6cfd83d: update effect to latest beta -- 1c858d3: fix request scope problems -- 52b0b01: fix bs -- 992d9fa: fix more branded types -- 505bfa9: Add concurrent decode helper APIs and migrate decode callsites to use them. - - - Add `withDefaultParseOptions` and keep `DefaultParseOptions` centralized. - - Export `decodeEffectConcurrently` and `decodeUnknownEffectConcurrently` from Schema and SchemaParser modules. - - Update repository, queue, client, form, and CLI decode paths to use concurrent decode helpers. - - Keep schema constructors free of hardcoded parse concurrency overrides. - -- 939bebc: Remove the local `Config.nested` / `ConfigProvider.nested` overrides. They worked around an effect v3 bug where the nested namespace segment bypassed the provider's `mapInput`/`constantCase` transform. effect `4.0.0-beta.84` fixes this upstream (its transformation-compose refactor threads the nested prefix through the provider's lookup transform), and the old overrides relied on internals (`Config.make`, `provider.get`/`mapInput`/`prefix`) that no longer exist. Both modules now re-export effect directly; behaviour for consumers (e.g. `Config.nested("cups")` against a `constantCase` env provider) is unchanged and correct. -- 52b0b01: Beta25 -- e0c4835: revert bs -- 32dbc54: fix stream type when no success specified -- 0a0030f: fix missing overloads -- 0263827: Fix `Date` / `DateValid` default helpers to pipe from their own schema. - - `withConstructorDefault` and `withDecodingDefaultType` on `DateValid` previously piped from `DateFromString`, dropping the `isDateValid()` check (and, after the recent identifier split, the `DateValid` annotations) from the resulting defaulted schema. Both `Date` and `DateValid` now use `extendM` so the helpers attach to the underlying schema (`DateFromString` / `DateValidFromString`). - -- 0263827: Distinguish `Date` and `DateValid` in JSON Schema output. - - - `Date` now emits identifier `DateOrInvalid` with description noting the value may be invalid. - - `DateValid` now emits its own annotated string (identifier `Date`) with description stating a valid ISO 8601 date is required. - -- c0e5a1b: Add `Schema.dropConstructorDefault`, a `Struct.Lambda` that clears a field's `withConstructorDefault`. Compose it with `Struct.omit`/`Struct.map` when deriving a partial-update schema from a "create" schema, so omitted fields' constructor defaults don't resurrect themselves in `.make(...)` output. - - Also, `makeExactOptional` now drops constructor defaults automatically, matching this behavior. - -- 10b55ff: update packages -- 14aba14: fix: clientFor -- a0075b8: `Email` and `PhoneNumber` carry native `arbitraryConstraint` patterns, so `effect/unstable/arbitrary` (and `generateFromSchema`) can sample them instead of exhausting rejection sampling on their `refine` guards. -- f052d38: Replace `(...) => Effect.gen` with `Effect.fnUntraced` in `apiClientFactory`. -- f233f3d: `PositiveNumber` and `NonNegativeNumber` bound native Arbitrary generation to at most 1,000,000 (`arbitraryConstraint` on their existing checks), so generated fixtures no longer overflow derived totals to `Infinity`. Validation and JSON Schema output are unchanged. -- 18bae5b: Upgrade Effect packages to `4.0.0-rc.114` (core `effect` via pkg.pr.new at `cdb3ac91` until npm publishes that version). Also bump independently versioned `@effect/language-service` to `0.87.2` and `@effect/tsgo` to `0.45.0` (requires `oxlint` `1.82.0`). - - Breaking API updates from this RC: - - - 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` - - `SchemaTransformation.transformOrFail` renamed to `transformEffect` - - `Fiber.currentSpan` moved to `fiber.cache.span` - - HTTP server addresses are `InetAddressV4`/`InetAddressV6` instead of `TcpAddress` - - Object JSON Schema now emits `additionalProperties: true` - - Schema `Union` AST stores matching as `options.mode` instead of `mode` - -- f313973: fix type make interface -- edc52e4: update -- ea1bd46: fix: prevent expansion of nominal brands -- 85a8275: Expose `make`, `makeOption`, and `makeEffect` static helpers on request classes created via `Query`/`Command`. -- 50b022e: Harden model facades and add `OpaqueErrorFacadeClass`. - - - `OpaqueErrorFacadeClass`: facade `TaggedErrorClass`/`ErrorClass` while keeping - `Cause.YieldableError` on the constructed instance (so `yield* new Err()`, - `Effect.fail`, and `instanceof` keep working through the facade). - - `OpaqueFacadeInput` relaxed to require only the codec service channels, so - transformed schemas (`.pipe(encodeKeys/annotate/filter/...)`) can be facaded; - `fields`/`copy`/`mapFields` flow through `OpaqueFacadeStatics` when present, and - `to` is carried so models that compose via `X.to.fields` keep working. - - Dropped the wide `fields`/`mapFields` overrides on the facade interfaces so the - precise statics win (keeps `Q.project(X.mapFields(...))` typed). - - Codegen (`eslint-codegen-model`): resolver prefers the private `_X` over the - self-referential facade; converges static service types in one run; per-model - classification (facade Opaque models, leave `Class` standard in mixed files); - `Make` emitted as `type X = {...} | void` when the make-input is voidable; - `readonly`-prefixed array/tuple elements parenthesized; value self-references in - the moved `_X` body rewritten to `_X`; instance getters surface on `Self`. - -- 52b0b01: Migrate schema arbitrary annotations to Effect v4 `toArbitrary` and cover `StringId` generation with a fast-check test. -- 47e3742: Preserve Struct.copy through `annotate`, `annotateKey`, and `mapFields` chains, and add tests covering chained copy behavior on Struct schemas. -- 458bb1b: fix type -- 3365758: workaround effect error messages -- 54ec1ef: fix bs -- 0d4e0b8: Fix `isGeneratorFunction` using `isObject` instead of `isFunction`: generator functions have `typeof === "function"`, not `"object"`, so the check always returned `false`. This caused `Command.streamFn` generator-form handlers to silently pass a raw `Generator` object rather than an `Effect`, meaning the mutation was never executed. -- 31739d7: Fix JSON schema output for Email, Date, PhoneNumber, and Url schemas. The `jsonSchema` annotation key is not recognized by Effect v4's JSON schema generator — use proper v4 annotations (`format`, `description`) and built-in checks (`isMinLength`, `isMaxLength`) instead. -- 0b21a02: fix: flatten simple allOf constraints in JSON Schema output for better codegen compatibility -- bbaa67e: Fix `TS2589: Type instantiation is excessively deep and possibly infinite` when querying models that embed `Schema.Defect()` (or any field encoded as `Json`). - - The query path type `Path` (used by `Q.where`/`Q.and`/`Q.order` to type dotted field paths) recursed through object types with no termination for self-referential ones. Under effect `4.0.0-beta.83`, `Schema.Defect()` encodes as `Json`, whose `JsonObject = { readonly [x: string]: Json }` index signature made `Path` descend forever, blowing past TypeScript's instantiation limit on any model with such a field (e.g. carrier-error states carrying `raw: Schema.Defect()`). - - `Path` now threads a `Seen` set of already-entered object types and stops before re-entering one, so self-referential types terminate (the field itself stays a valid leaf path; only the unbounded descent is cut). Finite models are unaffected. - -- a211c12: Fix projection schema typing for union members and computed projections. -- 347af48: Fix schema interface widening regression introduced in 4.0.0-beta.255 (changeset `olive-onions-lose`). - - The original refactor annotated branded schema constants with interfaces extending `S.Codec & WithDefaults<...>`. That widened the underlying `BrandedSchema<...>` chain and dropped phantom slots (`Iso`, `~type.make.in`, `~type.parameters`, `Rebuild`, etc.) that downstream combinators read. In consumer projects this surfaced as `DecodingServices` / `EncodingServices` leaking as `unknown` — for example `Q.project(schema, "project")` failing with `Type 'unknown' is not assignable to type 'never'`, and RPC handler / generator-return shape mismatches against `Effect`. - - Each `*Schema` interface now extends the concrete underlying chain (e.g. `BrandedSchema`) and adds a call signature for `withDefaultMake` (and an explicit `withConstructorDefault` field for the numeric and `StringId` schemas). The runtime values are unchanged and the type-display improvement is preserved. - - Affected: `NonEmptyString*`, `Min3String255`, `StringId`, `Url`, `PositiveInt`, `NonNegativeInt`, `Int`, `PositiveNumber`, `NonNegativeNumber`, and `brandedStringId`. - -- bd26832: Add nested anyOf flattening to JSON Schema post-processing -- 3053760: fix: restore context map container behavior -- 52b0b01: fix TaggedUnion -- 55c6572: update packages -- 52b0b01: Tighten schema helper generics in `Schema/ext.ts` so default-related helpers accept properly named schema type parameters instead of bare `S.Top` inputs. -- c991be1: update packages -- d738811: remove double prefix -- 8fffc3c: cleanup -- 52b0b01: Migrate `Schema/ext` context helpers to Effect v4 by implementing `provide` and `contextFromServices` with proper service subtraction across decoding and encoding requirements. -- a788432: fix circular -- 10e90d5: fix: TaggedRequest should be an Opaque Schema Class -- 4b95009: use Finite instead of Number -- 52b0b01: Replace `ForceVoid` in `makeClient` with a typed codec transformation instead of an unsafe cast. -- 178480a: Fix request handler input classification to use request schema fields instead of `make` parameters, preventing defaulted/nullable input fields from being treated as no-input handlers. -- 985176b: Align request handler input typing with the request's `make` signature. Handlers are now classified as no-input only when the request schema declares no payload fields; any payload (even fully-optional) yields a function handler whose input matches `make`'s first parameter. Adds `HandlerInput` and threads it through `CommandFromRequest`. -- dc465e3: update to latest effect beta -- 21017d5: `InvalidationSet.add` accepts arrays and is exposed as a static shortcut. - - - Single item: `yield* InvalidationSet.add(UserRsc.GetMe)` - - Batch: `yield* InvalidationSet.add([UserRsc.GetMe, ["custom", "key"]])` - - Skips the `.use(_ => _.add(...))` boilerplate. - - Existing `InvalidationSet.use(_ => _.add(...))` form still works; the service - identity is preserved via `Object.assign` so `Effect.provideService` and - `.use` / `.useSync` continue to operate on the same `Context.Reference`. - -- 0541f0d: fix withDefault types -- 8c645d5: update to latest effect -- 30c512d: fix rpc middleware context type issue -- 50b022e: Add generated opaque model facades that expose static Type, Encoded, Make, and Schema declarations without leaking the private struct schema type to downstream project references. -- 52b0b01: adapt isObject change -- b952f19: bye cruft -- a37aa38: Update to effect beta 43 -- 52b0b01: again -- c1e73de: -- d867272: the return of `Context` -- 774a9b3: `MiddlewareMaker.makeMiddlewareBasic` now derives each middleware's effective error from both the static `error` field on the tag AND the `rcm` config entry referenced by `dynamic.key`, rather than relying on the static field alone. - - Middlewares declared with `dynamic: RequestContextMap.get("foo")` (instead of an explicit static `error: ...`) end up with `tag.error = Schema.Never` at runtime — `RpcMiddleware.Tag` defaults the static error to `Never` when not provided. The composite `MiddlewareMaker.Tag(...).middleware(...)` walked `make[*].error` to build its own error union, collapsing to `Union ≡ Never`. - - `Rpc.exitSchema` walks `rpc.middlewares[*].error` when building the wire-level failure union for every rpc kind. Empty-union meant middleware-thrown errors (`NotLoggedInError`, `UnauthorizedError`, etc.) never reached the wire schema. Query/command happened to work because their wire `errorSchema = resource.error` already covered the merge from `makeRpcClient`. Stream rpcs have `errorSchema` force-set to `Schema.Never` by effect-rpc, so the resource-level merge never reached the wire — middleware errors decoded as "Expected never, got X". - - Per middleware, the new logic pushes both the static `_.error` (if non-`Never`) and `rcm[_.dynamic.key].error` (if non-`Never`) into the composite's failure union. - -- d67d17a: Source middleware errors exclusively from the rpc middleware tag, and move command/stream invalidation wrap/unwrap entirely into the routing layer (server) and `apiClientFactory` (client). `InvalidationMiddleware` and `InvalidationMiddlewareLive` are removed. - - ### Resource error schemas - - Three sites that used to fold `RequestContextMap[*].error` into a request's own error schema now stop doing so: - - - `makeRpcClient` / `makeRequestClass` — `failureSchema` is just `config.error` (still merged with the optional `generalErrors` parameter, which is the only remaining error mix on both type and runtime levels). - - `MiddlewareMaker.rpc()` — `error: options.error` only; the previous union with `rcm.config[*].error` is gone. - - Routing and `apiClientFactory.makeRpcGroupFromRequestsAndModuleName` — `Invalidation.makeCommandRpc` is called with `error: resource.error` (no widening with the composite middleware error union). - - Middleware errors reach the client through the rpc's `middlewares[*].error` failure-union channel of `Rpc.exitSchema`, exposed by attaching the middleware tag to the rpc on both sides: - - - **Server**: `makeRouter(middleware)` attaches the live composite tag (existing behavior). - - **Client**: new `middleware` option on `ClientForOptions` / `ApiClientFactory.makeFor(layer, { middleware })` attaches the same tag schema-only (no Live invoked). Threaded through `makeRpcGroupFromRequestsAndModuleName` to `RpcGroup.middleware(tag)`. Without it, stream rpcs (whose top-level `errorSchema` is forced to `Never` by effect-rpc) hit `SchemaError: Expected never | { _tag: "error", ... }` decoding middleware-thrown errors that bypass the in-stream `Stream.catch` wrap. - - **Migration**: handlers that yield errors previously sourced from rcm (e.g. `yield* new UnauthorizedError()`) now require those errors to be declared explicitly on the resource — `Req.Query()("...", fields, { success, error: UnauthorizedError })`. The handler error type no longer auto-includes the rcm union. - - ### Invalidation wrap/unwrap - - - `routing.ts` (server) provides a per-request `InvalidationSet` for commands, wraps the success value as `CommandResponseWithMetaData`, and converts handler-thrown failures into `CommandFailureWithMetaData` so accumulated invalidation keys reach the client on either path. Stream wrap (per-chunk envelope + final `done` chunk) was already in routing and is unchanged. - - `apiClientFactory.ts` (client) `unwrapCommand` strips both envelopes and forwards keys to `InvalidationKeysFromServer`. - - `InvalidationMiddleware` (the tag) and `InvalidationMiddlewareLive` (the layer) are **removed**. The middleware was the previous home of the wrap; with the wrap moved to routing/apiClientFactory, the middleware became a thin pass-through and is no longer needed. `DefaultGenericMiddlewares` and `DefaultGenericMiddlewaresLive` shrink accordingly — no migration needed for callers that used the defaults; callers that referenced `InvalidationMiddleware` / `InvalidationMiddlewareLive` directly should drop those imports. - - Middleware-thrown errors are never wrapped: by definition the handler never ran, so there is nothing to invalidate. They flow raw on the Cause and the client decodes them via the middleware-tag failure-union channel described above. - -- 8f09f77: fix -- 50ce7e6: Replace typescript-eslint with oxlint-tsgolint for type-aware lint. Drop ESLint entirely from non-vue packages (cli, effect-app, infra) — they now use only `oxlint --type-aware`. Vue packages keep ESLint to run `@effect-app/no-await-effect` (no tsgolint equivalent) via `@typescript-eslint/parser` + `vue-eslint-parser`. -- d71d976: fix -- 2aa8e5e: `queryInvalidation` / `invalidatesQueries` accept shorthand entries (per-mutation, Command, client-level `QueryInvalidation` maps, and server-side `Req.Command` `invalidatesQueries` callbacks). - - Each entry returned may now be: - - - a raw query key (`string[]`) - - an RPC handler (`{ id, options? }`) — its query key is derived via `makeQueryKey` - - the existing `{ filters, options }` raw tanstack-query invalidation - - ```ts - queryInvalidation: (queryKey) => [queryKey, GetMe, PackListIndex]; - ``` - - equivalent to: - - ```ts - queryInvalidation: (queryKey) => [ - { filters: { queryKey } }, - { filters: { queryKey: makeQueryKey(GetMe) } }, - { filters: { queryKey: makeQueryKey(PackListIndex) } }, - ]; - ``` - -- 29a1e57: Repository `changeFeed` is now namespace-aware. Events carry the `storeId` namespace as a third tuple element (`ChangeFeedEvent = [items, op, namespace]`). `subscribe` accepts `options.namespace` to register a per-namespace handler; omitting it registers a wildcard handler that receives events from every namespace. Per-namespace handler buckets eliminate cross-namespace fan-out and avoid waking handlers for irrelevant tenants. -- 52b0b01: Align effect-app tagged union helpers with Effect v4 by delegating to the native tagged union utilities, exposing v4-style `cases` and `guards`, and preserving the local `tags` helper. -- 52b0b01: Update Effect dependencies to 4.0.0-beta.31 across workspace packages. -- 6fff09c: unify encoded function for when you use encodedKeys -- 11422f8: Update request helper typing and runtime invocation to rely on schema `.make` instead of class constructors, avoiding `new`-based assumptions for request schemas. -- eb28ea5: bogus -- 52b0b01: Change `Schema.Class` and `Schema.TaggedClass` wrappers to default constructor options to `{ disableValidation: true }`. - - This avoids strict class identifier validation by default when constructing wrapper classes (for example passing a compatible view class), while keeping existing behavior when explicit options are provided. - -- 8f1cf6a: able to configure schema concurrency -- ee9694e: fix type issue? -- 52a31dd: Reduce schema type complexity by exposing explicit interfaces for branded string and number schemas. - - Each branded constant in `Schema/strings.ts`, `Schema/moreStrings.ts`, and `Schema/numbers.ts` now has a named interface (e.g. `NonEmptyStringSchema`, `StringIdSchema`, `PositiveIntSchema`, `UrlSchema`) annotating its `export const`. Mirrors the pattern used in `effect/Schema` itself — TypeScript reports the interface name instead of expanding the full pipe/brand/extension chain, which shrinks inferred types in consumer code and speeds up type display. - - No runtime or API surface changes; `brandedStringId` now returns `BrandedStringIdSchema` (same shape as before, just named). - -- 52b0b01: fix withDefault -- ca94edf: fix typo -- d1c15d3: Tighten projectComputed projection schema typing against encoded repo fields and computed output types. -- a1b59bc: Allow query projections to use a repository's full union schema. -- dc07df5: Fix nested DTO subset projection inference for tagged unions. -- 0fe925d: Tag-aware `ProjectableFromDomain` for `projectComputed`: projection Encoded fields must exist on the matching domain tagged state (or be computed). Prevents Overview.List SchemaErrors when cancel states omit workflow lock fields like `activeRequest`. -- 40585ca: Keep tag-aware `ProjectableFromDomain` for `projectComputed` only; restore loose key-presence guard for `project()` so view DTOs with reshaped fields keep typechecking. -- 5ac46cb: Make `Schema.provide` dual (pipeable) -- a354345: Move release tsconfig flattening from publish to pack lifecycle so package configs are restored before registry upload/auth can fail. -- 52b0b01: Refactor `Pure` helpers to use typed `useSync` calls instead of unsafe casts. -- 48d9f36: Memory, SQLite, and Postgres `Store.queryRaw` now run over stored JSON documents, not Encoded rows. `Repository.queryRaw` already applies `toCodecJson` to the projector output, so native Encoded `Date` / `Map` / `Set` values no longer fail `Expected JSON value`. Cosmos was already JSON and is unchanged. - - Memory projectors that assumed native Encoded `Date` / `Map` / `Set` values now see the stored JSON form (ISO strings, arrays). - -- 52b0b01: Update Effect dependencies to 4.0.0-beta.28 across workspace packages. -- 52b0b01: fix: isA -- d31253f: Refactor eligible schema classes and tagged classes to Opaque schemas, and migrate constructor call sites to use `.make` for those models. -- 5615e47: bs -- 3e46e7b: allow excess props in openapi schemas -- cec026d: update packages -- 88838fb: Remove pick/omit customizations from Class/TaggedClass/Struct/TaggedStruct. Use `Struct.pick(X.fields, [...])` from `effect-app` instead. -- 3bae238: Remove lodash type imports from utils.ts, replacing with native TypeScript equivalents. -- b2e438f: Remove Operations service and repo -- f150cf9: Remove obsolete queue machinery left unused after the move to cluster entities: the SB/SQLite/mem `QueueMaker` implementations (`QueueMaker/{SQLQueue,memQueue,sbqueue}.ts`), their `ServiceBus.ts`/`memQueue.ts` transports, and `RequestFiberSet.ts` (its `setRootParentSpan` helper is inlined into `MainFiberSet`). `effect-app/QueueMaker` keeps only `QueueMeta`; the `QueueBase` interface and empty `QueueMaker` ops object are dropped. -- 0c88f78: Remove `TaggedError` compatibility re-export, use `TaggedErrorClass` directly -- 261470f: Remove the local `Schema.Void` / `SchemaAST.Void` override. effect `4.0.0-beta.90` ships the TypeScript `void` parser semantics upstream (`SchemaAST.Void.getParser()` → `fromAnyToConst(undefined)`, the effect-smol PR #2475 behavior), so `S.Void` already accepts any present value and discards it to `undefined`. `Schema.Void` now re-uses effect's `S.Void` and `SchemaAST` is a plain re-export of `effect/SchemaAST`. The unused `Void_` alias is dropped. No behavior change. -- 8792221: Rename `withDefault` schema extensions to `withConstructorDefault` for clarity. - - Document that `.withConstructorDefault` is **construction-only** (applied during `.make(...)` when a field is omitted) and is **not** applied during decode — it cannot be used to JIT-migrate database fields. Per-property JSDoc on every `.withConstructorDefault` / `.withDecodingDefaultType` exposed by `Schema/ext.ts`, `Schema/numbers.ts`, `Schema/moreStrings.ts`, and `ids.ts` so the caveat is visible on hover. - - Re-export `withConstructorDefault`, `withDecodingDefault`, `withDecodingDefaultKey`, `withDecodingDefaultType`, and `withDecodingDefaultTypeKey` from `effect-app/Schema` with explicit JSDoc. `withDecodingDefault*` is discouraged for persisted data: a missing field may be data corruption rather than an old-shape document, and silently substituting a default hides the problem. Prefer an explicit, preferably versioned migration of database data over decode-time fallbacks. - -- 7bd8234: Use set-backed data dependencies and provide request-scoped dependency services through a shared root-checked helper. -- 54bfc59: Require middleware to flow through `makeRpcClient` and the live layer through `makeRouter`. - - ### `makeRpcClient(middleware, generalErrors?)` - - Signature drops the `rcs` (request-context map wrapper) parameter. `rcs` was only load-bearing on the type side for `RequestConfig` inference; that information is now derived from `middleware.requestContextMap`. `middleware` is required — the previous "rcs + optional middleware" overload is gone. - - **Migration**: - - ```diff - -makeRpcClient(RequestContextMap, undefined, AppMiddleware) - +makeRpcClient(AppMiddleware) - ``` - - For tests/clients without a real middleware, build a minimal stub (`{ requestContextMap, requestContext }`) or pass any value satisfying `ClientMiddleware`. - - ### `makeRouter(middlewareLive)` - - `makeRouter()` no longer infers the live middleware layer from `meta.middleware.Default`. The Live layer is now passed explicitly to `makeRouter`, and the request classes only carry the middleware tag (schema-only). This decouples the router from any assumption that the middleware tag exposes a `Default` static. - - **Migration**: - - ```diff - -export const { Router, matchAll } = makeRouter() - +export const { Router, matchAll } = makeRouter(AppMiddleware.Default) - ``` - -- ad0ede6: Keep tag-aware `ProjectableGuard` on both `project()` and `projectComputed` (no loose key-only paper-over). - - Hardening: - - - single-literal tags allow dual same-tag domain variants (KeysOfUnion of matched members) - - multi-tag / string tags still require keys on every matched member - - optional projection/domain keys checked by key presence only (not optional-assignability) - - Call sites must project domain-owned fields per tagged state. - -- 52b0b01: fix request attr -- 9ea024d: Improve repository schema encode/decode telemetry for event-loop tail analysis. - - - widen `app.schema.{encode,decode}.duration` histogram buckets into multi-second stalls - - count `app.schema.slow` (duration ≥ 100ms) with `app.entity` / operation attributes for alertable rates - - annotate spans with `app.schema.slow` and, on encode, `app.entity.state` from the first item's `_tag` - -- e6f2341: Add SecretURL tests and switch secretURL config to use Config.nonEmptyString -- 2495ace: `InvalidationSet.add` accepts RPC handler shorthand. - - Server-side handlers may now pass an RPC handler object directly to - `InvalidationSet.add`; its query key is derived via `makeQueryKey`. Raw - `InvalidationKey` arrays continue to work. - - ```ts - // before - yield * - Invalidation.InvalidationSet.use((_) => _.add(makeQueryKey(UserRsc.GetMe))); - - // after - yield * Invalidation.InvalidationSet.use((_) => _.add(UserRsc.GetMe)); - ``` - -- 0b3e00e: fix bug -- 52b0b01: fix Req -- c215db8: align TaggedUnion with array arg -- 12abb55: Refine schema copy behavior by keeping class copy constructor-based while using structural copy for Struct and TaggedStruct helpers. -- 0cff7c1: workaround middleware error issue -- c1a6fdc: fix proxify -- 8ae8b53: input mess -- a5248a9: use new schema features -- 0e824ef: add missing Brand -- 52b0b01: fix RequestName -- eb06b32: improve root union select -- 52b0b01: Update Effect dependencies to 4.0.0-beta.29 across workspace packages. -- 52b0b01: switch to NdJson -- acfdc9e: Point development package exports at TypeScript sources while keeping published exports on compiled dist files. -- 1186b09: improve branded types, keep types through Rebuild -- a69da09: Publish from generated staging directories so release-only files are created outside the source package tree. -- 3c1f52d: improve: class strictness enabled by default again, allow `strict: false` as opt out for now. -- 0054611: Stream RPC: drain read/write data-dependencies per emitted value chunk instead of sending the - cumulative set on every metadata chunk. Each metadata chunk now carries only the delta recorded - since the last chunk, the bucket is cleared for the next segment, and the terminal "done"/"error" - chunks drain the remainder. The emit condition also broadens to include non-empty reads, so stream - queries forward their read-dependencies mid-stream too. The client already accumulates deltas into - its per-call recorder, so no FE change is required. -- 583393f: Default the stream `mutateStream` execute resolved value to the request's success type when no `final` schema is declared. - - Previously the type defaulted to `void`, but the runtime already resolves with the last emitted value. Types now match runtime behaviour: `execute` returns `Final` if a `final` schema is set, otherwise the success type. - -- 459697f: Restore native Arbitrary for the schemas that lost custom fast-check `toArbitrary`. `StringId` uses `toCodecArbitrary` (the native replacement): generate a 210-byte `Uint8Array` and run `customRandom(urlAlphabet, 21, …)` — same as the old `StringIdArb`. JSON stays a branded string via `toCodec`. `Url` is `https://…`. `RequestId` samples unique nanoid-shaped ids. `Finite` generation is capped at ±1e6. `generateFromSchema` jumps its master seed per call so successive `count: 1` draws do not collide on attempt-0 edge strings. -- 3eda52e: Fix `StructFacade` to extend effect-app's own `Struct`, not `effect/Schema`'s. - - `StructFacade` was built on `Omit, ...>` where `S` is `effect/Schema` (core). Scanner / consumer models are effect-app `S.Struct`, which is a distinct type from effect core's `Struct`, so a facade built on the core `Struct` is not assignable where an effect-app struct is expected — e.g. workflow payloads (`Workflow<…, Struct>`) and other effect-app `Struct`-shaped positions failed to type-check. Switch the `Omit` base to effect-app's `Struct` (the `Fields extends S.Struct.Fields` constraint stays effect core, matching effect-app's own `Struct`). - -- b52b424: restore annotations for now -- 7c25dbb: Add relaxed wrapper support for `ErrorClass` and `TaggedErrorClass` in `effect-app/Schema`, matching the existing class wrapper behavior (`copy`, cached `ast`, and unbounded parse concurrency). -- b8b9080: update packages -- b90fa30: Repository `changeFeed` now broadcasts synchronously: `publish` awaits every subscribed handler before resolving. Replaces `PubSub.PubSub` with a `ChangeFeed` interface (`publish` + scoped `subscribe`). Handlers are auto-removed when the subscriber's scope closes, and the full handler set is cleared when the repository's scope closes. Repository construction now requires `Scope` — wire `makeRepo` through `Layer.scoped` / `Effect.scoped`. -- 57db551: Split `TaggedRequestFor` into `Query` and `Command` factories, and mark generated request classes with `type: "query" | "command"`. - - Vue client helpers now expose query-only helpers (`query`, `suspense`, `fetch`) for query requests and mutation-only helpers (`mutate`, `fetch`) for command requests. - -- 52b0b01: remove `fields` constraint from TaggedUnion/tags helpers, extract `_tag` via AST walk instead -- dbcc53b: Refactor command invalidation typing: declare resources via `Command()`, pass `invalidatesQueries` as the optional 4th argument, and enforce exact `clientFor` invalidation resources when required. -- 52b0b01: Configure Changesets fixed versioning for public packages. -- 140e192: Relax invalidation resource value constraints to allow arbitrary values while preserving query-only filtering in invalidation handling. -- 9992e70: pass options -- 24f0a5a: rename Literal to Literals, no longer hiding Schema's singular Literal. -- 256ae85: cleanup -- 52b0b01: update effect to 4.0.0-beta.37 and drop the Schema Class disableValidation workaround now that the patched effect schema covers it -- fac725d: update effect to latest beta -- 412f08b: Include changed entity IDs in repository data dependencies, with explicit query scopes and configurable write aliases, while retaining coarse collection dependencies. -- 79eb019: Remove redundant schema `title` annotations when they duplicate the schema `identifier`. -- 6ae3050: Preserve class annotation parseOptions in relaxed declaration struct decoding so custom parse options (including concurrency defaults) are applied consistently. -- f353d48: Rename the Class/TaggedClass relaxed declaration option to `strict` (default `false`) and apply it to `Class`, `TaggedClass`, `ExtendedClass`, and `ExtendedTaggedClass`. - - When `strict: true`, class decoding keeps strict class-level declaration behavior; by default, decoding remains relaxed and preserves field-level schema errors. - -- 52b0b01: Fix `TaggedRequest` no-config error type inference so requests without a third argument infer the same default error schema as requests with explicit success config. -- 01bab22: Work around tsgo failing to reduce `S.Codec.DecodingServices` (`X extends Top ? X["DecodingServices"] : never`) in generic positions, which left `unknown` and polluted the `R` channel of client handlers and RPC middleware failure context. Since the schemas involved are already constrained to `S.Top`, read `["DecodingServices"]` directly in `RequestHandlerFor`, `TagClass.FailureContext`, and the Vue `MutationExt` / `QueryProjection` types. -- 2a86a17: improve tsgo compat: avoid deferred `Schema.Type`/`Codec.Encoded`/`Codec.DecodingServices`/`Codec.EncodingServices` conditional helpers in generic positions where the type parameter is already constrained to `Schema.Top`. Index the property directly (`X["Type"]`, `X["Encoded"]`, `X["DecodingServices"]`, …) so tsgo doesn't leak `unknown` into `Effect` channels (notably `R`). - - Sites: `client/clientFor.ts` (`RequestHandlerFor`, `FinalTypeOf`, `ExtractResponse`, `ExtractEResponse`), `client/makeClient.ts` (`InputFromPayload`, `OutputFromSuccess`, `InvalidationConfigForCommand`, `TaggedRequestWithMeta` overloads), `rpc/MiddlewareMaker.ts` (`Errors`), `rpc/RpcMiddleware.ts` (`Failure`, `FailureContext`), `Schema/ext.ts` (`ReadonlySetFromArray`, `ReadonlyMapFromArray`), `infra/routing.ts` (`GetSuccessShape`, handlers, route matcher), `vue/makeClient.ts` (`MutationExt.project`, `MutationWithExtensions`, `QueryProjection`), `vue/routeParams.ts` (`parseRouteParams*`). - -- 52b0b01: update all teh tings -- c3299f7: update packages -- 6b57330: Upgrade Effect packages to npm `4.0.0`. -- e2d00b5: Upgrade Effect packages to npm `4.0.1`. -- 547e2e1: Update effect packages to `4.0.0-beta.107` (from `beta.90`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`, and `fast-check` to `^4.9.0`. Sync `repos/effect` subtree from `Effect-TS/effect` (effect-smol stopped publishing tags after beta.98). - - API adaptations for beta.107: - - - `concurrency: "inherit"` → `"unbounded"` - - `Schema.ErrorClass` / `TaggedErrorClass` → `Schema.Error` / `TaggedError` - - `Schema.LazyArbitrary` → `Schema.Arbitrary` - - `Schema.DateValid` / `isDateValid` removed (`Schema.Date` rejects invalid dates) - - `SchemaIssue` constructors no longer take `Option` (annotations + input) - - filter meta via `annotations.representation` instead of `annotations.meta` - - `context.defaultValue` → `context.constructorDefault` (single Link) - - Class detection via `~constructor` + static `identifier` - - Redacted detection via `representation.id` - - localized StandardSchema hooks updated for new issue/input model - - provide `NodeCrypto.layer` for cluster sqlite tests - - default `sync-effect` subtree URL → `Effect-TS/effect` - -- 52b0b01: update effect to 4.0.0-beta.36, adapt to Option revert from A | undefined -- Update effect packages to 4.0.0-beta.52 -- ea32222: Update to effect 4.0.0-beta.60 and use native `Rpc.custom` constructors (`makeCommandRpc`, `makeStreamRpc`) for metadata-wrapped RPC schemas instead of manually wrapping/unwrapping schemas inline. -- 7ca66ce: Update to effect 4.0.0-beta.66. Remove `Yieldable` and `asEffect()` (service tags are now `Effect` directly). -- 57a1862: Update to effect 4.0.0-beta.67. Switch deps from `pkg.pr.new` snapshot back to npm beta tag. -- 3e855bc: Update Effect packages to `4.0.0-beta.83` (from `beta.74`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/sql-sqlite-node`, `@effect/atom-vue`, `@effect/vitest`. - - Adapt the infra workflow engines to beta.83 API changes: - - - `Schema.Defect` is now a constructor function — use `S.Defect()` when building the deferred-exit codec (the bare constant no longer produces a usable schema and crashed `toType`). - - `Workflow` exposes its name as `_tag` instead of `name`. `WorkflowEngineSqlite`/`WorkflowEngineCosmos` now key the registry, codec caches, and persisted `workflow_name` off `workflow._tag`, fixing crash-recovery (stale-lease re-drive previously registered under an `undefined` key and never matched). - -- 78d076a: Update effect packages to `4.0.0-beta.84` (`effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`). -- ffd140f: Update effect packages to `4.0.0-beta.86` (from `beta.84`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. -- e8842aa: Update effect packages to `4.0.0-beta.88` (from `beta.86`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Also bump `@effect-app/cli` to `2.1.0-beta.35`. No source changes required — typecheck and tests pass unchanged. -- b6dda09: Update effect packages to `4.0.0-beta.90` (from `beta.88`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. -- 0263827: Update to effect `pkg.pr.new` snapshot at `a42ef66` (4.0.0-beta.66). Remove `Yieldable` and `asEffect()` (service tags are now `Effect` directly). -- 8bd5bfe: Update effect packages to `4.0.0-rc.112` (from `beta.107`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Sync `repos/effect` subtree from `Effect-TS/effect` at `effect@4.0.0-rc.112`. - - API adaptations for rc.112: - - - cluster encoded driver `resetAddress` → batched `resetAddresses` - - Cosmos `unprocessedMessages` honors optional `limit` / `addresses` (only claimed rows are returned) - - Service Bus `Runners.make` supplies `codecFor` for schema-aware RPC serialization - - `pnpm subtree:effect` passes `--url https://github.com/Effect-TS/effect.git` (published CLI still defaults to effect-smol) - - JSON Schema check constraints are compacted onto the parent (`minLength`/`maxLength` instead of `allOf`) - -- 2231ef8: Upgrade Effect packages to npm `4.0.0-rc.115`. Drop the pkg.pr.new pin used while `effect@4.0.0-rc.114` was unpublished. -- c7bbc41: fix - class should check type side -- 52b0b01: fix ForceVOid -- 52b0b01: fix ForceVoid for real - -## 4.0.0-beta.333 - -### Patch Changes - -- e2d00b5: Upgrade Effect packages to npm `4.0.1`. - -## 4.0.0-beta.332 - -### Patch Changes - -- 6b57330: Upgrade Effect packages to npm `4.0.0`. - -## 4.0.0-beta.331 - -## 4.0.0-beta.330 - -### Patch Changes - -- 48d9f36: Memory, SQLite, and Postgres `Store.queryRaw` now run over stored JSON documents, not Encoded rows. `Repository.queryRaw` already applies `toCodecJson` to the projector output, so native Encoded `Date` / `Map` / `Set` values no longer fail `Expected JSON value`. Cosmos was already JSON and is unchanged. - - Memory projectors that assumed native Encoded `Date` / `Map` / `Set` values now see the stored JSON form (ISO strings, arrays). - -## 4.0.0-beta.329 - -### Patch Changes - -- 459697f: Restore native Arbitrary for the schemas that lost custom fast-check `toArbitrary`. `StringId` uses `toCodecArbitrary` (the native replacement): generate a 210-byte `Uint8Array` and run `customRandom(urlAlphabet, 21, …)` — same as the old `StringIdArb`. JSON stays a branded string via `toCodec`. `Url` is `https://…`. `RequestId` samples unique nanoid-shaped ids. `Finite` generation is capped at ±1e6. `generateFromSchema` jumps its master seed per call so successive `count: 1` draws do not collide on attempt-0 edge strings. - -## 4.0.0-beta.328 - -### Minor Changes - -- 3a8c710: Store document decode fails as a typed `SchemaError` instead of a defect; repositories keep their public error channels by dying at the boundary; `validateSample` reports documents that fail at the store boundary. - -## 4.0.0-beta.327 - -## 4.0.0-beta.326 - -### Minor Changes - -- 2ebf8ae: `jitM` runs at the store boundary as JSON → JSON, before the `toCodecJson(toEncoded(schema))` decode; repositories decode the Encoded shape only. - - `jitM` used to be applied by the repository _after_ the store had already decoded the document, and was typed `(pm: Encoded) => Encoded` - a lie, since jitMs are written against the stored JSON and `Encoded` holds native `Date`/`Map`/`Set` values. - - The read pipeline is now: raw JSON document → merge `defaultValues` (unchanged: they only fill _absent_ keys, so an explicitly stored `null` reaches `jitM`) → `jitM` (JSON → JSON) → decode `Schema.toCodecJson(Schema.toEncoded(schema))` → Encoded. The repository then decodes Encoded → the domain type with `schema` and no longer applies `jitM` at all. Because `jitM` now runs before any schema decode sees the document, it can repair legacy shapes including explicit `null`s. - - - `StoreConfig.jitM?: (json: JsonRecord) => JsonRecord` is new, where `JsonRecord` (exported from `effect-app/Store`) is `{ readonly [key: string]: Schema.Json }`. It is not applied on the write/encode path, and never receives `_etag`. - - `RepositoryOptions.jitM` changes type from `(pm: Encoded) => Encoded` to `(json: JsonRecord) => JsonRecord` and is forwarded into the store config. - - **Migrating a jitM:** it now receives and must return JSON. A `Date` field arrives as an ISO string (return a string, not a `Date`), a `ReadonlySet` as an array, a `ReadonlyMap` as an array of `[key, value]` pairs. Field access is index-signature based (`json["x"]`) rather than typed property access. - - The JSON→Encoded decode stays strict: a document `jitM` does not repair fails at the store boundary instead of being read back half-decoded. - - `ValidationError.jitMResult` is deprecated: `validateSample` only ever sees the store's output, so it is identical to `rawData`. - -## 4.0.0-beta.325 - -### Patch Changes - -- f233f3d: `PositiveNumber` and `NonNegativeNumber` bound native Arbitrary generation to at most 1,000,000 (`arbitraryConstraint` on their existing checks), so generated fixtures no longer overflow derived totals to `Infinity`. Validation and JSON Schema output are unchanged. - -## 4.0.0-beta.324 - -### Patch Changes - -- a0075b8: `Email` and `PhoneNumber` carry native `arbitraryConstraint` patterns, so `effect/unstable/arbitrary` (and `generateFromSchema`) can sample them instead of exhausting rejection sampling on their `refine` guards. - -## 4.0.0-beta.323 - -### Minor Changes - -- ab289d4: Add opt-in `autoscaleMaxThroughput` for Cosmos containers created on demand (`StorageConfig`, `ClusterCosmosConfig`, `WorkflowEngineCosmosConfig`). When set, a missing container is created with autoscale at that max RU/s, unless its database has shared throughput (then it keeps sharing the database pool). Existing containers are untouched; unset keeps the Cosmos default of manual 400 RU/s per container. - -## 4.0.0-beta.322 - -### Patch Changes - -- 2231ef8: Upgrade Effect packages to npm `4.0.0-rc.115`. Drop the pkg.pr.new pin used while `effect@4.0.0-rc.114` was unpublished. - -## 4.0.0-beta.321 - -### Patch Changes - -- 18bae5b: Upgrade Effect packages to `4.0.0-rc.114` (core `effect` via pkg.pr.new at `cdb3ac91` until npm publishes that version). Also bump independently versioned `@effect/language-service` to `0.87.2` and `@effect/tsgo` to `0.45.0` (requires `oxlint` `1.82.0`). - - Breaking API updates from this RC: - - - 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` - - `SchemaTransformation.transformOrFail` renamed to `transformEffect` - - `Fiber.currentSpan` moved to `fiber.cache.span` - - HTTP server addresses are `InetAddressV4`/`InetAddressV6` instead of `TcpAddress` - - Object JSON Schema now emits `additionalProperties: true` - - Schema `Union` AST stores matching as `options.mode` instead of `mode` - -## 4.0.0-beta.320 - -## 4.0.0-beta.319 - -## 4.0.0-beta.318 - -### Patch Changes - -- 8bd5bfe: Update effect packages to `4.0.0-rc.112` (from `beta.107`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Sync `repos/effect` subtree from `Effect-TS/effect` at `effect@4.0.0-rc.112`. - - API adaptations for rc.112: - - - cluster encoded driver `resetAddress` → batched `resetAddresses` - - Cosmos `unprocessedMessages` honors optional `limit` / `addresses` (only claimed rows are returned) - - Service Bus `Runners.make` supplies `codecFor` for schema-aware RPC serialization - - `pnpm subtree:effect` passes `--url https://github.com/Effect-TS/effect.git` (published CLI still defaults to effect-smol) - - JSON Schema check constraints are compacted onto the parent (`minLength`/`maxLength` instead of `allOf`) - -## 4.0.0-beta.317 - -### Minor Changes - -- 186de3a: Stop forcing Date/Map/Set Encoded shapes to JSON. - - `Schema.Date` / `ReadonlySet` / `ReadonlyMap` now keep native Encoded types (`Date`, `Set`, `Map`). Use `DateFromString`, `ReadonlySetFromArray`, and `ReadonlyMapFromArray` when the Encoded form must be JSON. The query DSL accepts those native values, including array ops (`includes` / `in` / `includes-any`) on `Date[]` and `ReadonlySet` fields. Memory, Disk, SQL, and Cosmos convert Encoded Date/Map/Set through `Schema.toCodecJson` on write/read; query parameters and defaults lower the same way from the store schema. App types such as DateOnly stay native Encoded and JSON-lower via that schema — not a type registry. - -- 186de3a: Query maps as JSON arrays of `[key, value]` tuples. - - `where("meta", "hasKey" | "hasValue" | "hasKeyValue", ...)` (and `not*` / `*-any` / `*-all` variants) filter `ReadonlyMap` fields. Memory, Disk, SQLite, Postgres, and Cosmos compile those ops against the encoded tuple array. - -- 186de3a: JSON stores lower native Encoded values (Date, Map, Set, and app types such as DateOnly) through the store's document schema. - - `makeRepo` already passes that schema. Adapters encode documents, query parameters, and defaults with `Schema.toCodecJson(toEncoded(schema))` at the field path. No type registry. Schemaless stores still lower Date/Map/Set structurally. - -## 4.0.0-beta.316 - -## 4.0.0-beta.315 - -## 4.0.0-beta.314 - -## 4.0.0-beta.313 - -## 4.0.0-beta.312 - -### Patch Changes - -- 9ea024d: Improve repository schema encode/decode telemetry for event-loop tail analysis. - - - widen `app.schema.{encode,decode}.duration` histogram buckets into multi-second stalls - - count `app.schema.slow` (duration ≥ 100ms) with `app.entity` / operation attributes for alertable rates - - annotate spans with `app.schema.slow` and, on encode, `app.entity.state` from the first item's `_tag` - -## 4.0.0-beta.311 - -### Patch Changes - -- 547e2e1: Update effect packages to `4.0.0-beta.107` (from `beta.90`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`, and `fast-check` to `^4.9.0`. Sync `repos/effect` subtree from `Effect-TS/effect` (effect-smol stopped publishing tags after beta.98). - - API adaptations for beta.107: - - - `concurrency: "inherit"` → `"unbounded"` - - `Schema.ErrorClass` / `TaggedErrorClass` → `Schema.Error` / `TaggedError` - - `Schema.LazyArbitrary` → `Schema.Arbitrary` - - `Schema.DateValid` / `isDateValid` removed (`Schema.Date` rejects invalid dates) - - `SchemaIssue` constructors no longer take `Option` (annotations + input) - - filter meta via `annotations.representation` instead of `annotations.meta` - - `context.defaultValue` → `context.constructorDefault` (single Link) - - Class detection via `~constructor` + static `identifier` - - Redacted detection via `representation.id` - - localized StandardSchema hooks updated for new issue/input model - - provide `NodeCrypto.layer` for cluster sqlite tests - - default `sync-effect` subtree URL → `Effect-TS/effect` - -## 4.0.0-beta.310 - -## 4.0.0-beta.309 - -### Patch Changes - -- 99c43c4: Derive scoped dependency reads and writes from typed query filters and annotated model relationships, including previous aliases. - -## 4.0.0-beta.308 - -## 4.0.0-beta.307 - -### Patch Changes - -- 40585ca: Keep tag-aware `ProjectableFromDomain` for `projectComputed` only; restore loose key-presence guard for `project()` so view DTOs with reshaped fields keep typechecking. -- ad0ede6: Keep tag-aware `ProjectableGuard` on both `project()` and `projectComputed` (no loose key-only paper-over). - - Hardening: - - - single-literal tags allow dual same-tag domain variants (KeysOfUnion of matched members) - - multi-tag / string tags still require keys on every matched member - - optional projection/domain keys checked by key presence only (not optional-assignability) - - Call sites must project domain-owned fields per tagged state. - -## 4.0.0-beta.306 - -### Patch Changes - -- 0fe925d: Tag-aware `ProjectableFromDomain` for `projectComputed`: projection Encoded fields must exist on the matching domain tagged state (or be computed). Prevents Overview.List SchemaErrors when cancel states omit workflow lock fields like `activeRequest`. - -## 4.0.0-beta.305 - -### Minor Changes - -- 8bd9a11: Support ID-scoped signal dependencies and additive repository dependencies derived from previous and current entities. - -## 4.0.0-beta.304 - -### Patch Changes - -- 412f08b: Include changed entity IDs in repository data dependencies, with explicit query scopes and configurable write aliases, while retaining coarse collection dependencies. - -## 4.0.0-beta.303 - -### Patch Changes - -- 33b0544: Replace repository codec child spans with parent-span timing attributes and bounded metrics, and record database operation timing for instrumented stores. - -## 4.0.0-beta.302 - -### Patch Changes - -- c0e5a1b: Add `Schema.dropConstructorDefault`, a `Struct.Lambda` that clears a field's `withConstructorDefault`. Compose it with `Struct.omit`/`Struct.map` when deriving a partial-update schema from a "create" schema, so omitted fields' constructor defaults don't resurrect themselves in `.make(...)` output. - - Also, `makeExactOptional` now drops constructor defaults automatically, matching this behavior. - -## 4.0.0-beta.301 - -## 4.0.0-beta.300 - -## 4.0.0-beta.299 - -## 4.0.0-beta.298 - -### Patch Changes - -- 08d092c: Update Atom query caches in memory and accumulate stream invalidations so live events do not cause repeated RPC refetches. - -## 4.0.0-beta.297 - -### Patch Changes - -- 30c512d: fix rpc middleware context type issue - -## 4.0.0-beta.296 - -### Patch Changes - -- e0c4835: revert bs - -## 4.0.0-beta.295 - -### Patch Changes - -- eb06b32: improve root union select - -## 4.0.0-beta.294 - -### Patch Changes - -- a211c12: Fix projection schema typing for union members and computed projections. - -## 4.0.0-beta.293 - -### Patch Changes - -- d1c15d3: Tighten projectComputed projection schema typing against encoded repo fields and computed output types. - -## 4.0.0-beta.292 - -## 4.0.0-beta.291 - -## 4.0.0-beta.290 - -### Patch Changes - -- dc07df5: Fix nested DTO subset projection inference for tagged unions. - -## 4.0.0-beta.289 - -### Patch Changes - -- a1b59bc: Allow query projections to use a repository's full union schema. - -## 4.0.0-beta.288 - -### Minor Changes - -- aeb17bc: Add `disableQueryInvalidation` flag to Command config for background saves. - - Set `disableQueryInvalidation: true` in a `Req.Command` config (3rd argument) - to suppress all client-side query invalidation for that command — client - `invalidatesQueries` callbacks, server-returned `metadata.invalidateQueries`, - and repository-derived write-dependency matching are all skipped. Use for - background saves (e.g. debounced auto-save) whose writes should not trigger - query refetches. - - - `InvalidationConfig` gains `disableQueryInvalidation?: boolean`. - - `RequestHandlerWithInput` gains `disableQueryInvalidation?: boolean`, - propagated from `Request.config` by `ApiClientFactory.makeFor`. - - `invalidateCache` early-returns `Effect.void` when the flag is set, - applying to both regular and stream mutations. - -## 4.0.0-beta.287 - -### Patch Changes - -- 261470f: Remove the local `Schema.Void` / `SchemaAST.Void` override. effect `4.0.0-beta.90` ships the TypeScript `void` parser semantics upstream (`SchemaAST.Void.getParser()` → `fromAnyToConst(undefined)`, the effect-smol PR #2475 behavior), so `S.Void` already accepts any present value and discards it to `undefined`. `Schema.Void` now re-uses effect's `S.Void` and `SchemaAST` is a plain re-export of `effect/SchemaAST`. The unused `Void_` alias is dropped. No behavior change. - -## 4.0.0-beta.286 - -### Patch Changes - -- b6dda09: Update effect packages to `4.0.0-beta.90` (from `beta.88`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. - -## 4.0.0-beta.285 - -### Patch Changes - -- 7bd8234: Use set-backed data dependencies and provide request-scoped dependency services through a shared root-checked helper. - -## 4.0.0-beta.284 - -## 4.0.0-beta.283 - -### Minor Changes - -- 0054611: Derive query invalidation from repository read/write dependencies and propagate dependency metadata through RPC clients. - -### Patch Changes - -- 0054611: Stream RPC: drain read/write data-dependencies per emitted value chunk instead of sending the - cumulative set on every metadata chunk. Each metadata chunk now carries only the delta recorded - since the last chunk, the bucket is cleared for the next segment, and the terminal "done"/"error" - chunks drain the remainder. The emit condition also broadens to include non-empty reads, so stream - queries forward their read-dependencies mid-stream too. The client already accumulates deltas into - its per-call recorder, so no FE change is required. - -## 4.0.0-beta.282 - -### Minor Changes - -- b2df3fa: Align `Schema.Void` with TypeScript `void` return-value semantics (effect-smol PR #2475 / `b7d46ab`). - - `S.Void` now accepts **any present value** at runtime and discards it to `undefined`, while keeping the decoded/encoded type as `void` — matching a `void` return whose result callers never observe. This is implemented as an override of the AST node in a new `effect-app/SchemaAST` module (`export * from "effect/SchemaAST"` plus a `Void` subclass whose parser mirrors the PR's `fromAnyToConst(undefined)`), so there is a single canonical `Void` used everywhere, including RPC success schemas. - - - New `effect-app/SchemaAST` module; internal `SchemaAST` imports across the libs now route through it. - - Removed `ForceVoid` from `effect-app/client/makeClient` — use `S.Void` directly, which now carries this behaviour. - - `S.Void_` remains available as effect's original (`undefined`-only) Void. - -### Patch Changes - -- f150cf9: Remove obsolete queue machinery left unused after the move to cluster entities: the SB/SQLite/mem `QueueMaker` implementations (`QueueMaker/{SQLQueue,memQueue,sbqueue}.ts`), their `ServiceBus.ts`/`memQueue.ts` transports, and `RequestFiberSet.ts` (its `setRootParentSpan` helper is inlined into `MainFiberSet`). `effect-app/QueueMaker` keeps only `QueueMeta`; the `QueueBase` interface and empty `QueueMaker` ops object are dropped. -- e8842aa: Update effect packages to `4.0.0-beta.88` (from `beta.86`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. Also bump `@effect-app/cli` to `2.1.0-beta.35`. No source changes required — typecheck and tests pass unchanged. - -## 4.0.0-beta.281 - -### Patch Changes - -- ffd140f: Update effect packages to `4.0.0-beta.86` (from `beta.84`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`. No source changes required — typecheck and tests pass unchanged. - -## 4.0.0-beta.280 - -### Patch Changes - -- 3eda52e: Fix `StructFacade` to extend effect-app's own `Struct`, not `effect/Schema`'s. - - `StructFacade` was built on `Omit, ...>` where `S` is `effect/Schema` (core). Scanner / consumer models are effect-app `S.Struct`, which is a distinct type from effect core's `Struct`, so a facade built on the core `Struct` is not assignable where an effect-app struct is expected — e.g. workflow payloads (`Workflow<…, Struct>`) and other effect-app `Struct`-shaped positions failed to type-check. Switch the `Omit` base to effect-app's `Struct` (the `Fields extends S.Struct.Fields` constraint stays effect core, matching effect-app's own `Struct`). - -## 4.0.0-beta.279 - -### Minor Changes - -- 5f9cd6a: Schema/Class facades: add `StructFacade` and carry `identifier` on the class facades. - - - Add `StructFacade` = `Omit, "Type" | "Encoded" | "~type.make.in" | "DecodingServices" | "EncodingServices"> & { pinned type-level members }`. A top-level `const X = S.Struct(...)` / `S.TaggedStruct(...)` model faceted by the `.d.ts`-emit compiler is retyped to it: still a real `S.Struct` (so `Workflow.AnyStructSchema`, the `Struct` reconstruction, `Union`, `.fields.x` keep working) while `Type`/`Encoded`/make/services resolve to the named namespace interfaces. - - Add `readonly identifier: string` to `OpaqueClassFacade` and `OpaqueErrorFacadeClass` (an `S.Class` static lost from the bare emitted interface, which the compiler otherwise had to hand-emit per model). `OpaqueFacade` is intentionally left untouched — `S.Opaque` and requests build on `S.Bottom`, not `S.Class`, so they have no `identifier`. - -## 4.0.0-beta.278 - -### Patch Changes - -- a354345: Move release tsconfig flattening from publish to pack lifecycle so package configs are restored before registry upload/auth can fail. -- a69da09: Publish from generated staging directories so release-only files are created outside the source package tree. - -## 4.0.0-beta.277 - -## 4.0.0-beta.276 - -### Patch Changes - -- 32f05c8: Remove unused dependency declarations from package manifests. - -## 4.0.0-beta.275 - -## 4.0.0-beta.274 - -## 4.0.0-beta.273 - -### Patch Changes - -- 664e83d: Add Atom-native Vue query APIs, stream query pull atoms, and a TanStack-backed legacy query engine toggle. -- e4ff9a6: Fix the global `Array`/`ReadonlyArray.map` override breaking union-of-array receivers. The previous `this: NonEmptyArray`/`NonEmptyReadonlyArray` overload was selected-then-rejected on a union receiver (e.g. `(readonly A[] | readonly B[]).map(...)`), raising TS2684. Replaced with a single conditional-return signature (`this extends NonEmpty… ? NonEmpty : U[]`) that preserves NonEmpty refinement without a `this` parameter, so union-array `.map` calls type-check again. - -## 4.0.0-beta.272 - -### Patch Changes - -- 21ac90a: Unify generated model facades on `OpaqueFacade` and support class facades through encoded-key schema transforms. - -## 4.0.0-beta.271 - -### Patch Changes - -- 939bebc: Remove the local `Config.nested` / `ConfigProvider.nested` overrides. They worked around an effect v3 bug where the nested namespace segment bypassed the provider's `mapInput`/`constantCase` transform. effect `4.0.0-beta.84` fixes this upstream (its transformation-compose refactor threads the nested prefix through the provider's lookup transform), and the old overrides relied on internals (`Config.make`, `provider.get`/`mapInput`/`prefix`) that no longer exist. Both modules now re-export effect directly; behaviour for consumers (e.g. `Config.nested("cups")` against a `constantCase` env provider) is unchanged and correct. - -## 4.0.0-beta.270 - -### Patch Changes - -- 78d076a: Update effect packages to `4.0.0-beta.84` (`effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/atom-vue`, `@effect/sql-sqlite-node`, `@effect/vitest`). - -## 4.0.0-beta.269 - -### Patch Changes - -- 50b022e: Harden model facades and add `OpaqueErrorFacadeClass`. - - - `OpaqueErrorFacadeClass`: facade `TaggedErrorClass`/`ErrorClass` while keeping - `Cause.YieldableError` on the constructed instance (so `yield* new Err()`, - `Effect.fail`, and `instanceof` keep working through the facade). - - `OpaqueFacadeInput` relaxed to require only the codec service channels, so - transformed schemas (`.pipe(encodeKeys/annotate/filter/...)`) can be facaded; - `fields`/`copy`/`mapFields` flow through `OpaqueFacadeStatics` when present, and - `to` is carried so models that compose via `X.to.fields` keep working. - - Dropped the wide `fields`/`mapFields` overrides on the facade interfaces so the - precise statics win (keeps `Q.project(X.mapFields(...))` typed). - - Codegen (`eslint-codegen-model`): resolver prefers the private `_X` over the - self-referential facade; converges static service types in one run; per-model - classification (facade Opaque models, leave `Class` standard in mixed files); - `Make` emitted as `type X = {...} | void` when the make-input is voidable; - `readonly`-prefixed array/tuple elements parenthesized; value self-references in - the moved `_X` body rewritten to `_X`; instance getters surface on `Self`. - -- 50b022e: Add generated opaque model facades that expose static Type, Encoded, Make, and Schema declarations without leaking the private struct schema type to downstream project references. - -## 4.0.0-beta.268 - -### Patch Changes - -- acfdc9e: Point development package exports at TypeScript sources while keeping published exports on compiled dist files. - -## 4.0.0-beta.267 - -### Minor Changes - -- 23a7167: Add a typed, serializable `DatabaseError { message, transient, cause }` for store adapter failures. - - Store adapters wrapped DB calls in bare `Effect.promise` / `.orDie`, so a transient infra failure (request timeout, throttle, 5xx, dropped socket) became a raw-`Error` defect — which `Effect.retry` can't retry and which breaks JSON encoding of a workflow exit cause ("Expected JSON value, got Error"). `DatabaseError` is now exposed on all `Store` and `Repository` method error channels (writes: `OptimisticConcurrencyException | DatabaseError`) and added to `SupportedErrors` so the api/client/FE treat it as a 500-class error. Each adapter wraps its db-call failures into `DatabaseError` with a `transient` flag (timeout/throttle/5xx ⇒ retryable); the `cause` serializes via `Schema.Defect`. Construction/seed/DDL paths stay `orDie`. - -## 4.0.0-beta.266 - -### Patch Changes - -- bbaa67e: Fix `TS2589: Type instantiation is excessively deep and possibly infinite` when querying models that embed `Schema.Defect()` (or any field encoded as `Json`). - - The query path type `Path` (used by `Q.where`/`Q.and`/`Q.order` to type dotted field paths) recursed through object types with no termination for self-referential ones. Under effect `4.0.0-beta.83`, `Schema.Defect()` encodes as `Json`, whose `JsonObject = { readonly [x: string]: Json }` index signature made `Path` descend forever, blowing past TypeScript's instantiation limit on any model with such a field (e.g. carrier-error states carrying `raw: Schema.Defect()`). - - `Path` now threads a `Seen` set of already-entered object types and stops before re-entering one, so self-referential types terminate (the field itself stays a valid leaf path; only the unbounded descent is cut). Finite models are unaffected. - -## 4.0.0-beta.265 - -### Patch Changes - -- 3e855bc: Update Effect packages to `4.0.0-beta.83` (from `beta.74`): `effect`, `@effect/platform-node`, `@effect/platform-browser`, `@effect/sql-sqlite-node`, `@effect/atom-vue`, `@effect/vitest`. - - Adapt the infra workflow engines to beta.83 API changes: - - - `Schema.Defect` is now a constructor function — use `S.Defect()` when building the deferred-exit codec (the bare constant no longer produces a usable schema and crashed `toType`). - - `Workflow` exposes its name as `_tag` instead of `name`. `WorkflowEngineSqlite`/`WorkflowEngineCosmos` now key the registry, codec caches, and persisted `workflow_name` off `workflow._tag`, fixing crash-recovery (stale-lease re-drive previously registered under an `undefined` key and never matched). - -## 4.0.0-beta.264 - -## 4.0.0-beta.263 - -## 4.0.0-beta.262 - -### Patch Changes - -- 55c6572: update packages - -## 4.0.0-beta.261 - -## 4.0.0-beta.260 - -### Patch Changes - -- bogus - -## 4.0.0-beta.259 - -## 4.0.0-beta.258 - -## 4.0.0-beta.257 - -### Minor Changes - -- e71eb78: Tighten typing of query DSL builders so field/path parameters are constrained to actual field paths instead of bare `string`: - - - `relation(path)` now infers the relation's element type and constrains `distinctCount`/`sum`/`collect`/`collectDistinct`/`collectFields`/`collectDistinctFields` and the `unit` of `sumExprBy`/`sumExprNormalized` to `FieldPath`. - - `relation(path).expr` exposes a scope-bound math-expression builder so `expr.field(...)` inside `sumExpr`/`sumExprBy`/`sumExprNormalized` is typed against the relation element. - - Top-level `expr.field` accepts an optional generic for opt-in tightening (`expr.field("x")`). - - `aggregate(schema, build)` accepts a builder callback whose `agg` argument is bound to the source row inferred from the pipe — paths are checked without any explicit generic: `make().pipe(aggregate(schema, ($) => ({ city: $.field("address.city") })))`. Plain `AggregateMap` form still accepted. - - `projectComputed(schema, build, mode?)` accepts a builder callback whose `{ relation }` argument is bound to the source row inferred from the pipe: `make().pipe(projectComputed(out, ({ relation }) => ({ total: relation("items").sum("qty") })))`. Plain `ComputedProjectionMap` form still accepted. - - `agg()` factory remains as an escape hatch when the builder is built outside a pipe. - -## 4.0.0-beta.256 - -### Patch Changes - -- 347af48: Fix schema interface widening regression introduced in 4.0.0-beta.255 (changeset `olive-onions-lose`). - - The original refactor annotated branded schema constants with interfaces extending `S.Codec & WithDefaults<...>`. That widened the underlying `BrandedSchema<...>` chain and dropped phantom slots (`Iso`, `~type.make.in`, `~type.parameters`, `Rebuild`, etc.) that downstream combinators read. In consumer projects this surfaced as `DecodingServices` / `EncodingServices` leaking as `unknown` — for example `Q.project(schema, "project")` failing with `Type 'unknown' is not assignable to type 'never'`, and RPC handler / generator-return shape mismatches against `Effect`. - - Each `*Schema` interface now extends the concrete underlying chain (e.g. `BrandedSchema`) and adds a call signature for `withDefaultMake` (and an explicit `withConstructorDefault` field for the numeric and `StringId` schemas). The runtime values are unchanged and the type-display improvement is preserved. - - Affected: `NonEmptyString*`, `Min3String255`, `StringId`, `Url`, `PositiveInt`, `NonNegativeInt`, `Int`, `PositiveNumber`, `NonNegativeNumber`, and `brandedStringId`. - -## 4.0.0-beta.255 - -### Patch Changes - -- 52a31dd: Reduce schema type complexity by exposing explicit interfaces for branded string and number schemas. - - Each branded constant in `Schema/strings.ts`, `Schema/moreStrings.ts`, and `Schema/numbers.ts` now has a named interface (e.g. `NonEmptyStringSchema`, `StringIdSchema`, `PositiveIntSchema`, `UrlSchema`) annotating its `export const`. Mirrors the pattern used in `effect/Schema` itself — TypeScript reports the interface name instead of expanding the full pipe/brand/extension chain, which shrinks inferred types in consumer code and speeds up type display. - - No runtime or API surface changes; `brandedStringId` now returns `BrandedStringIdSchema` (same shape as before, just named). - -## 4.0.0-beta.254 - -### Patch Changes - -- 29a1e57: Repository `changeFeed` is now namespace-aware. Events carry the `storeId` namespace as a third tuple element (`ChangeFeedEvent = [items, op, namespace]`). `subscribe` accepts `options.namespace` to register a per-namespace handler; omitting it registers a wildcard handler that receives events from every namespace. Per-namespace handler buckets eliminate cross-namespace fan-out and avoid waking handlers for irrelevant tenants. - -## 4.0.0-beta.253 - -### Patch Changes - -- b90fa30: Repository `changeFeed` now broadcasts synchronously: `publish` awaits every subscribed handler before resolving. Replaces `PubSub.PubSub` with a `ChangeFeed` interface (`publish` + scoped `subscribe`). Handlers are auto-removed when the subscriber's scope closes, and the full handler set is cleared when the repository's scope closes. Repository construction now requires `Scope` — wire `makeRepo` through `Layer.scoped` / `Effect.scoped`. - -## 4.0.0-beta.252 - -### Patch Changes - -- a788432: fix circular - -## 4.0.0-beta.251 - -### Patch Changes - -- 1c858d3: fix request scope problems - -## 4.0.0-beta.250 - -### Patch Changes - -- 3053760: fix: restore context map container behavior - -## 4.0.0-beta.249 - -### Minor Changes - -- ba789a2: Move core service contracts and runtime-agnostic modules into `effect-app`, keep `infra` and `vue` focused on adapters, and drop the temporary `infra` compatibility re-export paths in favor of the new canonical imports. - - `@effect-app/infra` no longer re-exports moved core modules such as `./Model`, `./Emailer/service`, `./QueueMaker/service`, `./Store/service`, `./adapters/*`, or `./api/*` entrypoints. - -## 4.0.0-beta.248 - -### Patch Changes - -- 2a86a17: improve tsgo compat: avoid deferred `Schema.Type`/`Codec.Encoded`/`Codec.DecodingServices`/`Codec.EncodingServices` conditional helpers in generic positions where the type parameter is already constrained to `Schema.Top`. Index the property directly (`X["Type"]`, `X["Encoded"]`, `X["DecodingServices"]`, …) so tsgo doesn't leak `unknown` into `Effect` channels (notably `R`). - - Sites: `client/clientFor.ts` (`RequestHandlerFor`, `FinalTypeOf`, `ExtractResponse`, `ExtractEResponse`), `client/makeClient.ts` (`InputFromPayload`, `OutputFromSuccess`, `InvalidationConfigForCommand`, `TaggedRequestWithMeta` overloads), `rpc/MiddlewareMaker.ts` (`Errors`), `rpc/RpcMiddleware.ts` (`Failure`, `FailureContext`), `Schema/ext.ts` (`ReadonlySetFromArray`, `ReadonlyMapFromArray`), `infra/routing.ts` (`GetSuccessShape`, handlers, route matcher), `vue/makeClient.ts` (`MutationExt.project`, `MutationWithExtensions`, `QueryProjection`), `vue/routeParams.ts` (`parseRouteParams*`). - -## 4.0.0-beta.247 - -### Patch Changes - -- 01bab22: Work around tsgo failing to reduce `S.Codec.DecodingServices` (`X extends Top ? X["DecodingServices"] : never`) in generic positions, which left `unknown` and polluted the `R` channel of client handlers and RPC middleware failure context. Since the schemas involved are already constrained to `S.Top`, read `["DecodingServices"]` directly in `RequestHandlerFor`, `TagClass.FailureContext`, and the Vue `MutationExt` / `QueryProjection` types. - -## 4.0.0-beta.246 - -## 4.0.0-beta.245 - -## 4.0.0-beta.244 - -### Patch Changes - -- b8b9080: update packages - -## 4.0.0-beta.243 - -## 4.0.0-beta.242 - -### Patch Changes - -- da83c1f: Align `InvalidationEntry` (vue) with `InvalidateQueryInstruction` (effect-app). - - `InvalidateQueryInstruction` is now parametrized over `Filters` / `Options` - (defaulting to `Record` so the core stays framework-agnostic). - `@effect-app/vue` exposes `InvalidationEntry` as a narrowed alias substituting - `@tanstack/vue-query`'s `InvalidateQueryFilters` and `InvalidateOptions`. Single - source of truth for the union shape across both packages. - -- 21017d5: `InvalidationSet.add` accepts arrays and is exposed as a static shortcut. - - - Single item: `yield* InvalidationSet.add(UserRsc.GetMe)` - - Batch: `yield* InvalidationSet.add([UserRsc.GetMe, ["custom", "key"]])` - - Skips the `.use(_ => _.add(...))` boilerplate. - - Existing `InvalidationSet.use(_ => _.add(...))` form still works; the service - identity is preserved via `Object.assign` so `Effect.provideService` and - `.use` / `.useSync` continue to operate on the same `Context.Reference`. - -- 2495ace: `InvalidationSet.add` accepts RPC handler shorthand. - - Server-side handlers may now pass an RPC handler object directly to - `InvalidationSet.add`; its query key is derived via `makeQueryKey`. Raw - `InvalidationKey` arrays continue to work. - - ```ts - // before - yield * - Invalidation.InvalidationSet.use((_) => _.add(makeQueryKey(UserRsc.GetMe))); - - // after - yield * Invalidation.InvalidationSet.use((_) => _.add(UserRsc.GetMe)); - ``` - -## 4.0.0-beta.241 - -### Patch Changes - -- b035b1c: Isolate per-call `Effect.provide(layers)` / `Stream.provide(layers)` in the - RPC api client factory via `{ local: true }`. - - `layers` here is built per RPC invocation from a caller-supplied - `requestLevelLayers` plus a fresh `RequestName` layer. Today both pieces are - stateless, so no observable bug — but `Effect.provide(layer)` without - `local: true` resolves its `MemoMap` from the ambient fiber context, and on a - long-lived runtime (browser app, server fiber) any stateful layer slipped into - `requestLevelLayers` by a caller would be memoized and shared across every - subsequent RPC call. `{ local: true }` builds the layer fresh per call and - skips the ambient MemoMap entirely. - - Also documents the rule in `AGENTS.md` ("Per-request `Effect.provide(layer)` - must isolate its MemoMap") so the pattern is enforceable in code review and - caught early. Companion to the `provideOnRequestScope` MemoMap fix in - `@effect-app/infra`. - -## 4.0.0-beta.240 - -## 4.0.0-beta.239 - -## 4.0.0-beta.238 - -## 4.0.0-beta.237 - -## 4.0.0-beta.236 - -## 4.0.0-beta.235 - -### Patch Changes - -- ba4bdc3: improve: support .ts extension -- 6cfd83d: update effect to latest beta - -## 4.0.0-beta.234 - -## 4.0.0-beta.233 - -### Patch Changes - -- 57a1862: Update to effect 4.0.0-beta.67. Switch deps from `pkg.pr.new` snapshot back to npm beta tag. - -## 4.0.0-beta.232 - -### Patch Changes - -- 0263827: Fix `Date` / `DateValid` default helpers to pipe from their own schema. - - `withConstructorDefault` and `withDecodingDefaultType` on `DateValid` previously piped from `DateFromString`, dropping the `isDateValid()` check (and, after the recent identifier split, the `DateValid` annotations) from the resulting defaulted schema. Both `Date` and `DateValid` now use `extendM` so the helpers attach to the underlying schema (`DateFromString` / `DateValidFromString`). - -- 0263827: Distinguish `Date` and `DateValid` in JSON Schema output. - - - `Date` now emits identifier `DateOrInvalid` with description noting the value may be invalid. - - `DateValid` now emits its own annotated string (identifier `Date`) with description stating a valid ISO 8601 date is required. - -- 0263827: Update to effect `pkg.pr.new` snapshot at `a42ef66` (4.0.0-beta.66). Remove `Yieldable` and `asEffect()` (service tags are now `Effect` directly). - -## 4.0.0-beta.231 - -### Patch Changes - -- 7ca66ce: Update to effect 4.0.0-beta.66. Remove `Yieldable` and `asEffect()` (service tags are now `Effect` directly). - -## 4.0.0-beta.230 - -## 4.0.0-beta.229 - -## 4.0.0-beta.228 - -## 4.0.0-beta.227 - -## 4.0.0-beta.226 - -## 4.0.0-beta.225 - -## 4.0.0-beta.224 - -## 4.0.0-beta.223 - -## 4.0.0-beta.222 - -## 4.0.0-beta.221 - -### Patch Changes - -- 439cbeb: Adopt module system from effect-smol: replace barrel imports with specific submodule imports (`import * as X from "effect-app/X"` / `import * as X from "effect/X"`). - -## 4.0.0-beta.220 - -## 4.0.0-beta.219 - -### Patch Changes - -- d738811: remove double prefix - -## 4.0.0-beta.218 - -## 4.0.0-beta.217 - -### Patch Changes - -- 0e824ef: add missing Brand - -## 4.0.0-beta.216 - -## 4.0.0-beta.215 - -## 4.0.0-beta.214 - -## 4.0.0-beta.213 - -## 4.0.0-beta.212 - -### Patch Changes - -- 8792221: Rename `withDefault` schema extensions to `withConstructorDefault` for clarity. - - Document that `.withConstructorDefault` is **construction-only** (applied during `.make(...)` when a field is omitted) and is **not** applied during decode — it cannot be used to JIT-migrate database fields. Per-property JSDoc on every `.withConstructorDefault` / `.withDecodingDefaultType` exposed by `Schema/ext.ts`, `Schema/numbers.ts`, `Schema/moreStrings.ts`, and `ids.ts` so the caveat is visible on hover. - - Re-export `withConstructorDefault`, `withDecodingDefault`, `withDecodingDefaultKey`, `withDecodingDefaultType`, and `withDecodingDefaultTypeKey` from `effect-app/Schema` with explicit JSDoc. `withDecodingDefault*` is discouraged for persisted data: a missing field may be data corruption rather than an old-shape document, and silently substituting a default hides the problem. Prefer an explicit, preferably versioned migration of database data over decode-time fallbacks. - -## 4.0.0-beta.211 - -## 4.0.0-beta.210 - -## 4.0.0-beta.209 - -### Patch Changes - -- c991be1: update packages -- 3bae238: Remove lodash type imports from utils.ts, replacing with native TypeScript equivalents. - -## 4.0.0-beta.208 - -## 4.0.0-beta.207 - -### Patch Changes - -- 8fffc3c: cleanup - -## 4.0.0-beta.206 - -### Patch Changes - -- 54bfc59: Require middleware to flow through `makeRpcClient` and the live layer through `makeRouter`. - - ### `makeRpcClient(middleware, generalErrors?)` - - Signature drops the `rcs` (request-context map wrapper) parameter. `rcs` was only load-bearing on the type side for `RequestConfig` inference; that information is now derived from `middleware.requestContextMap`. `middleware` is required — the previous "rcs + optional middleware" overload is gone. - - **Migration**: - - ```diff - -makeRpcClient(RequestContextMap, undefined, AppMiddleware) - +makeRpcClient(AppMiddleware) - ``` - - For tests/clients without a real middleware, build a minimal stub (`{ requestContextMap, requestContext }`) or pass any value satisfying `ClientMiddleware`. - - ### `makeRouter(middlewareLive)` - - `makeRouter()` no longer infers the live middleware layer from `meta.middleware.Default`. The Live layer is now passed explicitly to `makeRouter`, and the request classes only carry the middleware tag (schema-only). This decouples the router from any assumption that the middleware tag exposes a `Default` static. - - **Migration**: - - ```diff - -export const { Router, matchAll } = makeRouter() - +export const { Router, matchAll } = makeRouter(AppMiddleware.Default) - ``` - -## 4.0.0-beta.205 - -### Patch Changes - -- f313973: fix type make interface - -## 4.0.0-beta.204 - -### Patch Changes - -- 0a0030f: fix missing overloads - -## 4.0.0-beta.203 - -### Patch Changes - -- 992d9fa: fix more branded types - -## 4.0.0-beta.202 - -### Patch Changes - -- 1186b09: improve branded types, keep types through Rebuild - -## 4.0.0-beta.201 - -### Patch Changes - -- d67d17a: Source middleware errors exclusively from the rpc middleware tag, and move command/stream invalidation wrap/unwrap entirely into the routing layer (server) and `apiClientFactory` (client). `InvalidationMiddleware` and `InvalidationMiddlewareLive` are removed. - - ### Resource error schemas - - Three sites that used to fold `RequestContextMap[*].error` into a request's own error schema now stop doing so: - - - `makeRpcClient` / `makeRequestClass` — `failureSchema` is just `config.error` (still merged with the optional `generalErrors` parameter, which is the only remaining error mix on both type and runtime levels). - - `MiddlewareMaker.rpc()` — `error: options.error` only; the previous union with `rcm.config[*].error` is gone. - - Routing and `apiClientFactory.makeRpcGroupFromRequestsAndModuleName` — `Invalidation.makeCommandRpc` is called with `error: resource.error` (no widening with the composite middleware error union). - - Middleware errors reach the client through the rpc's `middlewares[*].error` failure-union channel of `Rpc.exitSchema`, exposed by attaching the middleware tag to the rpc on both sides: - - - **Server**: `makeRouter(middleware)` attaches the live composite tag (existing behavior). - - **Client**: new `middleware` option on `ClientForOptions` / `ApiClientFactory.makeFor(layer, { middleware })` attaches the same tag schema-only (no Live invoked). Threaded through `makeRpcGroupFromRequestsAndModuleName` to `RpcGroup.middleware(tag)`. Without it, stream rpcs (whose top-level `errorSchema` is forced to `Never` by effect-rpc) hit `SchemaError: Expected never | { _tag: "error", ... }` decoding middleware-thrown errors that bypass the in-stream `Stream.catch` wrap. - - **Migration**: handlers that yield errors previously sourced from rcm (e.g. `yield* new UnauthorizedError()`) now require those errors to be declared explicitly on the resource — `Req.Query()("...", fields, { success, error: UnauthorizedError })`. The handler error type no longer auto-includes the rcm union. - - ### Invalidation wrap/unwrap - - - `routing.ts` (server) provides a per-request `InvalidationSet` for commands, wraps the success value as `CommandResponseWithMetaData`, and converts handler-thrown failures into `CommandFailureWithMetaData` so accumulated invalidation keys reach the client on either path. Stream wrap (per-chunk envelope + final `done` chunk) was already in routing and is unchanged. - - `apiClientFactory.ts` (client) `unwrapCommand` strips both envelopes and forwards keys to `InvalidationKeysFromServer`. - - `InvalidationMiddleware` (the tag) and `InvalidationMiddlewareLive` (the layer) are **removed**. The middleware was the previous home of the wrap; with the wrap moved to routing/apiClientFactory, the middleware became a thin pass-through and is no longer needed. `DefaultGenericMiddlewares` and `DefaultGenericMiddlewaresLive` shrink accordingly — no migration needed for callers that used the defaults; callers that referenced `InvalidationMiddleware` / `InvalidationMiddlewareLive` directly should drop those imports. - - Middleware-thrown errors are never wrapped: by definition the handler never ran, so there is nothing to invalidate. They flow raw on the Cause and the client decodes them via the middleware-tag failure-union channel described above. - -## 4.0.0-beta.200 - -### Patch Changes - -- 8f1cf6a: able to configure schema concurrency -- 0cff7c1: workaround middleware error issue - -## 4.0.0-beta.199 - -## 4.0.0-beta.198 - -### Patch Changes - -- 32dbc54: fix stream type when no success specified - -## 4.0.0-beta.197 - -### Minor Changes - -- 3dc0d2a: Add streaming as a `stream: true` config option on `Query` / `Command` instead of a separate request type. - - `TaggedRequestFor` now exposes only `Query` and `Command` factories — the standalone `Stream` factory is removed. To produce a Stream of `success` values, pass `stream: true` in the request config. The request `type` field stays `"command" | "query"`; a new `stream: boolean` field carries the streaming flag (stripped from the stored handler config). - - ```ts - // Query that streams results - Req.Query()("Tag", {}, { stream: true, success: ... }) - - // Command that streams results - Req.Command()("Tag", {}, { stream: true, success: ... }) - ``` - - Vue client mapping (per-handler properties mirror the non-stream API — `.query`, `.fn`, `.mutate`): - - - `query` + `stream: true` → exposes `.query` (read-only streaming, tracked Vue Query). Helper map key: `${name}Query`. - - `command` + `stream: true` → exposes `.fn` and `.mutate` (mutating streaming). - - Plain `query` / `command` unchanged. - - Server routing dispatches via the new `stream` flag (`makeStreamRpc` for streaming commands/queries, `makeCommandRpc` / `Rpc.make` otherwise). - - Also lifts the `Struct` / `TaggedStruct` and `Opaque` definitions in `effect-app/Schema` to use `S.Bottom` / `S.Opaque` directly, exposing `fields`, `mapFields`, and a `MakeIn` that allows `void` when all fields are optional. `TaggedRequestFor` request classes now use `Opaque(TaggedStruct(...))` instead of `TaggedClass`, and decoding/encoding services are derived from `success` / `error` rather than stored on the request. - - **Migration**: replace `Req.Stream` with `Req.Query` or `Req.Command` and add `stream: true` to the config — `Query` for read-only streams, `Command` for mutating streams. - -## 4.0.0-beta.196 - -## 4.0.0-beta.195 - -### Patch Changes - -- 774a9b3: `MiddlewareMaker.makeMiddlewareBasic` now derives each middleware's effective error from both the static `error` field on the tag AND the `rcm` config entry referenced by `dynamic.key`, rather than relying on the static field alone. - - Middlewares declared with `dynamic: RequestContextMap.get("foo")` (instead of an explicit static `error: ...`) end up with `tag.error = Schema.Never` at runtime — `RpcMiddleware.Tag` defaults the static error to `Never` when not provided. The composite `MiddlewareMaker.Tag(...).middleware(...)` walked `make[*].error` to build its own error union, collapsing to `Union ≡ Never`. - - `Rpc.exitSchema` walks `rpc.middlewares[*].error` when building the wire-level failure union for every rpc kind. Empty-union meant middleware-thrown errors (`NotLoggedInError`, `UnauthorizedError`, etc.) never reached the wire schema. Query/command happened to work because their wire `errorSchema = resource.error` already covered the merge from `makeRpcClient`. Stream rpcs have `errorSchema` force-set to `Schema.Never` by effect-rpc, so the resource-level merge never reached the wire — middleware errors decoded as "Expected never, got X". - - Per middleware, the new logic pushes both the static `_.error` (if non-`Never`) and `rcm[_.dynamic.key].error` (if non-`Never`) into the composite's failure union. - -## 4.0.0-beta.194 - -## 4.0.0-beta.193 - -## 4.0.0-beta.192 - -## 4.0.0-beta.191 - -### Patch Changes - -- 50ce7e6: Replace typescript-eslint with oxlint-tsgolint for type-aware lint. Drop ESLint entirely from non-vue packages (cli, effect-app, infra) — they now use only `oxlint --type-aware`. Vue packages keep ESLint to run `@effect-app/no-await-effect` (no tsgolint equivalent) via `@typescript-eslint/parser` + `vue-eslint-parser`. - -## 4.0.0-beta.190 - -### Patch Changes - -- 985176b: Align request handler input typing with the request's `make` signature. Handlers are now classified as no-input only when the request schema declares no payload fields; any payload (even fully-optional) yields a function handler whose input matches `make`'s first parameter. Adds `HandlerInput` and threads it through `CommandFromRequest`. - -## 4.0.0-beta.189 - -### Patch Changes - -- ea32222: Update to effect 4.0.0-beta.60 and use native `Rpc.custom` constructors (`makeCommandRpc`, `makeStreamRpc`) for metadata-wrapped RPC schemas instead of manually wrapping/unwrapping schemas inline. - -## 4.0.0-beta.188 - -### Patch Changes - -- b2e438f: Remove Operations service and repo - -## 4.0.0-beta.187 - -### Patch Changes - -- 0d4e0b8: Fix `isGeneratorFunction` using `isObject` instead of `isFunction`: generator functions have `typeof === "function"`, not `"object"`, so the check always returned `false`. This caused `Command.streamFn` generator-form handlers to silently pass a raw `Generator` object rather than an `Effect`, meaning the mutation was never executed. - -## 4.0.0-beta.186 - -## 4.0.0-beta.185 - -## 4.0.0-beta.184 - -## 4.0.0-beta.183 - -## 4.0.0-beta.182 - -## 4.0.0-beta.181 - -### Patch Changes - -- 583393f: Default the stream `mutateStream` execute resolved value to the request's success type when no `final` schema is declared. - - Previously the type defaulted to `void`, but the runtime already resolves with the last emitted value. Types now match runtime behaviour: `execute` returns `Final` if a `final` schema is set, otherwise the success type. - -## 4.0.0-beta.180 - -### Minor Changes - -- 7fa3045: V1/V2/V3: stream and command requests carry invalidation metadata - - **V1** – stream final response includes metadata - - - `Invalidation.StreamResponseChunk` wraps each stream item as `{ _tag: "value", value }` and appends `{ _tag: "done", metadata }` at the end carrying all accumulated invalidation keys. - - **V2** – invalidation keys included in failures - - - `Invalidation.CommandFailureWithMetaData` and `Invalidation.StreamFailureChunk` carry keys accumulated up to the point of failure, so clients can invalidate queries even when a command or stream errors. - - `InvalidationMiddlewareLive` wraps command failures; `routing.ts` wraps stream failures. - - `apiClientFactory.ts` unwraps both on the client side, forwarding keys before re-failing with the original error. - - **V3** – mid-stream metadata chunks - - - `Invalidation.StreamResponseChunk` now also includes `{ _tag: "metadata", metadata }` for mid-stream invalidation. - - After each emitted value, the server drains accumulated keys and emits a "metadata" chunk if any keys were collected since the last drain (bucket reset via `InvalidationSet.drain`). - - `apiClientFactory.ts` processes "metadata" chunks the same as "done" chunks, forwarding keys to `InvalidationKeysFromServer` immediately. - - `makeInvalidationKeysService` accepts an optional `onAdded` callback that fires after each key addition, enabling `mutate.ts` to trigger query invalidation mid-stream without waiting for the stream to complete. - -## 4.0.0-beta.179 - -### Minor Changes - -- 828d264: Stream requests now support an optional `final` schema that models the final success type of the stream. When declared, `mutateStream`'s execute effect resolves with the last emitted value typed as `Final` instead of `void`. - - ```ts - class MyStream extends SomethingStream()( - "MyStream", - { id: S.String }, - { - success: S.Union([OperationProgress, ExportComplete]), - final: ExportComplete, // execute now resolves with ExportComplete - } - ) {} - ``` - -## 4.0.0-beta.178 - -## 4.0.0-beta.177 - -### Minor Changes - -- 89d8b3a: Add Effect RPC `Stream` support to the wrapper. - - - New `Stream` request constructor on `TaggedRequestFor` parallel to `Query`/`Command`. Emits resources with `type: "stream"`. - - Server router (`@effect-app/infra` `routing.ts`) accepts stream resources whose handlers return a `Stream.Stream` (or a function from input to one). Forwards `stream: true` to `Rpc.make` so `RpcSchema.Stream` wrapping is applied. Streams bypass `applyRequestTypeInterruptibility` and the `Effect.withSpan` wrapping (the RPC server adds its own span). - - Client (`apiClientFactory.ts`) detects stream resources, forwards `stream: true` when constructing `RpcGroup`, and exposes the per-request `handler` as a `Stream.Stream` (via `Stream.unwrap` over the `ManagedRuntime` context) instead of an `Effect`. `Invalidation.CommandResponseWithMetaData` continues to apply only to commands. - - New `RequestStreamHandler` / `RequestStreamHandlerWithInput` shapes in `clientFor.ts`; `RequestHandlers` dispatches on `type: "stream"`. - -## 4.0.0-beta.176 - -### Patch Changes - -- pass options - -## 4.0.0-beta.175 - -## 4.0.0-beta.174 - -### Minor Changes - -- 821468d: Add server-driven cache invalidation via RPC response headers. - - - `effect-app/rpc`: new `Invalidation` module with `InvalidationKey` / `InvalidationKeys` schemas, `Invalidates` annotation (for declaring static invalidation on Rpc definitions), `InvalidationSet` reference (request-scoped accumulator), and `makeInvalidationSet` helper. - - `effect-app/middleware`: new `InvalidationMiddleware` RPC middleware tag; included in `DefaultGenericMiddlewares`. - - `effect-app/client`: new `InvalidationKeys` module with `InvalidationKeysFromServer` reference and `makeInvalidationKeysService` helper; `apiClientFactory` now taps HTTP responses to read the `x-invalidate` header and forward keys to `InvalidationKeysFromServer`. - - `@effect-app/infra`: new `InvalidationMiddlewareLive` RPC middleware implementation that owns the full lifecycle — creates a request-scoped `InvalidationSet` (backed by a `Ref`), pre-populates it from the `Invalidates` annotation, provides it to the handler, and after the handler completes registers an HTTP pre-response handler (via `appendPreResponseHandlerUnsafe`) to write the accumulated keys as an `x-invalidate` response header. No separate HTTP middleware is needed. - - `@effect-app/vue`: `invalidateQueries` / `useMutation` now reads server-provided invalidation keys from `InvalidationKeysFromServer` after each mutation and applies them alongside the client-side invalidation. - -## 4.0.0-beta.173 - -## 4.0.0-beta.172 - -## 4.0.0-beta.171 - -### Patch Changes - -- d71d976: fix - -## 4.0.0-beta.170 - -### Patch Changes - -- 8f09f77: fix - -## 4.0.0-beta.169 - -### Patch Changes - -- 8ae8b53: input mess - -## 4.0.0-beta.168 - -### Patch Changes - -- 178480a: Fix request handler input classification to use request schema fields instead of `make` parameters, preventing defaulted/nullable input fields from being treated as no-input handlers. - -## 4.0.0-beta.167 - -### Patch Changes - -- 140e192: Relax invalidation resource value constraints to allow arbitrary values while preserving query-only filtering in invalidation handling. - -## 4.0.0-beta.166 - -### Patch Changes - -- dbcc53b: Refactor command invalidation typing: declare resources via `Command()`, pass `invalidatesQueries` as the optional 4th argument, and enforce exact `clientFor` invalidation resources when required. - -## 4.0.0-beta.165 - -### Minor Changes - -- f88ea34: Move `makeQueryKey` into `effect-app/client` and update Vue source and tests to import it from the shared client module. Vue still re-exports `makeQueryKey` from `src/lib` for compatibility. - -## 4.0.0-beta.164 - -### Minor Changes - -- 8cb3de4: Add command invalidation helpers that preserve query-only resource types and pass mutation input and `Exit` results into invalidation callbacks. Update Vue `clientFor` to merge request-level invalidation config with call-site invalidation and require matching invalidation resources. - -## 4.0.0-beta.163 - -### Patch Changes - -- b952f19: bye cruft - -## 4.0.0-beta.162 - -### Patch Changes - -- b52b424: restore annotations for now - -## 4.0.0-beta.161 - -## 4.0.0-beta.160 - -### Patch Changes - -- 505bfa9: Add concurrent decode helper APIs and migrate decode callsites to use them. - - - Add `withDefaultParseOptions` and keep `DefaultParseOptions` centralized. - - Export `decodeEffectConcurrently` and `decodeUnknownEffectConcurrently` from Schema and SchemaParser modules. - - Update repository, queue, client, form, and CLI decode paths to use concurrent decode helpers. - - Keep schema constructors free of hardcoded parse concurrency overrides. - -## 4.0.0-beta.159 - -### Patch Changes - -- c1e73de: - -## 4.0.0-beta.158 - -### Patch Changes - -- 3c1f52d: improve: class strictness enabled by default again, allow `strict: false` as opt out for now. -- 6ae3050: Preserve class annotation parseOptions in relaxed declaration struct decoding so custom parse options (including concurrency defaults) are applied consistently. - -## 4.0.0-beta.157 - -### Patch Changes - -- 6fff09c: unify encoded function for when you use encodedKeys - -## 4.0.0-beta.156 - -## 4.0.0-beta.155 - -### Patch Changes - -- c215db8: align TaggedUnion with array arg - -## 4.0.0-beta.154 - -## 4.0.0-beta.153 - -## 4.0.0-beta.152 - -## 4.0.0-beta.151 - -## 4.0.0-beta.150 - -### Patch Changes - -- 85a8275: Expose `make`, `makeOption`, and `makeEffect` static helpers on request classes created via `Query`/`Command`. - -## 4.0.0-beta.149 - -### Patch Changes - -- f317c5e: Preserve omitted-input make helpers through Schema.Opaque when the wrapped schema allows optional or default-only constructor input. - -## 4.0.0-beta.148 - -### Patch Changes - -- 199e9a5: Allow Struct and TaggedStruct make helpers to omit input when every constructor field is optional or defaulted, and preserve widening copy typings through a lighter named public type to improve TypeScript editor responsiveness. - -## 4.0.0-beta.147 - -### Patch Changes - -- 47e3742: Preserve Struct.copy through `annotate`, `annotateKey`, and `mapFields` chains, and add tests covering chained copy behavior on Struct schemas. - -## 4.0.0-beta.146 - -### Patch Changes - -- a4dff57: Adjust Struct and TaggedStruct copy typing to follow class-style widening constraints while keeping structural copy runtime behavior. - -## 4.0.0-beta.145 - -### Patch Changes - -- 12abb55: Refine schema copy behavior by keeping class copy constructor-based while using structural copy for Struct and TaggedStruct helpers. - -## 4.0.0-beta.144 - -### Patch Changes - -- 11422f8: Update request helper typing and runtime invocation to rely on schema `.make` instead of class constructors, avoiding `new`-based assumptions for request schemas. -- d31253f: Refactor eligible schema classes and tagged classes to Opaque schemas, and migrate constructor call sites to use `.make` for those models. - -## 4.0.0-beta.143 - -### Patch Changes - -- 79eb019: Remove redundant schema `title` annotations when they duplicate the schema `identifier`. - -## 4.0.0-beta.142 - -### Minor Changes - -- 025de47: Fold the encoded-override support from `ExtendedClass` and `ExtendedTaggedClass` into `Class`, `TaggedClass`, `ErrorClass`, and `TaggedErrorClass`, and update model codegen to detect the new second-generic form. - -### Patch Changes - -- 3436d44: Extend `Schema.Opaque` in `effect-app/Schema` to support an optional encoded-type generic while preserving the original single-generic behavior. - -## 4.0.0-beta.141 - -### Patch Changes - -- 7c25dbb: Add relaxed wrapper support for `ErrorClass` and `TaggedErrorClass` in `effect-app/Schema`, matching the existing class wrapper behavior (`copy`, cached `ast`, and unbounded parse concurrency). - -## 4.0.0-beta.140 - -## 4.0.0-beta.139 - -## 4.0.0-beta.138 - -## 4.0.0-beta.137 - -## 4.0.0-beta.136 - -## 4.0.0-beta.135 - -### Patch Changes - -- c7bbc41: fix - class should check type side - -## 4.0.0-beta.134 - -### Patch Changes - -- 9d3495e: Preserve field-level schema decode errors for relaxed Class and TaggedClass declarations so decode failures report nested constraints (for example min-length violations) instead of only a generic class-type mismatch. -- f353d48: Rename the Class/TaggedClass relaxed declaration option to `strict` (default `false`) and apply it to `Class`, `TaggedClass`, `ExtendedClass`, and `ExtendedTaggedClass`. - - When `strict: true`, class decoding keeps strict class-level declaration behavior; by default, decoding remains relaxed and preserves field-level schema errors. - -## 4.0.0-beta.133 - -### Patch Changes - -- c3299f7: update packages - -## 4.0.0-beta.132 - -## 4.0.0-beta.131 - -## 4.0.0-beta.130 - -### Patch Changes - -- ea1bd46: fix: prevent expansion of nominal brands - -## 4.0.0-beta.129 - -## 4.0.0-beta.128 - -### Patch Changes - -- 57db551: Split `TaggedRequestFor` into `Query` and `Command` factories, and mark generated request classes with `type: "query" | "command"`. - - Vue client helpers now expose query-only helpers (`query`, `suspense`, `fetch`) for query requests and mutation-only helpers (`mutate`, `fetch`) for command requests. - -## 4.0.0-beta.127 - -## 4.0.0-beta.126 - -### Patch Changes - -- 458bb1b: fix type - -## 4.0.0-beta.125 - -## 4.0.0-beta.124 - -### Patch Changes - -- 256ae85: cleanup - -## 4.0.0-beta.123 - -### Patch Changes - -- 14aba14: fix: clientFor - -## 4.0.0-beta.122 - -### Patch Changes - -- f052d38: Replace `(...) => Effect.gen` with `Effect.fnUntraced` in `apiClientFactory`. - -## 4.0.0-beta.121 - -### Patch Changes - -- 5ac46cb: Make `Schema.provide` dual (pipeable) - -## 4.0.0-beta.120 - -### Minor Changes - -- f21190c: Add `copy` property to `Class` and `TaggedClass` for creating modified instances with updated fields. The `copy` method is cached per-class and supports both object and function-based updates, with pipeline support. - -## 4.0.0-beta.119 - -## 4.0.0-beta.118 - -### Minor Changes - -- 08d2e70: Add `concurrency: "unbounded"` parseOptions annotation to Schema constructors (Struct, Array, NonEmptyArray, Record, TaggedStruct, ReadonlySet, ReadonlyMap, Class, TaggedClass) so all encode/decode operations automatically run with unbounded concurrency. Also override `mapFields` on Struct and Class/TaggedClass to preserve the annotation. - -### Patch Changes - -- bd26832: Add nested anyOf flattening to JSON Schema post-processing -- Update effect packages to 4.0.0-beta.52 - -## 4.0.0-beta.117 - -## 4.0.0-beta.116 - -## 4.0.0-beta.115 - -## 4.0.0-beta.114 - -## 4.0.0-beta.113 - -## 4.0.0-beta.112 - -## 4.0.0-beta.111 - -### Patch Changes - -- ca94edf: fix typo - -## 4.0.0-beta.110 - -## 4.0.0-beta.109 - -## 4.0.0-beta.108 - -### Patch Changes - -- 3e46e7b: allow excess props in openapi schemas - -## 4.0.0-beta.107 - -## 4.0.0-beta.106 - -## 4.0.0-beta.105 - -## 4.0.0-beta.104 - -### Minor Changes - -- e944bca: Add `generateGuards` and `generateGuardsFor` to `TaggedUnion` / `ExtendTaggedUnion` for property-scoped type guards. - - - `generateGuards("key")` — generic per-guard, no need to specify the container type - - `generateGuardsFor()("key")` — curried, fixes `A` for concrete guard signatures - - Both return `{ is{Tag}, isAnyOf }` guards that narrow the container type by its tagged union property. - -## 4.0.0-beta.103 - -### Minor Changes - -- 7119320: Add `generateGuards` to `TaggedUnion` / `ExtendTaggedUnion` for generating property-scoped type guards. - - Usage: `MyUnion.generateGuards()("propertyKey")` returns `{ is{Tag}, isAnyOf }` guards that narrow the container type by its tagged union property. - -## 4.0.0-beta.102 - -## 4.0.0-beta.101 - -## 4.0.0-beta.100 - -## 4.0.0-beta.99 - -## 4.0.0-beta.98 - -## 4.0.0-beta.97 - -## 4.0.0-beta.96 - -### Patch Changes - -- 5615e47: bs - -## 4.0.0-beta.95 - -### Patch Changes - -- 88838fb: Remove pick/omit customizations from Class/TaggedClass/Struct/TaggedStruct. Use `Struct.pick(X.fields, [...])` from `effect-app` instead. - -## 4.0.0-beta.94 - -## 4.0.0-beta.93 - -## 4.0.0-beta.92 - -## 4.0.0-beta.91 - -### Minor Changes - -- 738b482: Add pick/omit to Struct and TaggedStruct - -## 4.0.0-beta.90 - -## 4.0.0-beta.89 - -## 4.0.0-beta.88 - -## 4.0.0-beta.87 - -## 4.0.0-beta.86 - -## 4.0.0-beta.85 - -## 4.0.0-beta.84 - -## 4.0.0-beta.83 - -## 4.0.0-beta.82 - -## 4.0.0-beta.81 - -## 4.0.0-beta.80 - -## 4.0.0-beta.79 - -### Minor Changes - -- d16845e: Remove `TaggedRequest` from `makeRpcClient`, now only `TaggedRequestFor` is returned. Remove all legacy `meta.moduleName` support — `id` and `moduleName` are now required on `Req` type. Remove `makeRpcGroup` (use `makeRpcGroupFromRequestsAndModuleName` instead). - -## 4.0.0-beta.78 - -## 4.0.0-beta.77 - -### Minor Changes - -- 3613e87: Add `TaggedRequestFor` helper to `makeRpcClient` that curries a `moduleName`, producing request classes with static `id` and `moduleName` properties. This enables passing request classes directly to `makeQueryKey` without going through `clientFor` first. The `clientFor` function no longer requires a `meta` property on the module when requests carry `moduleName`. The meta codegen preset now generates `Req = TaggedRequestFor(moduleName)`. Original `TaggedRequest` remains for backwards compatibility. - -## 4.0.0-beta.76 - -### Patch Changes - -- a5248a9: use new schema features -- fac725d: update effect to latest beta - -## 4.0.0-beta.75 - -### Patch Changes - -- 24f0a5a: rename Literal to Literals, no longer hiding Schema's singular Literal. - -## 4.0.0-beta.74 - -### Patch Changes - -- 54ec1ef: fix bs - -## 4.0.0-beta.73 - -## 4.0.0-beta.72 - -### Patch Changes - -- 0541f0d: fix withDefault types - -## 4.0.0-beta.71 - -### Minor Changes - -- beae3a0: Remove `withDefaultConstructor` wrapper, use `S.withConstructorDefault` directly with `Effect.succeed`/`Effect.sync`. - -## 4.0.0-beta.70 - -## 4.0.0-beta.69 - -### Patch Changes - -- dc465e3: update to latest effect beta - -## 4.0.0-beta.68 - -### Patch Changes - -- e6f2341: Add SecretURL tests and switch secretURL config to use Config.nonEmptyString - -## 4.0.0-beta.67 - -## 4.0.0-beta.66 - -### Patch Changes - -- edc52e4: update - -## 4.0.0-beta.65 - -### Minor Changes - -- 1f103b2: Replace `proxify` with explicit service accessor helpers: `accessFn`, `accessEffectFn`, `accessCn`, `accessEffectCn`. - -### Patch Changes - -- 31739d7: Fix JSON schema output for Email, Date, PhoneNumber, and Url schemas. The `jsonSchema` annotation key is not recognized by Effect v4's JSON schema generator — use proper v4 annotations (`format`, `description`) and built-in checks (`isMinLength`, `isMaxLength`) instead. - -## 4.0.0-beta.64 - -### Patch Changes - -- c1a6fdc: fix proxify - -## 4.0.0-beta.63 - -## 4.0.0-beta.62 - -### Patch Changes - -- 0b21a02: fix: flatten simple allOf constraints in JSON Schema output for better codegen compatibility - -## 4.0.0-beta.61 - -## 4.0.0-beta.60 - -## 4.0.0-beta.59 - -### Patch Changes - -- cec026d: update packages - -## 4.0.0-beta.58 - -## 4.0.0-beta.57 - -## 4.0.0-beta.56 - -## 4.0.0-beta.55 - -## 4.0.0-beta.54 - -### Patch Changes - -- d867272: the return of `Context` - -## 4.0.0-beta.53 - -### Patch Changes - -- ee9694e: fix type issue? - -## 4.0.0-beta.52 - -### Minor Changes - -- 6252808: Add `ReadonlySetFromArray` and `ReadonlyMapFromArray` schema transformations that decode from annotated arrays to Set/Map. Update `ReadonlySet` and `ReadonlyMap` to use these internally. - -## 4.0.0-beta.51 - -## 4.0.0-beta.50 - -## 4.0.0-beta.49 - -### Patch Changes - -- e585c9c: fix config nested - -## 4.0.0-beta.48 - -### Patch Changes - -- 0c88f78: Remove `TaggedError` compatibility re-export, use `TaggedErrorClass` directly - -## 4.0.0-beta.47 - -### Patch Changes - -- 3365758: workaround effect error messages - -## 4.0.0-beta.46 - -## 4.0.0-beta.45 - -### Patch Changes - -- 10b55ff: update packages - -## 4.0.0-beta.44 - -### Patch Changes - -- a37aa38: Update to effect beta 43 - -## 4.0.0-beta.43 - -## 4.0.0-beta.42 - -## 4.0.0-beta.41 - -## 4.0.0-beta.40 - -## 4.0.0-beta.39 - -### Patch Changes - -- 10e90d5: fix: TaggedRequest should be an Opaque Schema Class - -## 4.0.0-beta.38 - -### Patch Changes - -- 0b3e00e: fix bug - -## 4.0.0-beta.37 - -### Patch Changes - -- 947fe20: fix input date - -## 4.0.0-beta.36 - -## 4.0.0-beta.35 - -## 4.0.0-beta.34 - -### Patch Changes - -- 8c645d5: update to latest effect - -## 4.0.0-beta.33 - -### Patch Changes - -- 4b95009: use Finite instead of Number - -## 4.0.0-beta.32 - -### Patch Changes - -- 01d862a: again - -## 4.0.0-beta.31 - -### Patch Changes - -- 6b6d601: fix ForceVOid - -## 4.0.0-beta.30 - -### Patch Changes - -- 5ec1f45: fix ForceVoid for real - -## 4.0.0-beta.29 - -### Patch Changes - -- a899d46: Replace `ForceVoid` in `makeClient` with a typed codec transformation instead of an unsafe cast. - -## 4.0.0-beta.28 - -### Patch Changes - -- 0099208: Refactor `Pure` helpers to use typed `useSync` calls instead of unsafe casts. - -## 4.0.0-beta.27 - -### Patch Changes - -- 601a1ff: update effect to 4.0.0-beta.37 and drop the Schema Class disableValidation workaround now that the patched effect schema covers it - -## 4.0.0-beta.26 - -### Patch Changes - -- 4da28e2: Change `Schema.Class` and `Schema.TaggedClass` wrappers to default constructor options to `{ disableValidation: true }`. - - This avoids strict class identifier validation by default when constructing wrapper classes (for example passing a compatible view class), while keeping existing behavior when explicit options are provided. - -## 4.0.0-beta.25 - -### Patch Changes - -- 6282d4b: Update `Effect.allLower` to call `svc.asEffect()` when available, ensuring service entries are normalized before `Effect.all` evaluation. - -## 4.0.0-beta.24 - -### Patch Changes - -- 32f71bf: Fix `Schema.TaggedUnion(...).tags` extraction for class-based members (for example `TaggedClass`) by using a local AST sentinel walker instead of relying on internal effect APIs. - - Add tests covering: - - - `TaggedUnion` with `encodeKeys`-wrapped members - - `TaggedUnion` with `TaggedClass` members - -## 4.0.0-beta.23 - -### Patch Changes - -- ea08a79: remove `fields` constraint from TaggedUnion/tags helpers, extract `_tag` via AST walk instead - -## 4.0.0-beta.22 - -## 4.0.0-beta.21 - -### Patch Changes - -- 0d349e7: Align effect-app tagged union helpers with Effect v4 by delegating to the native tagged union utilities, exposing v4-style `cases` and `guards`, and preserving the local `tags` helper. -- 75c72ee: update effect to 4.0.0-beta.36, adapt to Option revert from A | undefined - -## 4.0.0-beta.20 - -### Patch Changes - -- aa921db: Fix `TaggedRequest` no-config error type inference so requests without a third argument infer the same default error schema as requests with explicit success config. - -## 4.0.0-beta.19 - -### Patch Changes - -- ffc10a4: Migrate `Schema/ext` context helpers to Effect v4 by implementing `provide` and `contextFromServices` with proper service subtraction across decoding and encoding requirements. - -## 4.0.0-beta.18 - -### Patch Changes - -- e9dbbdd: Tighten schema helper generics in `Schema/ext.ts` so default-related helpers accept properly named schema type parameters instead of bare `S.Top` inputs. - -## 4.0.0-beta.17 - -### Patch Changes - -- c8a6a3f: fix TaggedUnion - -## 4.0.0-beta.16 - -### Patch Changes - -- 56db2c4: fix: isA - -## 4.0.0-beta.15 - -### Patch Changes - -- 20186b6: Migrate schema arbitrary annotations to Effect v4 `toArbitrary` and cover `StringId` generation with a fast-check test. - -## 4.0.0-beta.14 - -## 4.0.0-beta.13 - -### Patch Changes - -- 16de88c: remove hopefully obsolete Struct overrides -- 1c3c541: Update Effect dependencies to 4.0.0-beta.31 across workspace packages. -- de2359d: Update Effect dependencies to 4.0.0-beta.28 across workspace packages. -- 0d72f34: Update Effect dependencies to 4.0.0-beta.29 across workspace packages. - -## 4.0.0-beta.12 - -### Patch Changes - -- 622d54a: Configure Changesets fixed versioning for public packages. - -## 4.0.0-beta.10 - -### Patch Changes - -- 01c70d0: update all teh tings - -## 4.0.0-beta.9 - -### Patch Changes - -- 5727372: switch to NdJson - -## 4.0.0-beta.8 - -### Patch Changes - -- 1f336bc: fix RequestName - -## 4.0.0-beta.7 - -### Patch Changes - -- 62b4989: fix request attr - -## 4.0.0-beta.6 - -### Patch Changes - -- df75041: fix Req - -## 4.0.0-beta.5 - -### Patch Changes - -- 016c5a3: adapt isObject change - -## 4.0.0-beta.4 - -### Patch Changes - -- 88b90c3: fix withDefault - -## 4.0.0-beta.3 - -### Patch Changes - -- 3a7abae: fix bs - -## 4.0.0-beta.2 - -### Major Changes - -- 3887256: Fix Schema->Codec - -## 4.0.0-beta.1 - -### Patch Changes - -- 64786af: Beta25 - -## 4.0.0-beta.0 - -### Major Changes - -- 880df28: Effect v4 beta - -## 3.16.0 - -### Minor Changes - -- 1a102e2: update packages - -## 3.15.1 - -### Patch Changes - -- 049b54a: improve: add NEA support to Array.groupByT. - -## 3.15.0 - -### Minor Changes - -- ecbd35f: drop default proxy - -## 3.14.2 - -### Patch Changes - -- faf7e9d: add utils, fix bugs - -## 3.14.1 - -### Patch Changes - -- c5a2bd5: add dropUndefinedRecursive - -## 3.14.0 - -### Minor Changes - -- 9316017: feat: add `dropUndefinedT2` - -### Patch Changes - -- 358782e: update packages - -## 3.13.0 - -### Minor Changes - -- 07f1de5: update packages and tsconfigs - -## 3.12.0 - -### Minor Changes - -- 23ba90f: improve: compare deep structurally - -## 3.11.0 - -### Minor Changes - -- 9697a93: feat: Map withDefault - -## 3.10.2 - -### Patch Changes - -- fdf1781: enhance runtimeFiberAsPromise with abort. - -## 3.10.1 - -### Patch Changes - -- fe49192: improve literal default, overridable - -## 3.10.0 - -### Minor Changes - -- 51ded8d: add "withDefault" to Schema.Literal. - -## 3.9.0 - -### Minor Changes - -- b8f5e30: update packages - -## 3.8.1 - -### Patch Changes - -- 493fbb8: fix tagged class pick/omit omitting \_tag. - -## 3.8.1-next.0 - -### Patch Changes - -- fix tagged class pick/omit omitting \_tag. - -## 3.8.0 - -### Minor Changes - -- 3cb4c4a: simplify requests/client, drop raw for now, fix missing schema context in request handlers - -## 3.7.1 - -### Patch Changes - -- 4da7f79: cleanup helpers namespace - -## 3.7.0 - -### Minor Changes - -- 6d8c512: update packages - -## 3.6.1 - -### Patch Changes - -- d38a50a: align S.Class to original, adding missing make. - -## 3.6.0 - -### Minor Changes - -- 3d49d81: update packages - -## 3.5.0 - -### Minor Changes - -- 68e91c9: rename `name` to `id` - -## 3.4.0 - -### Minor Changes - -- 923f0d8: improve defaultFailureMessageHandler - -## 3.3.7 - -### Patch Changes - -- 4000c95: track const action name - -## 3.3.6 - -### Patch Changes - -- 8096576: new eslint shared config package - -## 3.3.5 - -### Patch Changes - -- 47e83ca: update cli - -## 3.3.4 - -### Patch Changes - -- f9aee77: some fixes - -## 3.3.3 - -### Patch Changes - -- f6a3328: export base eslint files so that we import them in projects - -## 3.3.2 - -### Patch Changes - -- f5da9d0: update packages - -## 3.3.1 - -### Patch Changes - -- d670d5a: runtimefiber errors should be wrapped - -## 3.3.0 - -### Minor Changes - -- d50dfe6: add support for RuntimeFiber, fix should return the Promise for eventOnSubmit. - -## 3.2.4 - -### Patch Changes - -- 8cb893c: dprint rules the world - -## 3.2.3 - -### Patch Changes - -- f101ef0: upgrade ea - -## 3.2.2 - -### Patch Changes - -- 33f100d: finalize new cli - -## 3.2.1 - -### Patch Changes - -- c12a49b: effect app cli to effect cli - -## 3.2.0 - -### Minor Changes - -- bd12e89: Disable Schema batching by default, not to run into unexpected endless waiting for resolving to finish. - -## 3.1.0 - -### Minor Changes - -- 1e4de39: remove namespace/type merging in Prelude. - -### Patch Changes - -- 41352d8: refactor: remove internal usage of namespace/type collission. - -## 3.0.10 - -### Patch Changes - -- ce91007: fix non inverted rpc context map use - -## 3.0.9 - -### Patch Changes - -- ea6380f: cleanup RpcContextMap - -## 3.0.8 - -### Patch Changes - -- 3543bfb: cleanup dev-mode - -## 3.0.7 - -### Patch Changes - -- d42afe6: cleanup - -## 3.0.6 - -### Patch Changes - -- 868eab6: cleanup - -## 3.0.5 - -### Patch Changes - -- 182ca78: improve: MiddlewareMaker is a Tag with Self so we can extend it from class. - -## 3.0.4 - -### Patch Changes - -- bc68d9f: feat: introduce Layer.make as alternative to Context.ProvideService - -## 3.0.3 - -### Patch Changes - -- c31423a: cleanup rpc - -## 3.0.2 - -### Patch Changes - -- 88bb989: align dynamic/global middleware with V4 - -## 3.0.1 - -### Patch Changes - -- 74c28d1: move common rpc out of infra - -## 3.0.0 - -### Major Changes - -- 3e4c45d: feat: Add new @effect/rpc Dynamic and Global Middleware, based on effect-app Router - -## 2.56.0 - -### Minor Changes - -- db2b575: switch to new HttpLayerRouter - -## 2.55.0 - -### Minor Changes - -- 37eb29a: improve ServiceBus api - -## 2.54.3 - -### Patch Changes - -- dd30471: improve - -## 2.54.2 - -### Patch Changes - -- 79ffd88: updates - -## 2.54.1 - -### Patch Changes - -- 9f90266: fix prefix namespace - -## 2.54.0 - -### Minor Changes - -- 0f2b2cf: update packages - -## 2.53.1 - -### Patch Changes - -- fa2c8d0: fix prefix - -## 2.53.0 - -### Minor Changes - -- 5c51597: refactor Middleware to run as RPC middleware - -## 2.52.1 - -### Patch Changes - -- 244f84d: various improvements, documentation - -## 2.52.0 - -### Minor Changes - -- 99cf15f: unify middleware api - -## 2.51.0 - -### Minor Changes - -- 54dd59a: cleanup NES - -## 2.50.2 - -### Patch Changes - -- 0d33085: fix dts freak - -## 2.50.1 - -### Patch Changes - -- build fix - -## 2.50.0 - -### Minor Changes - -- 0a82e7f: update packages - -## 2.49.0 - -### Minor Changes - -- a36ceed: feat: composable dynamic middleware - -## 2.48.0 - -### Minor Changes - -- 8bb246b: cleanup and document router/middleware - -## 2.47.2 - -### Patch Changes - -- fup - -## 2.47.1 - -### Patch Changes - -- fup - -## 2.47.0 - -### Minor Changes - -- d29cb1b: refactor: drop optional identifier for Schema classes. - -## 2.46.1 - -### Patch Changes - -- b095bb5: tscp - -## 2.46.0 - -### Minor Changes - -- ba4ad65: update packages; drop ServiceStrict in favour of language-service - -## 2.45.0 - -### Minor Changes - -- 2c217ce: update packages - -## 2.44.0 - -### Minor Changes - -- 55da9e7: improve: Router supports contextProvider through middleware, replacing flawed requestLayers - -## 2.43.3 - -### Patch Changes - -- b771967: update packages - -## 2.43.2 - -### Patch Changes - -- aec6960: fix complex ast tagged union - -## 2.43.1 - -### Patch Changes - -- a2e1e1e: improve jsonschema of Email - -## 2.43.0 - -### Minor Changes - -- eac9c67: update packages - -## 2.42.1 - -### Patch Changes - -- 49d93a5: update packages - -## 2.42.0 - -### Minor Changes - -- ba96171: improve Cosmos errors - -## 2.41.0 - -### Minor Changes - -- b1d4af4: housekeeping: update packages - -## 2.40.1 - -### Patch Changes - -- 0230385: improve: rpc spans - - fewer duplicate spans - - improve name - -## 2.40.0 - -### Minor Changes - -- 8cb33d5: update packages - -## 2.39.1 - -### Patch Changes - -- d3b05cb: update packages -- 4627c7b: improve: optimise getFirstById. -- 1e1f32b: housekeeping: update packages - -## 2.39.0 - -### Minor Changes - -- bdc9d5c: feat: add support for skippable namespaces in query keys - -## 2.38.5 - -### Patch Changes - -- 8b7fdbe: update packages - -## 2.38.4 - -### Patch Changes - -- revert copyOrigin changes - -## 2.38.3 - -### Patch Changes - -- update packages - -## 2.38.2 - -### Patch Changes - -- c0ed28a: preserve readonlyness in the returned type for copyOrigin -- 0fcab38: update packages - -## 2.38.1 - -### Patch Changes - -- cd15a8e: some fixes for copyOrigin - -## 2.38.0 - -### Minor Changes - -- 81479bf: add specialized copy function wrt a constructor - -## 2.37.1 - -### Patch Changes - -- 7bedb13: updates - -## 2.37.0 - -### Minor Changes - -- 14ea804: housekeeping: update packages - -## 2.36.1 - -### Patch Changes - -- 4c8dfb4: update packages - -## 2.36.0 - -### Minor Changes - -- e42d5fb: update to effect 3.14 with native rpc2 - -## 2.35.0 - -### Minor Changes - -- f38e31f: make LogLevels readonly - -### Patch Changes - -- f30b57e: fix faker and rebuild dts - -## 2.34.0 - -### Minor Changes - -- 64d3a54: refactor: replace custom FiberRefs with Context.Reference in preparation of Effect v4 - -### Patch Changes - -- 771ac45: update packages - -## 2.33.1 - -### Patch Changes - -- 36996ed: housekeeping: update packages - -## 2.33.0 - -### Minor Changes - -- 1e88a86: improve error handled marking - -## 2.32.3 - -### Patch Changes - -- 63e966a: update packages - -## 2.32.2 - -### Patch Changes - -- 96158a1: remove manual error headers/body gathering. - -## 2.32.1 - -### Patch Changes - -- a42d5a6: mark also number for brand - -## 2.32.0 - -### Minor Changes - -- bca03c7: feat: enhance Types module - -## 2.31.1 - -### Patch Changes - -- 24417c7: lol - -## 2.31.0 - -### Minor Changes - -- 9ebae95: improve: rpc error reporting - -## 2.30.0 - -### Minor Changes - -- 10a8adf: improve: lock rpc responses to 418/422/200, report otherwise as response error! helping with bad proxies in between. -- 8be2984: housekeeping: update packages - -## 2.29.0 - -### Minor Changes - -- 7a4d393: report errors anyway to Sentry but with lower severity. add log namespace annotations. - -## 2.28.0 - -### Minor Changes - -- 32730a4: improve error reporting - -## 2.27.0 - -### Minor Changes - -- 9b2e385: update packages - -## 2.26.3 - -### Patch Changes - -- 02c4141: fix type - -## 2.26.2 - -### Patch Changes - -- 411c3d5: fix type - -## 2.26.1 - -### Patch Changes - -- 709c9cc: fix missing \_tag on TaggedClass - -## 2.26.0 - -### Minor Changes - -- 2d0ad81: update packages - -## 2.25.0 - -### Minor Changes - -- f597755: feat: remove void check and force void conversion at runtime. - -## 2.24.3 - -### Patch Changes - -- 9d11dfb: housekeeping: update packages - -## 2.24.2 - -### Patch Changes - -- 42e6776: fix Map error - -## 2.24.1 - -### Patch Changes - -- 469f28d: fix: groupByT should support any key type -- ab9d1ea: housekeeping: update packages - -## 2.24.0 - -### Minor Changes - -- 3fa0eda: fix: stream error, change to nostream - -## 2.23.2 - -### Patch Changes - -- 1c3af52: fix HttpClient type - -## 2.23.1 - -### Patch Changes - -- 93e88c2: fix: export - -## 2.23.0 - -### Minor Changes - -- c8ebfb4: update packages - -## 2.22.0 - -### Minor Changes - -- 67023f8: kill \_cjs - -## 2.21.0 - -### Minor Changes - -- b1396c9: refactor: rename ApiClient to ApiClientFactory - -## 2.20.0 - -### Minor Changes - -- 3388736: feat: add `cause` support to errors - -### Patch Changes - -- 9f504c6: update packages - -## 2.19.5 - -### Patch Changes - -- 47eb94e: cleanup legacy annotations - -## 2.19.4 - -### Patch Changes - -- fix: return type - -## 2.19.3 - -### Patch Changes - -- 750b46a: update packages - -## 2.19.2 - -### Patch Changes - -- 1bfc3d8: update packages - -## 2.19.1 - -### Patch Changes - -- 5ef6481: update packages - -## 2.19.0 - -### Minor Changes - -- 29a1a65: update packages - -## 2.18.0 - -### Minor Changes - -- fe7783b: improve: TaggedUnion .is is now S.is, isA, is the old is - -## 2.17.0 - -### Minor Changes - -- 47a543b: remove transform-paths as default plugin -- d84aef7: update packages - -## 2.16.0 - -### Minor Changes - -- cc2c43e: update packages - -## 2.15.0 - -### Minor Changes - -- improve: clientFor as part of service so that cache is also fine. - -## 2.14.0 - -### Minor Changes - -- a30da33: improve: cleanup client legacy -- 9301abb: cleanup legacy - -## 2.13.3 - -### Patch Changes - -- 125e2ea: update packages - -## 2.13.2 - -### Patch Changes - -- 54f700f: fix: missing empty config - -## 2.13.1 - -### Patch Changes - -- 0feb32c: update packages - -## 2.13.0 - -### Minor Changes - -- bump - -## 2.12.0 - -### Minor Changes - -- 82363f6: add: Schema.contextFromServices - -## 2.11.2 - -### Patch Changes - -- c7c76b4: update packages - -## 2.11.1 - -### Patch Changes - -- c971d73: update packages - -## 2.11.0 - -### Minor Changes - -- update - -## 2.10.0 - -### Minor Changes - -- f19762b: update packages - -## 2.9.0 - -### Minor Changes - -- 565d678: improve: strict dependencies for routing and services. - -## 2.8.2 - -### Patch Changes - -- c2fa0ed: fix error schema - -## 2.8.1 - -### Patch Changes - -- 1e83e5c: fix type - -## 2.8.0 - -### Minor Changes - -- ba1c7fc: feat: add support for specifying success fields directly. - closes #102 - -### Patch Changes - -- 412cb3d: update packages - -## 2.7.0 - -### Minor Changes - -- 590e5bf: update packages - -## 2.6.0 - -### Minor Changes - -- d4ab73f: update packages - -## 2.5.2 - -### Patch Changes - -- 19ea035: update packages - -## 2.5.1 - -### Patch Changes - -- 724a1a8: error bs - -## 2.5.0 - -### Minor Changes - -- e95cab7: improve: also treat cosmos 409 error as OptimisticConcurrencyException - -## 2.4.0 - -### Minor Changes - -- d5623a6: update packages - -## 2.3.0 - -### Minor Changes - -- 9ef18f6: adapt Prelude workaround - -### Patch Changes - -- 9ef18f6: adapt Prelude workaround - -## 2.2.1 - -### Patch Changes - -- f68dbd2: update packages - -## 2.2.0 - -### Minor Changes - -- 02c3f55: refactor: restructure infra - -## 2.1.1 - -### Patch Changes - -- bs - -## 2.1.0 - -### Minor Changes - -- a34ba2a: add Schema.provide - -## 2.0.2 - -### Patch Changes - -- 25b154d: fix circular - -## 2.0.1 - -### Patch Changes - -- 39e4f3d: fix: missing export - -## 2.0.0 - -### Major Changes - -- 13abe12: refactor: merge core into prelude - -## 1.32.0 - -### Minor Changes - -- d0471b1: refactor: merge @effect-app/schema into core - -### Patch Changes - -- Updated dependencies [d0471b1] - - @effect-app/core@1.19.0 - -## 1.31.0 - -### Minor Changes - -- 3872061: migrate to effect/Schema - -### Patch Changes - -- Updated dependencies [3872061] - - @effect-app/schema@1.20.0 - - @effect-app/core@1.18.0 - -## 1.30.5 - -### Patch Changes - -- e836dcc: cleanup - -## 1.30.4 - -### Patch Changes - -- 0eac9da: improve: Operation error handling - -## 1.30.3 - -### Patch Changes - -- 4a814f5: improve error report - -## 1.30.2 - -### Patch Changes - -- cda1477: update packages -- Updated dependencies [cda1477] - - @effect-app/schema@1.19.1 - - @effect-app/core@1.17.1 - -## 1.30.1 - -### Patch Changes - -- cleanup - -## 1.30.0 - -### Minor Changes - -- c069b8e: add WithOptionalSpan - -## 1.29.0 - -### Minor Changes - -- 3d4e45a: cleanup FiberSets - -## 1.28.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@1.19.0 - - @effect-app/core@1.17.0 - -## 1.27.0 - -### Minor Changes - -- 9ddb31b: update packages - -### Patch Changes - -- Updated dependencies [9ddb31b] - - @effect-app/schema@1.18.0 - - @effect-app/core@1.16.0 - -## 1.26.8 - -### Patch Changes - -- 0ea3023: drop pipe ext, should've known -- Updated dependencies [0ea3023] - - @effect-app/core@1.15.8 - - @effect-app/schema@1.17.8 - -## 1.26.7 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@1.15.7 - - @effect-app/schema@1.17.7 - -## 1.26.6 - -### Patch Changes - -- Updated dependencies [c57fd1b] - - @effect-app/core@1.15.6 - - @effect-app/schema@1.17.6 - -## 1.26.5 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@1.15.5 - - @effect-app/schema@1.17.5 - -## 1.26.4 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@1.15.4 - - @effect-app/schema@1.17.4 - -## 1.26.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@1.15.3 - - @effect-app/schema@1.17.3 - -## 1.26.2 - -### Patch Changes - -- Updated dependencies [56b764d] - - @effect-app/core@1.15.2 - - @effect-app/schema@1.17.2 - -## 1.26.1 - -### Patch Changes - -- Updated dependencies [56df0f3] - - @effect-app/core@1.15.1 - - @effect-app/schema@1.17.1 - -## 1.26.0 - -### Minor Changes - -- 0effd97: update packages - -### Patch Changes - -- Updated dependencies [0effd97] - - @effect-app/schema@1.17.0 - - @effect-app/core@1.15.0 - -## 1.25.0 - -### Minor Changes - -- a2ac359: add pipe extensions experiment - -### Patch Changes - -- Updated dependencies [a2ac359] - - @effect-app/core@1.14.0 - - @effect-app/schema@1.16.2 - -## 1.24.1 - -### Patch Changes - -- Updated dependencies [61eb517] - - @effect-app/core@1.13.1 - - @effect-app/schema@1.16.1 - -## 1.24.0 - -### Minor Changes - -- b9b64b8: housekeeping: update packages - -### Patch Changes - -- Updated dependencies [b9b64b8] - - @effect-app/schema@1.16.0 - - @effect-app/core@1.13.0 - -## 1.23.1 - -### Patch Changes - -- 16bc3b2: fix typo - -## 1.23.0 - -### Minor Changes - -- 67a202a: fix location - -## 1.22.1 - -### Patch Changes - -- fup - -## 1.22.0 - -### Minor Changes - -- c2487d4: remove legacy FetchResponse - -## 1.21.1 - -### Patch Changes - -- 66086f3: f - -## 1.21.0 - -### Minor Changes - -- cleanup all legacy router cruft - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@1.15.0 - -## 1.20.0 - -### Minor Changes - -- 65dcc73: update packages - -### Patch Changes - -- Updated dependencies [65dcc73] - - @effect-app/schema@1.14.0 - - @effect-app/core@1.12.0 - -## 1.19.1 - -### Patch Changes - -- supply layer - -## 1.19.0 - -### Minor Changes - -- 4d2b804: housekeeping: update packages - -### Patch Changes - -- Updated dependencies [4d2b804] - - @effect-app/schema@1.13.0 - - @effect-app/core@1.11.0 - -## 1.18.0 - -### Minor Changes - -- b670052: feat: followup rpc client side - -## 1.17.5 - -### Patch Changes - -- 447cba4: fix paths - -## 1.17.4 - -### Patch Changes - -- 550ba43: control log levels -- Updated dependencies [550ba43] - - @effect-app/core@1.10.2 - - @effect-app/schema@1.12.2 - -## 1.17.3 - -### Patch Changes - -- 6a3e66a: feat: expose tags on TaggedUnion - -## 1.17.2 - -### Patch Changes - -- 13b7aa1: fix: TaggedUnion should inherit the rest of Union - -## 1.17.1 - -### Patch Changes - -- 38c0e65: cleanup - -## 1.17.0 - -### Minor Changes - -- 190b139: update packages - -## 1.16.1 - -### Patch Changes - -- c710953: update packagesy -- Updated dependencies [3de8bfb] - - @effect-app/core@1.10.1 - - @effect-app/schema@1.12.1 - -## 1.16.0 - -### Minor Changes - -- 4bc6e99: update packages -- 1018e58: update packages - -### Patch Changes - -- Updated dependencies [4bc6e99] -- Updated dependencies [1018e58] - - @effect-app/schema@1.12.0 - - @effect-app/core@1.10.0 - -## 1.15.5 - -### Patch Changes - -- d9fd446: fix type - -## 1.15.4 - -### Patch Changes - -- 70c6222: improve doc - -## 1.15.3 - -### Patch Changes - -- 97a0bf8: fix client type - -## 1.15.2 - -### Patch Changes - -- fb62f02: bwc - -## 1.15.1 - -### Patch Changes - -- 2b56d7e: cleanup routing -- 73d5e0a: cleanup routing - -## 1.15.0 - -### Minor Changes - -- feat: add new router support - -## 1.14.0 - -### Minor Changes - -- b3d9a3e: update packages - -### Patch Changes - -- Updated dependencies [b3d9a3e] - - @effect-app/schema@1.11.0 - - @effect-app/core@1.9.0 - -## 1.13.4 - -### Patch Changes - -- d08286c: update packages -- Updated dependencies [d08286c] - - @effect-app/schema@1.10.1 - - @effect-app/core@1.8.1 - -## 1.13.3 - -### Patch Changes - -- 6c81e3c: improve: remove framework stacktrace location - -## 1.13.2 - -### Patch Changes - -- a64695e: fix runtime error - -## 1.13.1 - -### Patch Changes - -- e5fc429: fix types - -## 1.13.0 - -### Minor Changes - -- c5f5223: update packages - -### Patch Changes - -- Updated dependencies [c5f5223] - - @effect-app/schema@1.10.0 - - @effect-app/core@1.8.0 - -## 1.12.5 - -### Patch Changes - -- Updated dependencies [5cb4b35] - - @effect-app/schema@1.9.5 - -## 1.12.4 - -### Patch Changes - -- Updated dependencies [0151436] - - @effect-app/schema@1.9.4 - -## 1.12.3 - -### Patch Changes - -- update packages -- Updated dependencies - - @effect-app/schema@1.9.3 - - @effect-app/core@1.7.5 - -## 1.12.2 - -### Patch Changes - -- e6eecd2: update packages -- Updated dependencies [e6eecd2] - - @effect-app/schema@1.9.2 - - @effect-app/core@1.7.4 - -## 1.12.1 - -### Patch Changes - -- ff -- Updated dependencies - - @effect-app/core@1.7.3 - - @effect-app/schema@1.9.1 - -## 1.12.0 - -### Minor Changes - -- 056f293: fix type - -### Patch Changes - -- Updated dependencies [056f293] - - @effect-app/schema@1.9.0 - -## 1.11.3 - -### Patch Changes - -- fix -- Updated dependencies - - @effect-app/core@1.7.2 - - @effect-app/schema@1.8.1 - -## 1.11.2 - -### Patch Changes - -- 65135b2: fix issue - -## 1.11.1 - -### Patch Changes - -- c229a73: add tagMap to TaggedUnion - -## 1.11.0 - -### Minor Changes - -- 24f0c0f: feat: allow custom errors per endpoint. - -### Patch Changes - -- Updated dependencies [24f0c0f] - - @effect-app/schema@1.8.0 - -## 1.10.2 - -### Patch Changes - -- 1ac74ec: cleanup tsconfig base -- 0b399ce: update packages -- Updated dependencies [0b399ce] - - @effect-app/schema@1.7.1 - - @effect-app/core@1.7.1 - -## 1.10.1 - -### Patch Changes - -- 9fbf129: fix: messagefallback - -## 1.10.0 - -### Minor Changes - -- 2143dd9: update packages - -### Patch Changes - -- Updated dependencies [2143dd9] - - @effect-app/schema@1.7.0 - - @effect-app/core@1.7.0 - -## 1.9.2 - -### Patch Changes - -- e120966: update packages -- f6066ac: update packages -- Updated dependencies [e120966] -- Updated dependencies [f6066ac] - - @effect-app/schema@1.6.1 - - @effect-app/core@1.6.1 - -## 1.9.1 - -### Patch Changes - -- 26551ce: improve rendering - -## 1.9.0 - -### Minor Changes - -- 79f58af: update packages - -### Patch Changes - -- Updated dependencies [79f58af] - - @effect-app/schema@1.6.0 - - @effect-app/core@1.6.0 - -## 1.8.2 - -### Patch Changes - -- b3ebc41: update packages -- Updated dependencies [b3ebc41] - - @effect-app/schema@1.5.2 - - @effect-app/core@1.5.2 - -## 1.8.1 - -### Patch Changes - -- f569681: update packages -- Updated dependencies [f569681] - - @effect-app/schema@1.5.1 - - @effect-app/core@1.5.1 - -## 1.8.0 - -### Minor Changes - -- d12fc1a: improve: expose interrupt, not waitUntilEmpty on FiberSets - -## 1.7.0 - -### Minor Changes - -- 613c148: update packages - -### Patch Changes - -- Updated dependencies [613c148] - - @effect-app/schema@1.5.0 - - @effect-app/core@1.5.0 - -## 1.6.0 - -### Minor Changes - -- 8d0c511: update packages - -### Patch Changes - -- Updated dependencies [8d0c511] - - @effect-app/schema@1.4.0 - - @effect-app/core@1.4.0 - -## 1.5.2 - -### Patch Changes - -- Updated dependencies [b48c630] - - @effect-app/schema@1.3.1 - -## 1.5.1 - -### Patch Changes - -- 8438eed: fix ConfigSecretURL - -## 1.5.0 - -### Minor Changes - -- 9cc6f86: update packages - -### Patch Changes - -- Updated dependencies [9cc6f86] - - @effect-app/schema@1.3.0 - - @effect-app/core@1.3.0 - -## 1.4.8 - -### Patch Changes - -- fe2754a: improve: decode span -- 349c421: Update packages -- Updated dependencies [349c421] - - @effect-app/schema@1.2.8 - - @effect-app/core@1.2.3 - -## 1.4.7 - -### Patch Changes - -- 744cf0f: add client.response span - -## 1.4.6 - -### Patch Changes - -- 9d944a1: update packages -- Updated dependencies [9d944a1] - - @effect-app/schema@1.2.7 - - @effect-app/core@1.2.2 - -## 1.4.5 - -### Patch Changes - -- a67d115: improve client trace - -## 1.4.4 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@1.2.6 - -## 1.4.3 - -### Patch Changes - -- 24c3fe9: align brands -- Updated dependencies [24c3fe9] - - @effect-app/schema@1.2.5 - -## 1.4.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@1.2.4 - -## 1.4.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@1.2.3 - -## 1.4.0 - -### Minor Changes - -- c10818c: improve forking - -### Patch Changes - -- 2c661c1: update packages -- Updated dependencies [2c661c1] - - @effect-app/schema@1.2.2 - - @effect-app/core@1.2.1 - -## 1.3.1 - -### Patch Changes - -- Updated dependencies [92f303f] - - @effect-app/schema@1.2.1 - -## 1.3.0 - -### Minor Changes - -- 9ac9416: update packages - -### Patch Changes - -- Updated dependencies [9ac9416] - - @effect-app/schema@1.2.0 - - @effect-app/core@1.2.0 - -## 1.2.5 - -### Patch Changes - -- revert - -## 1.2.4 - -### Patch Changes - -- 6335139: allow error - -## 1.2.3 - -### Patch Changes - -- 3411e78: update packages -- Updated dependencies [3411e78] - - @effect-app/schema@1.1.3 - - @effect-app/core@1.1.3 - -## 1.2.2 - -### Patch Changes - -- 128f636: update packages -- Updated dependencies [128f636] - - @effect-app/schema@1.1.2 - - @effect-app/core@1.1.2 - -## 1.2.1 - -### Patch Changes - -- Updated dependencies [132bbc4] - - @effect-app/core@1.1.1 - - @effect-app/schema@1.1.1 - -## 1.2.0 - -### Minor Changes - -- 2e29520: update packages - -### Patch Changes - -- Updated dependencies [2e29520] - - @effect-app/schema@1.1.0 - - @effect-app/core@1.1.0 - -## 1.1.1 - -### Patch Changes - -- 4dba4a5: add annotateSpanWithError - -## 1.1.0 - -### Minor Changes - -- b8b729e: adopt @effect-rx - -## 1.0.12 - -### Patch Changes - -- c99cdbb: update packages -- Updated dependencies [c99cdbb] - - @effect-app/schema@1.0.10 - - @effect-app/core@1.0.6 - -## 1.0.11 - -### Patch Changes - -- feat: improve error reported state. - -## 1.0.10 - -### Patch Changes - -- Updated dependencies [f202812] - - @effect-app/schema@1.0.9 - -## 1.0.9 - -### Patch Changes - -- 83f19e1: schema next minor -- Updated dependencies [83f19e1] - - @effect-app/schema@1.0.8 - -## 1.0.8 - -### Patch Changes - -- 86d33e8: add error reporter for vue - -## 1.0.7 - -### Patch Changes - -- c4dbb6c: update packages -- Updated dependencies [c4dbb6c] - - @effect-app/schema@1.0.7 - - @effect-app/core@1.0.5 - -## 1.0.6 - -### Patch Changes - -- 93f53d4: update packages -- Updated dependencies [93f53d4] - - @effect-app/schema@1.0.6 - - @effect-app/core@1.0.4 - -## 1.0.5 - -### Patch Changes - -- Updated dependencies [9c69b5b] - - @effect-app/core@1.0.3 - - @effect-app/schema@1.0.5 - -## 1.0.4 - -### Patch Changes - -- Updated dependencies [6363ff5] - - @effect-app/core@1.0.2 - - @effect-app/schema@1.0.4 - -## 1.0.3 - -### Patch Changes - -- bf702ba: update packages -- Updated dependencies [bf702ba] - - @effect-app/schema@1.0.3 - - @effect-app/core@1.0.1 - -## 1.0.2 - -### Patch Changes - -- Updated dependencies [156664e] - - @effect-app/schema@1.0.2 - -## 1.0.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@1.0.1 - -## 1.0.0 - -### Major Changes - -- 4e9992f: effect 3.0! - -### Patch Changes - -- Updated dependencies [4e9992f] - - @effect-app/schema@1.0.0 - - @effect-app/core@1.0.0 - -## 0.204.0 - -### Minor Changes - -- version prob - -## 0.203.1 - -### Patch Changes - -- ff5d466: cleanup Struct type -- Updated dependencies [ff5d466] - - @effect-app/schema@0.221.1 - -## 0.203.0 - -### Minor Changes - -- 0d1b378: update to latest schema - -### Patch Changes - -- Updated dependencies [0d1b378] - - @effect-app/schema@0.221.0 - - @effect-app/core@0.170.0 - -## 0.202.1 - -### Patch Changes - -- 45770ea: re-add MainFiberSet join logging - -## 0.202.0 - -### Minor Changes - -- c0a2bb1: rename FiberBag to MainFiberSet - -## 0.201.6 - -### Patch Changes - -- 4a242e9: use built in FiberSet.join - -## 0.201.5 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.169.1 - - @effect-app/schema@0.220.4 - -## 0.201.4 - -### Patch Changes - -- 36fb254: fix emptyto check - -## 0.201.3 - -### Patch Changes - -- Updated dependencies [0ad9ea9] - - @effect-app/schema@0.220.3 - -## 0.201.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.220.2 - -## 0.201.1 - -### Patch Changes - -- Updated dependencies [89bdcf0] - - @effect-app/schema@0.220.1 - -## 0.201.0 - -### Minor Changes - -- 290a709: update packages - -### Patch Changes - -- Updated dependencies [290a709] - - @effect-app/schema@0.220.0 - - @effect-app/core@0.169.0 - -## 0.200.0 - -### Minor Changes - -- 564a95c: fix branded schemas - -### Patch Changes - -- Updated dependencies [564a95c] - - @effect-app/schema@0.219.0 - -## 0.199.0 - -### Minor Changes - -- ecb05f6: update packages -- fbfa538: add title to Operations and expose all - -### Patch Changes - -- Updated dependencies [ecb05f6] - - @effect-app/schema@0.218.0 - - @effect-app/core@0.168.0 - -## 0.198.2 - -### Patch Changes - -- Updated dependencies [7b1ff1c] - - @effect-app/core@0.167.0 - - @effect-app/schema@0.217.2 - -## 0.198.1 - -### Patch Changes - -- Updated dependencies [e165c07] - - @effect-app/schema@0.217.1 - -## 0.198.0 - -### Minor Changes - -- 01f8283: cleanup: drop legacy `FromClass` - -### Patch Changes - -- Updated dependencies [01f8283] - - @effect-app/schema@0.217.0 - -## 0.197.0 - -### Minor Changes - -- revert form changes - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.166.0 - - @effect-app/schema@0.216.0 - -## 0.196.2 - -### Patch Changes - -- Updated dependencies [2044733] - - @effect-app/core@0.165.0 - - @effect-app/schema@0.215.2 - -## 0.196.1 - -### Patch Changes - -- Updated dependencies [a9a81a1] - - @effect-app/core@0.164.0 - - @effect-app/schema@0.215.1 - -## 0.196.0 - -### Minor Changes - -- 7a3f05c: update packages - -### Patch Changes - -- Updated dependencies [7a3f05c] - - @effect-app/core@0.163.0 - - @effect-app/schema@0.215.0 - -## 0.195.0 - -### Minor Changes - -- bbaa558: bs - -### Patch Changes - -- Updated dependencies [bbaa558] - - @effect-app/core@0.162.0 - - @effect-app/schema@0.214.0 - -## 0.194.1 - -### Patch Changes - -- Updated dependencies [ac37ff7] - - @effect-app/fluent-extensions@0.155.1 - - @effect-app/core@0.161.1 - - @effect-app/schema@0.213.1 - -## 0.194.0 - -### Minor Changes - -- e5b0d8e: update packages -- 2e2fa98: update packages -- 44ef913: update packages - -### Patch Changes - -- Updated dependencies [e5b0d8e] -- Updated dependencies [2e2fa98] -- Updated dependencies [44ef913] - - @effect-app/fluent-extensions@0.155.0 - - @effect-app/schema@0.213.0 - - @effect-app/core@0.161.0 - -## 0.193.0 - -### Minor Changes - -- 494842c: update packages - -### Patch Changes - -- Updated dependencies [494842c] - - @effect-app/fluent-extensions@0.154.0 - - @effect-app/schema@0.212.0 - - @effect-app/core@0.160.0 - -## 0.192.1 - -### Patch Changes - -- Updated dependencies [413bdee] - - @effect-app/core@0.159.1 - - @effect-app/schema@0.211.2 - -## 0.192.0 - -### Minor Changes - -- 215071f: cleanup TagClass -- af56c32: Integrate Service in Context - -### Patch Changes - -- Updated dependencies [af56c32] - - @effect-app/core@0.159.0 - - @effect-app/schema@0.211.1 - -## 0.191.0 - -### Minor Changes - -- cb84068: replace lodash with builtin and cleanup - -### Patch Changes - -- Updated dependencies [cb84068] - - @effect-app/fluent-extensions@0.153.0 - - @effect-app/schema@0.211.0 - - @effect-app/core@0.158.0 - -## 0.190.0 - -### Minor Changes - -- 790bfc2: cleanup - -### Patch Changes - -- Updated dependencies [790bfc2] - - @effect-app/fluent-extensions@0.152.0 - - @effect-app/core@0.157.0 - - @effect-app/schema@0.210.2 - -## 0.189.1 - -### Patch Changes - -- 0334285: update packages -- Updated dependencies [0334285] -- Updated dependencies [b9f35eb] - - @effect-app/fluent-extensions@0.151.0 - - @effect-app/schema@0.210.1 - - @effect-app/core@0.156.1 - -## 0.189.0 - -### Minor Changes - -- 498deb0: update packages - -### Patch Changes - -- Updated dependencies [498deb0] - - @effect-app/fluent-extensions@0.150.0 - - @effect-app/schema@0.210.0 - - @effect-app/core@0.156.0 - -## 0.188.0 - -### Minor Changes - -- fd8f019: update packages - -### Patch Changes - -- Updated dependencies [fd8f019] - - @effect-app/fluent-extensions@0.149.0 - - @effect-app/schema@0.209.0 - - @effect-app/core@0.155.0 - -## 0.187.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.208.1 - -## 0.187.0 - -### Minor Changes - -- 51253a2: update packages - -### Patch Changes - -- Updated dependencies [51253a2] - - @effect-app/fluent-extensions@0.148.0 - - @effect-app/schema@0.208.0 - - @effect-app/core@0.154.0 - -## 0.186.2 - -### Patch Changes - -- 0a0b8e9: add ServiceUnavailableError - -## 0.186.1 - -### Patch Changes - -- Updated dependencies [df2a290] - - @effect-app/schema@0.207.1 - -## 0.186.0 - -### Minor Changes - -- f2c3553: update packages - -### Patch Changes - -- Updated dependencies [f2c3553] - - @effect-app/fluent-extensions@0.147.0 - - @effect-app/schema@0.207.0 - - @effect-app/core@0.153.0 - -## 0.185.8 - -### Patch Changes - -- Updated dependencies [2b53680] - - @effect-app/schema@0.206.7 - -## 0.185.7 - -### Patch Changes - -- Updated dependencies [2794b0a] - - @effect-app/schema@0.206.6 - -## 0.185.6 - -### Patch Changes - -- Updated dependencies [41bec85] - - @effect-app/schema@0.206.5 - -## 0.185.5 - -### Patch Changes - -- Updated dependencies [511d81b] - - @effect-app/schema@0.206.4 - -## 0.185.4 - -### Patch Changes - -- df3a550: fup - -## 0.185.3 - -### Patch Changes - -- f385129: fix legacy struct use -- Updated dependencies [f385129] - - @effect-app/schema@0.206.3 - -## 0.185.2 - -### Patch Changes - -- Updated dependencies [fe910fe] - - @effect-app/schema@0.206.2 - -## 0.185.1 - -### Patch Changes - -- Updated dependencies [dbc7c83] - - @effect-app/schema@0.206.1 - -## 0.185.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/fluent-extensions@0.146.0 - - @effect-app/schema@0.206.0 - - @effect-app/core@0.152.1 - -## 0.184.0 - -### Minor Changes - -- 5178025: update packages - -### Patch Changes - -- Updated dependencies [5178025] - - @effect-app/fluent-extensions@0.145.0 - - @effect-app/schema@0.205.0 - - @effect-app/core@0.152.0 - -## 0.183.2 - -### Patch Changes - -- c7b8007: bag it - -## 0.183.1 - -### Patch Changes - -- Updated dependencies [58233dd] - - @effect-app/core@0.151.1 - - @effect-app/schema@0.204.1 - -## 0.183.0 - -### Minor Changes - -- 77366cc: don't go through service constructor - -## 0.182.1 - -### Patch Changes - -- f188543: update accessor shape - -## 0.182.0 - -### Minor Changes - -- d0a1774: update ts to 5.4.1-rc - -### Patch Changes - -- Updated dependencies [d0a1774] - - @effect-app/fluent-extensions@0.144.0 - - @effect-app/schema@0.204.0 - - @effect-app/core@0.151.0 - -## 0.181.8 - -### Patch Changes - -- Updated dependencies [ce64bc5] - - @effect-app/schema@0.203.4 - -## 0.181.7 - -### Patch Changes - -- Updated dependencies [bbf24a6] - - @effect-app/schema@0.203.3 - -## 0.181.6 - -### Patch Changes - -- Updated dependencies [e12b4f8] - - @effect-app/schema@0.203.2 - -## 0.181.5 - -### Patch Changes - -- Updated dependencies [09bc0d5] - - @effect-app/schema@0.203.1 - -## 0.181.4 - -### Patch Changes - -- Updated dependencies [e355a45] - - @effect-app/schema@0.203.0 - -## 0.181.3 - -### Patch Changes - -- Updated dependencies [f4bea89] - - @effect-app/fluent-extensions@0.143.4 - - @effect-app/core@0.150.5 - - @effect-app/schema@0.202.5 - -## 0.181.2 - -### Patch Changes - -- Updated dependencies [ea3dd75] - - @effect-app/fluent-extensions@0.143.3 - - @effect-app/core@0.150.4 - - @effect-app/schema@0.202.4 - -## 0.181.1 - -### Patch Changes - -- 579f42d: fix typo - -## 0.181.0 - -### Minor Changes - -- 752958a: awesome tag - -## 0.180.10 - -### Patch Changes - -- abb5332: add ServiceShape - -## 0.180.9 - -### Patch Changes - -- Updated dependencies [cef7a30] - - @effect-app/fluent-extensions@0.143.2 - - @effect-app/core@0.150.3 - - @effect-app/schema@0.202.3 - -## 0.180.8 - -### Patch Changes - -- 190415c: fix - -## 0.180.7 - -### Patch Changes - -- 6ece717: base FiberBag on FiberSet - -## 0.180.6 - -### Patch Changes - -- dba2711: fix generic - -## 0.180.5 - -### Patch Changes - -- e470bd5: add forkDaemon and forkScoped in FiberBag. - -## 0.180.4 - -### Patch Changes - -- Updated dependencies [81f152f] - - @effect-app/core@0.150.2 - - @effect-app/schema@0.202.2 - -## 0.180.3 - -### Patch Changes - -- add fiberbag - -## 0.180.2 - -### Patch Changes - -- Updated dependencies [ef4b0ba] - - @effect-app/fluent-extensions@0.143.1 - - @effect-app/core@0.150.1 - - @effect-app/schema@0.202.1 - -## 0.180.1 - -### Patch Changes - -- f2eea61: fix TagClass - -## 0.180.0 - -### Minor Changes - -- 7346691: update packages - -### Patch Changes - -- Updated dependencies [7346691] - - @effect-app/fluent-extensions@0.143.0 - - @effect-app/schema@0.202.0 - - @effect-app/core@0.150.0 - -## 0.179.0 - -### Minor Changes - -- add AcessTag support - -## 0.178.1 - -### Patch Changes - -- fix - -## 0.178.0 - -### Minor Changes - -- 0354650: prototype - -## 0.177.0 - -### Minor Changes - -- 32f852e: add requestname to span - -## 0.176.2 - -### Patch Changes - -- Updated dependencies [d1e4f1b] - - @effect-app/schema@0.201.2 - -## 0.176.1 - -### Patch Changes - -- Updated dependencies [0b514ad] - - @effect-app/schema@0.201.1 - -## 0.176.0 - -### Minor Changes - -- e8e8ee2: update packages - -### Patch Changes - -- Updated dependencies [e8e8ee2] - - @effect-app/fluent-extensions@0.142.0 - - @effect-app/schema@0.201.0 - - @effect-app/core@0.149.0 - -## 0.175.1 - -### Patch Changes - -- Updated dependencies [ee00776] - - @effect-app/schema@0.200.0 - -## 0.175.0 - -### Minor Changes - -- 117be64: bump - -### Patch Changes - -- Updated dependencies [117be64] - - @effect-app/core@0.148.0 - - @effect-app/fluent-extensions@0.141.0 - - @effect-app/schema@0.199.0 - -## 0.174.1 - -### Patch Changes - -- 4008eb1: cleanup and add byIdAndSaveWithPure - -## 0.174.0 - -### Minor Changes - -- A new day -- 78b3c9e: reduce tspuse - -### Patch Changes - -- Updated dependencies -- Updated dependencies [78b3c9e] - - @effect-app/core@0.147.0 - - @effect-app/fluent-extensions@0.140.0 - - @effect-app/schema@0.198.0 - -## 0.173.4 - -### Patch Changes - -- 9577608: cleanup - -## 0.173.3 - -### Patch Changes - -- 4d9b13c: add extendTaggedUnion - -## 0.173.2 - -### Patch Changes - -- Updated dependencies [3170396] - - @effect-app/core@0.146.1 - - @effect-app/schema@0.197.1 - -## 0.173.1 - -### Patch Changes - -- ac8e9aa: add OperationResult.is - -## 0.173.0 - -### Minor Changes - -- c070e0d: update effect - -### Patch Changes - -- dd0cf42: update -- Updated dependencies [c070e0d] -- Updated dependencies [dd0cf42] - - @effect-app/fluent-extensions@0.139.0 - - @effect-app/schema@0.197.0 - - @effect-app/core@0.146.0 - -## 0.172.0 - -### Minor Changes - -- 57fcfef: update packages - -### Patch Changes - -- 739e742: add missing plugins in tsconfig -- Updated dependencies [57fcfef] - - @effect-app/fluent-extensions@0.138.0 - - @effect-app/schema@0.196.0 - - @effect-app/core@0.145.0 - -## 0.171.4 - -### Patch Changes - -- Updated dependencies [b5543d4] - - @effect-app/core@0.144.3 - - @effect-app/schema@0.195.3 - -## 0.171.3 - -### Patch Changes - -- Updated dependencies [39beb09] - - @effect-app/core@0.144.2 - - @effect-app/schema@0.195.2 - -## 0.171.2 - -### Patch Changes - -- 0fd4175: f - -## 0.171.1 - -### Patch Changes - -- 73c3e1b: update packages -- Updated dependencies [73c3e1b] - - @effect-app/fluent-extensions@0.137.1 - - @effect-app/core@0.144.1 - - @effect-app/schema@0.195.1 - -## 0.171.0 - -### Minor Changes - -- 49fb62c: align Tag arg order. - -## 0.170.0 - -### Minor Changes - -- 76ea9ae: improve - -## 0.169.0 - -### Minor Changes - -- 7079fc8: connection reset by peer - -### Patch Changes - -- Updated dependencies [7079fc8] - - @effect-app/fluent-extensions@0.137.0 - - @effect-app/schema@0.195.0 - - @effect-app/core@0.144.0 - -## 0.168.0 - -### Minor Changes - -- update TagClassId - -## 0.167.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/fluent-extensions@0.136.1 - - @effect-app/core@0.143.1 - - @effect-app/schema@0.194.2 - -## 0.167.1 - -### Patch Changes - -- Updated dependencies [c4ac357] - - @effect-app/core@0.143.0 - - @effect-app/schema@0.194.1 - -## 0.167.0 - -### Minor Changes - -- 8374f61: update packages - -### Patch Changes - -- Updated dependencies [8374f61] - - @effect-app/fluent-extensions@0.136.0 - - @effect-app/schema@0.194.0 - - @effect-app/core@0.142.0 - -## 0.166.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.141.1 - - @effect-app/schema@0.193.1 - -## 0.166.0 - -### Minor Changes - -- de336f6: src - -### Patch Changes - -- Updated dependencies [de336f6] - - @effect-app/fluent-extensions@0.135.0 - - @effect-app/schema@0.193.0 - - @effect-app/core@0.141.0 - -## 0.165.2 - -### Patch Changes - -- dbbd0da: fix path - -## 0.165.1 - -### Patch Changes - -- 7715b74: add tsconfig.base.json -- f10e630: update packages -- Updated dependencies [f10e630] - - @effect-app/fluent-extensions@0.134.1 - - @effect-app/schema@0.192.1 - - @effect-app/core@0.140.1 - -## 0.165.0 - -### Minor Changes - -- 47f46f0: use actual action name casing so that editor go to works. - -## 0.164.0 - -### Minor Changes - -- 5f52e06: update packages - -### Patch Changes - -- Updated dependencies [5f52e06] - - @effect-app/fluent-extensions@0.134.0 - - @effect-app/schema@0.192.0 - - @effect-app/core@0.140.0 - -## 0.163.0 - -### Minor Changes - -- m - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.139.0 - - @effect-app/fluent-extensions@0.133.0 - - @effect-app/schema@0.191.0 - -## 0.162.3 - -### Patch Changes - -- 5b30ea1: cleanup - -## 0.162.2 - -### Patch Changes - -- fix name - -## 0.162.1 - -### Patch Changes - -- fix name - -## 0.162.0 - -### Minor Changes - -- bs - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.138.0 - - @effect-app/fluent-extensions@0.132.0 - - @effect-app/schema@0.190.0 - -## 0.161.0 - -### Minor Changes - -- 8794c23: bs - -## 0.160.0 - -### Minor Changes - -- 797056c: update packages - -### Patch Changes - -- Updated dependencies [797056c] - - @effect-app/fluent-extensions@0.131.0 - - @effect-app/core@0.137.0 - - @effect-app/schema@0.189.1 - -## 0.159.0 - -### Minor Changes - -- 6145788: switch to tanstack-query - -## 0.158.5 - -### Patch Changes - -- b4ee7e7: f - -## 0.158.4 - -### Patch Changes - -- pf - -## 0.158.3 - -### Patch Changes - -- Updated dependencies [c31e783] - - @effect-app/fluent-extensions@0.130.3 - -## 0.158.2 - -### Patch Changes - -- 4848bc6: bump -- Updated dependencies [4848bc6] - - @effect-app/fluent-extensions@0.130.2 - -## 0.158.1 - -### Patch Changes - -- Updated dependencies [67571c9] - - @effect-app/fluent-extensions@0.130.1 - -## 0.158.0 - -### Minor Changes - -- 12eb7f7: update packages - -### Patch Changes - -- Updated dependencies [12eb7f7] - - @effect-app/fluent-extensions@0.130.0 - - @effect-app/schema@0.189.0 - - @effect-app/core@0.136.0 - -## 0.157.4 - -### Patch Changes - -- Updated dependencies [b525b4f] - - @effect-app/fluent-extensions@0.129.3 - -## 0.157.3 - -### Patch Changes - -- Updated dependencies [aed6cd9] - - @effect-app/fluent-extensions@0.129.2 - -## 0.157.2 - -### Patch Changes - -- Updated dependencies [37c167f] - - @effect-app/fluent-extensions@0.129.1 - -## 0.157.1 - -### Patch Changes - -- 28feba6: fix var - -## 0.157.0 - -### Minor Changes - -- 17cfe0f: update packages - -### Patch Changes - -- Updated dependencies [17cfe0f] - - @effect-app/fluent-extensions@0.129.0 - - @effect-app/schema@0.188.0 - - @effect-app/core@0.135.0 - -## 0.156.5 - -### Patch Changes - -- Updated dependencies [3c1cb25] - - @effect-app/core@0.134.1 - - @effect-app/fluent-extensions@0.128.5 - - @effect-app/schema@0.187.5 - -## 0.156.4 - -### Patch Changes - -- Updated dependencies [eddbf3c] - - @effect-app/core@0.134.0 - - @effect-app/fluent-extensions@0.128.4 - - @effect-app/schema@0.187.4 - -## 0.156.3 - -### Patch Changes - -- 7c28686: update packages -- Updated dependencies [7c28686] - - @effect-app/fluent-extensions@0.128.3 - - @effect-app/schema@0.187.3 - - @effect-app/core@0.133.3 - -## 0.156.2 - -### Patch Changes - -- Updated dependencies [963b8d0] - - @effect-app/core@0.133.2 - - @effect-app/fluent-extensions@0.128.2 - - @effect-app/schema@0.187.2 - -## 0.156.1 - -### Patch Changes - -- b9e1e73: ext -- Updated dependencies [b9e1e73] - - @effect-app/core@0.133.1 - - @effect-app/fluent-extensions@0.128.1 - - @effect-app/schema@0.187.1 - -## 0.156.0 - -### Minor Changes - -- 9b67f67: update packages - -### Patch Changes - -- Updated dependencies [9b67f67] - - @effect-app/fluent-extensions@0.128.0 - - @effect-app/schema@0.187.0 - - @effect-app/core@0.133.0 - -## 0.155.2 - -### Patch Changes - -- b3e095c: fix - -## 0.155.1 - -### Patch Changes - -- fix - -## 0.155.0 - -### Minor Changes - -- 71dbfc3: update packages -- 1cc7ee7: cleanup service tags - -### Patch Changes - -- Updated dependencies [71dbfc3] - - @effect-app/fluent-extensions@0.127.0 - - @effect-app/schema@0.186.0 - - @effect-app/core@0.132.0 - -## 0.154.3 - -### Patch Changes - -- Updated dependencies [8577c90] - - @effect-app/core@0.131.9 - - @effect-app/fluent-extensions@0.126.10 - - @effect-app/schema@0.185.2 - -## 0.154.2 - -### Patch Changes - -- 7db0be1: dual copy/clone -- Updated dependencies [7db0be1] - - @effect-app/core@0.131.8 - - @effect-app/fluent-extensions@0.126.9 - - @effect-app/schema@0.185.1 - -## 0.154.1 - -### Patch Changes - -- Updated dependencies [ac55751] - - @effect-app/schema@0.185.0 - - @effect-app/fluent-extensions@0.126.8 - -## 0.154.0 - -### Minor Changes - -- 83091eb: cleanup - -## 0.153.7 - -### Patch Changes - -- Updated dependencies [dc45062] - - @effect-app/core@0.131.7 - - @effect-app/fluent-extensions@0.126.7 - - @effect-app/schema@0.184.7 - -## 0.153.6 - -### Patch Changes - -- Updated dependencies [fc86a75] - - @effect-app/core@0.131.6 - - @effect-app/fluent-extensions@0.126.6 - - @effect-app/schema@0.184.6 - -## 0.153.5 - -### Patch Changes - -- Updated dependencies [e47df05] - - @effect-app/core@0.131.5 - - @effect-app/fluent-extensions@0.126.5 - - @effect-app/schema@0.184.5 - -## 0.153.4 - -### Patch Changes - -- Updated dependencies [3db43d9] - - @effect-app/core@0.131.4 - - @effect-app/fluent-extensions@0.126.4 - - @effect-app/schema@0.184.4 - -## 0.153.3 - -### Patch Changes - -- Updated dependencies [8dd650f] - - @effect-app/core@0.131.3 - - @effect-app/fluent-extensions@0.126.3 - - @effect-app/schema@0.184.3 - -## 0.153.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.131.2 - - @effect-app/fluent-extensions@0.126.2 - - @effect-app/schema@0.184.2 - -## 0.153.1 - -### Patch Changes - -- Updated dependencies [27e6867] - - @effect-app/core@0.131.1 - - @effect-app/fluent-extensions@0.126.1 - - @effect-app/schema@0.184.1 - -## 0.153.0 - -### Minor Changes - -- c8a07bc: fix path - -### Patch Changes - -- Updated dependencies [c8a07bc] - - @effect-app/core@0.131.0 - - @effect-app/fluent-extensions@0.126.0 - - @effect-app/schema@0.184.0 - -## 0.152.0 - -### Minor Changes - -- 525237e: rename @effect-app/prelude to effect-app - -### Patch Changes - -- Updated dependencies [525237e] - - @effect-app/core@0.130.0 - - @effect-app/fluent-extensions@0.125.9 - - @effect-app/schema@0.183.14 - -## 0.151.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.129.3 - - @effect-app/fluent-extensions@0.125.8 - - @effect-app/schema@0.183.13 - -## 0.151.2 - -### Patch Changes - -- Updated dependencies [0d02cbd] - - @effect-app/core@0.129.2 - - @effect-app/fluent-extensions@0.125.7 - - @effect-app/schema@0.183.12 - -## 0.151.1 - -### Patch Changes - -- Updated dependencies [b01fd1e] - - @effect-app/core@0.129.1 - - @effect-app/fluent-extensions@0.125.6 - - @effect-app/schema@0.183.11 - -## 0.151.0 - -### Minor Changes - -- b2d3e53: consolidate - -### Patch Changes - -- Updated dependencies [b2d3e53] - - @effect-app/core@0.129.0 - - @effect-app/fluent-extensions@0.125.5 - - @effect-app/schema@0.183.10 - -## 0.150.3 - -### Patch Changes - -- 200a840: address -- Updated dependencies [200a840] - - @effect-app/core@0.128.2 - - @effect-app/fluent-extensions@0.125.4 - - @effect-app/schema@0.183.9 - -## 0.150.2 - -### Patch Changes - -- Updated dependencies [0c57301] - - @effect-app/core@0.128.1 - - @effect-app/fluent-extensions@0.125.3 - - @effect-app/schema@0.183.8 - -## 0.150.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.128.0 - - @effect-app/fluent-extensions@0.125.2 - - @effect-app/schema@0.183.7 - -## 0.150.0 - -### Minor Changes - -- 83b5bb6: fix option -- 87753e4: improve prelude export - -### Patch Changes - -- Updated dependencies [83b5bb6] -- Updated dependencies [87753e4] - - @effect-app/core@0.127.0 - - @effect-app/fluent-extensions@0.125.1 - - @effect-app/schema@0.183.6 - -## 0.149.6 - -### Patch Changes - -- 988ed1d: update packages -- Updated dependencies [38bff6b] -- Updated dependencies [988ed1d] - - @effect-app/fluent-extensions@0.125.0 - - @effect-app/core@0.126.1 - - @effect-app/schema@0.183.5 - -## 0.149.5 - -### Patch Changes - -- Updated dependencies [c497ff3] - - @effect-app/schema@0.183.4 - - @effect-app/fluent-extensions@0.124.4 - -## 0.149.4 - -### Patch Changes - -- 3c4251e: fix - -## 0.149.3 - -### Patch Changes - -- Updated dependencies [65f8b7b] - - @effect-app/schema@0.183.3 - - @effect-app/fluent-extensions@0.124.3 - -## 0.149.2 - -### Patch Changes - -- Updated dependencies [8290a87] - - @effect-app/schema@0.183.2 - - @effect-app/fluent-extensions@0.124.2 - -## 0.149.1 - -### Patch Changes - -- Updated dependencies [d17dc32] - - @effect-app/schema@0.183.1 - - @effect-app/fluent-extensions@0.124.1 - -## 0.149.0 - -### Minor Changes - -- e5cdd42: update packages - -### Patch Changes - -- Updated dependencies [e5cdd42] - - @effect-app/fluent-extensions@0.124.0 - - @effect-app/schema@0.183.0 - - @effect-app/core@0.126.0 - -## 0.148.0 - -### Minor Changes - -- update to latest Effect "minor" - -### Patch Changes - -- Updated dependencies - - @effect-app/fluent-extensions@0.123.0 - - @effect-app/schema@0.182.0 - - @effect-app/core@0.125.0 - -## 0.147.0 - -### Minor Changes - -- c29b6ff: bump - -### Patch Changes - -- Updated dependencies [c29b6ff] - - @effect-app/core@0.124.0 - - @effect-app/fluent-extensions@0.122.0 - - @effect-app/schema@0.181.0 - -## 0.145.3 - -### Patch Changes - -- 6e9f253: fix requestid withDefault - -## 0.146.0 - -### Minor Changes - -- 0c47b83: update packages - -### Patch Changes - -- Updated dependencies [0c47b83] - - @effect-app/fluent-extensions@0.121.0 - - @effect-app/schema@0.180.0 - - @effect-app/core@0.123.0 - -## 0.145.2 - -### Patch Changes - -- Updated dependencies [9d31709] - - @effect-app/fluent-extensions@0.120.0 - -## 0.145.1 - -### Patch Changes - -- 871cccf: remove cpy - -## 0.145.0 - -### Minor Changes - -- bump - -### Patch Changes - -- Updated dependencies - - @effect-app/fluent-extensions@0.119.0 - - @effect-app/schema@0.179.0 - - @effect-app/core@0.122.0 - -## 0.144.0 - -### Minor Changes - -- d740bf6: use schema as patch instead - -### Patch Changes - -- Updated dependencies [d740bf6] - - @effect-app/schema@0.178.0 - - @effect-app/fluent-extensions@0.118.1 - -## 0.143.11 - -### Patch Changes - -- Updated dependencies [028af99] - - @effect-app/fluent-extensions@0.118.0 - -## 0.143.10 - -### Patch Changes - -- Updated dependencies [8217bcd] - - @effect-app/fluent-extensions@0.117.4 - -## 0.143.9 - -### Patch Changes - -- Updated dependencies - - @effect-app/fluent-extensions@0.117.3 - -## 0.143.8 - -### Patch Changes - -- Updated dependencies - - @effect-app/fluent-extensions@0.116.6 - -## 0.143.7 - -### Patch Changes - -- Updated dependencies [e1c9d4c] - - @effect-app/fluent-extensions@0.117.0 - -## 0.143.6 - -### Patch Changes - -- Updated dependencies [cf3080b] - - @effect-app/fluent-extensions@0.116.6 - -## 0.143.5 - -### Patch Changes - -- Updated dependencies [e279f74] - - @effect-app/fluent-extensions@0.116.5 - -## 0.143.4 - -### Patch Changes - -- Updated dependencies [fb25eb5] - - @effect-app/fluent-extensions@0.116.4 - -## 0.143.3 - -### Patch Changes - -- Updated dependencies [34b8449] - - @effect-app/fluent-extensions@0.116.3 - -## 0.143.2 - -### Patch Changes - -- Updated dependencies [67d4ec9] - - @effect-app/fluent-extensions@0.116.2 - -## 0.143.1 - -### Patch Changes - -- Updated dependencies [bc98aeb] - - @effect-app/schema@0.177.5 - - @effect-app/fluent-extensions@0.116.1 - -## 0.143.0 - -### Minor Changes - -- e6c8452: remove global Either - -### Patch Changes - -- Updated dependencies [e6c8452] - - @effect-app/fluent-extensions@0.116.0 - - @effect-app/core@0.121.0 - - @effect-app/schema@0.177.4 - -## 0.142.3 - -### Patch Changes - -- Updated dependencies [acbbe35] - - @effect-app/schema@0.177.3 - - @effect-app/fluent-extensions@0.115.2 - -## 0.142.2 - -### Patch Changes - -- Updated dependencies [39cc9b0] - - @effect-app/fluent-extensions@0.115.1 - -## 0.142.1 - -### Patch Changes - -- 78de0f5: fix - -## 0.142.0 - -### Minor Changes - -- b51e7b3: ext - -### Patch Changes - -- Updated dependencies [b51e7b3] - - @effect-app/fluent-extensions@0.115.0 - -## 0.141.3 - -### Patch Changes - -- Updated dependencies [6806433] - - @effect-app/fluent-extensions@0.114.3 - -## 0.141.2 - -### Patch Changes - -- Updated dependencies [f28cec3] - - @effect-app/fluent-extensions@0.114.2 - -## 0.141.1 - -### Patch Changes - -- Updated dependencies [6de437e] - - @effect-app/fluent-extensions@0.114.1 - -## 0.141.0 - -### Minor Changes - -- 72e0db6: package updates - -### Patch Changes - -- Updated dependencies [72e0db6] - - @effect-app/fluent-extensions@0.114.0 - - @effect-app/core@0.120.0 - - @effect-app/schema@0.177.2 - -## 0.140.8 - -### Patch Changes - -- Updated dependencies [e66dbfc] - - @effect-app/fluent-extensions@0.113.13 - -## 0.140.7 - -### Patch Changes - -- Updated dependencies [6677889] - - @effect-app/fluent-extensions@0.113.12 - - @effect-app/schema@0.177.1 - -## 0.140.6 - -### Patch Changes - -- Updated dependencies [78982c7] - - @effect-app/fluent-extensions@0.113.11 - -## 0.140.5 - -### Patch Changes - -- Updated dependencies [928dfc8] - - @effect-app/fluent-extensions@0.113.10 - -## 0.140.4 - -### Patch Changes - -- Updated dependencies [2ab1f78] - - @effect-app/fluent-extensions@0.113.9 - -## 0.140.3 - -### Patch Changes - -- Updated dependencies [287c4e6] - - @effect-app/schema@0.177.0 - - @effect-app/fluent-extensions@0.113.8 - -## 0.140.2 - -### Patch Changes - -- Updated dependencies [bff7d58] - - @effect-app/schema@0.176.2 - - @effect-app/fluent-extensions@0.113.7 - -## 0.140.1 - -### Patch Changes - -- Updated dependencies [f7da591] - - @effect-app/schema@0.176.1 - - @effect-app/fluent-extensions@0.113.6 - -## 0.140.0 - -### Minor Changes - -- 76c89b8: cleanup exts - -### Patch Changes - -- Updated dependencies [76c89b8] -- Updated dependencies [76c89b8] - - @effect-app/schema@0.176.0 - - @effect-app/core@0.119.0 - - @effect-app/fluent-extensions@0.113.5 - -## 0.139.0 - -### Minor Changes - -- 2ccca0f: update exts - -### Patch Changes - -- Updated dependencies [ad46a20] -- Updated dependencies [40d51e4] -- Updated dependencies [2ccca0f] - - @effect-app/schema@0.175.0 - - @effect-app/fluent-extensions@0.113.4 - -## 0.138.0 - -### Minor Changes - -- b487764: improve extensions - -### Patch Changes - -- Updated dependencies [b487764] - - @effect-app/fluent-extensions@0.113.0 - -## 0.137.0 - -### Minor Changes - -- 465afb5: fix: Schema functions should use ParseError variants - -### Patch Changes - -- Updated dependencies [465afb5] - - @effect-app/core@0.118.0 - - @effect-app/schema@0.174.0 - -## 0.136.0 - -### Minor Changes - -- f01680a: update packages -- 349d358: improve support for schemas with R. - -### Patch Changes - -- ebcd0dc: update packages -- Updated dependencies [ebcd0dc] -- Updated dependencies [f01680a] -- Updated dependencies [349d358] - - @effect-app/schema@0.173.0 - -## 0.135.0 - -### Minor Changes - -- 744e5fc: disable auto constructor call - -### Patch Changes - -- 0a898f8: update packages -- Updated dependencies [0a898f8] - - @effect-app/schema@0.172.1 - -## 0.134.0 - -### Minor Changes - -- 449b343: update packages - -### Patch Changes - -- Updated dependencies [449b343] -- Updated dependencies [d103a32] - - @effect-app/schema@0.172.0 - -## 0.133.0 - -### Minor Changes - -- 2fd03d3: update packages - -### Patch Changes - -- Updated dependencies [2fd03d3] - - @effect-app/schema@0.171.0 - - @effect-app/core@0.117.0 - -## 0.132.1 - -### Patch Changes - -- c80a6bb: fix type - -## 0.132.0 - -### Minor Changes - -- 99f79ab: update packages -- cd25b5f: improve: typed error handling - -### Patch Changes - -- Updated dependencies [99f79ab] - - @effect-app/schema@0.170.0 - - @effect-app/core@0.116.0 - -## 0.131.0 - -### Minor Changes - -- 352d57c: cleanup query/mutations. - -## 0.130.5 - -### Patch Changes - -- Updated dependencies [17dbbff] - - @effect-app/schema@0.169.3 - -## 0.130.4 - -### Patch Changes - -- 578e25c: pick POST for query/delete when not all members assignable to string -- Updated dependencies [578e25c] - - @effect-app/schema@0.169.2 - -## 0.130.3 - -### Patch Changes - -- fix - -## 0.130.2 - -### Patch Changes - -- f5d1644: fix - -## 0.130.1 - -### Patch Changes - -- Updated dependencies [cb62aaa] - - @effect-app/schema@0.169.1 - -## 0.130.0 - -### Minor Changes - -- 1832f49: add auto method support - -### Patch Changes - -- Updated dependencies [1832f49] - - @effect-app/schema@0.169.0 - -## 0.129.0 - -### Minor Changes - -- default paths to / - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.168.0 - -## 0.128.2 - -### Patch Changes - -- 7db2745: meta - -## 0.128.1 - -### Patch Changes - -- f725d15: fix js - -## 0.128.0 - -### Minor Changes - -- 5786fb6: prefix path with request name - -## 0.127.2 - -### Patch Changes - -- Updated dependencies [690624e] - - @effect-app/schema@0.167.0 - -## 0.127.1 - -### Patch Changes - -- 9780ef0: fup - -## 0.127.0 - -### Minor Changes - -- a30db88: update packages -- b454cd7: embrace Matcher - -### Patch Changes - -- Updated dependencies [a30db88] -- Updated dependencies [b454cd7] - - @effect-app/schema@0.166.0 - - @effect-app/core@0.115.0 - -## 0.126.0 - -### Minor Changes - -- ed813b4: update packages - -### Patch Changes - -- Updated dependencies [ed813b4] - - @effect-app/schema@0.165.0 - -## 0.125.0 - -### Minor Changes - -- 3eb4057: update packages - -### Patch Changes - -- Updated dependencies [3eb4057] - - @effect-app/schema@0.164.0 - - @effect-app/core@0.114.0 - -## 0.124.0 - -### Minor Changes - -- 051bad7: override reasonable Email/PhoneNumber - -### Patch Changes - -- Updated dependencies [051bad7] - - @effect-app/core@0.113.0 - - @effect-app/schema@0.163.1 - -## 0.123.0 - -### Minor Changes - -- 4c915ea: update packages - -### Patch Changes - -- Updated dependencies [4c915ea] - - @effect-app/schema@0.163.0 - - @effect-app/core@0.112.0 - -## 0.122.1 - -### Patch Changes - -- Updated dependencies [e356144] - - @effect-app/schema@0.162.1 - -## 0.122.0 - -### Minor Changes - -- fix exts - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.111.0 - - @effect-app/schema@0.162.0 - -## 0.121.0 - -### Minor Changes - -- e2b5df8: revert workaround - -### Patch Changes - -- Updated dependencies [e2b5df8] - - @effect-app/schema@0.161.0 - - @effect-app/core@0.110.0 - -## 0.120.0 - -### Minor Changes - -- efa846c: improve: class workaround - -### Patch Changes - -- Updated dependencies [efa846c] - - @effect-app/schema@0.160.0 - - @effect-app/core@0.109.0 - -## 0.119.2 - -### Patch Changes - -- d3bc95b: fix lazy arb - -## 0.119.1 - -### Patch Changes - -- Updated dependencies [3936865] - - @effect-app/schema@0.159.1 - -## 0.119.0 - -### Minor Changes - -- 4aaa360: update packages -- a64728d: update to latest Schema - -### Patch Changes - -- Updated dependencies [4aaa360] -- Updated dependencies [a64728d] - - @effect-app/schema@0.159.0 - - @effect-app/core@0.108.0 - -## 0.118.0 - -### Minor Changes - -- 3ee1505: refactor: remove \_\_call extensions - -### Patch Changes - -- Updated dependencies [3ee1505] - - @effect-app/core@0.107.0 - - @effect-app/schema@0.158.1 - -## 0.117.0 - -### Minor Changes - -- 33dcf2e: update packages - -### Patch Changes - -- Updated dependencies [33dcf2e] - - @effect-app/schema@0.158.0 - - @effect-app/core@0.106.0 - -## 0.116.1 - -### Patch Changes - -- fd233aa: package updates -- Updated dependencies [5fe664e] -- Updated dependencies [afa98cf] - - @effect-app/core@0.105.0 - - @effect-app/schema@0.157.1 - -## 0.116.0 - -### Minor Changes - -- e535d05: update packages - -### Patch Changes - -- Updated dependencies [e535d05] - - @effect-app/schema@0.157.0 - - @effect-app/core@0.104.0 - -## 0.115.0 - -### Minor Changes - -- 9049ccd: filter path params from qs - -### Patch Changes - -- Updated dependencies [9049ccd] - - @effect-app/core@0.103.0 - - @effect-app/schema@0.156.1 - -## 0.114.0 - -### Minor Changes - -- a894a63: update packages - -### Patch Changes - -- Updated dependencies [a894a63] - - @effect-app/schema@0.156.0 - - @effect-app/core@0.102.0 - -## 0.113.0 - -### Minor Changes - -- 4e2e01b: roll into one - -## 0.112.1 - -### Patch Changes - -- 59077e8: feat: add `TagClassMakeScoped` - -## 0.112.0 - -### Minor Changes - -- 7945f38: update packages - -### Patch Changes - -- bc5f735: add `TagClassMake` -- Updated dependencies [7945f38] - - @effect-app/schema@0.155.0 - - @effect-app/core@0.101.0 - -## 0.111.0 - -### Minor Changes - -- 067274a: Update packages - -### Patch Changes - -- Updated dependencies [067274a] - - @effect-app/schema@0.154.0 - -## 0.110.0 - -### Minor Changes - -- 63425a7: update packages - -### Patch Changes - -- Updated dependencies [63425a7] - - @effect-app/schema@0.153.0 - - @effect-app/core@0.100.0 - -## 0.109.3 - -### Patch Changes - -- fix Positive->NonNegative - -## 0.109.2 - -### Patch Changes - -- 4464df4: missing ext - -## 0.109.1 - -### Patch Changes - -- Updated dependencies [fd85bed] - - @effect-app/schema@0.152.1 - -## 0.109.0 - -### Minor Changes - -- 82f0f0d: update packages - -### Patch Changes - -- Updated dependencies [82f0f0d] - - @effect-app/schema@0.152.0 - - @effect-app/core@0.99.0 - -## 0.108.3 - -### Patch Changes - -- Updated dependencies [153a19f] - - @effect-app/core@0.98.1 - - @effect-app/schema@0.151.2 - -## 0.108.2 - -### Patch Changes - -- c8e7d1d: improve: stack error helper on assign.tag - -## 0.108.1 - -### Patch Changes - -- 26e8e9f: improve: use meta info for request name -- 0f8297a: feat: add class annotate decorator -- Updated dependencies [0f8297a] - - @effect-app/schema@0.151.1 - -## 0.108.0 - -### Minor Changes - -- 2ea1e3c: update packages - -### Patch Changes - -- Updated dependencies [2ea1e3c] - - @effect-app/schema@0.151.0 - - @effect-app/core@0.98.0 - -## 0.107.11 - -### Patch Changes - -- Updated dependencies [cf69a10] - - @effect-app/schema@0.150.10 - -## 0.107.10 - -### Patch Changes - -- 5564b85: update packages -- Updated dependencies [5564b85] - - @effect-app/schema@0.150.9 - - @effect-app/core@0.97.3 - -## 0.107.9 - -### Patch Changes - -- f8b45b9: export numbers -- Updated dependencies [f8b45b9] - - @effect-app/schema@0.150.8 - -## 0.107.8 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.150.7 - -## 0.107.7 - -### Patch Changes - -- Updated dependencies [a72f796] - - @effect-app/schema@0.150.6 - -## 0.107.6 - -### Patch Changes - -- c615f35: update packages -- Updated dependencies [c615f35] - - @effect-app/core@0.97.2 - - @effect-app/schema@0.150.5 - -## 0.107.5 - -### Patch Changes - -- Updated dependencies [d9034fe] - - @effect-app/schema@0.150.4 - -## 0.107.4 - -### Patch Changes - -- 38f3367: fix unsafe - -## 0.107.3 - -### Patch Changes - -- Updated dependencies [6a5591a] - - @effect-app/schema@0.150.3 - -## 0.107.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.97.1 - - @effect-app/schema@0.150.2 - -## 0.107.1 - -### Patch Changes - -- fd520f7: chore: restore UnionBrand -- Updated dependencies [fd520f7] - - @effect-app/schema@0.150.1 - -## 0.107.0 - -### Minor Changes - -- 640d24a: reference schema2 -- 640d24a: update packages - -### Patch Changes - -- 640d24a: mm -- 640d24a: clean -- 640d24a: cleanup -- 640d24a: f -- 640d24a: update -- 640d24a: faker -- 640d24a: cleanup schema ext -- 640d24a: fix stringid and clientfor -- 640d24a: updates -- 640d24a: S -- 640d24a: fix things -- 640d24a: improve -- 640d24a: update schema -- 640d24a: upd -- 640d24a: cl -- 640d24a: form and client fixes -- 640d24a: update packages -- 640d24a: ext -- 640d24a: improve rc -- 640d24a: fix RequestId -- 640d24a: dynamic faker arb -- 640d24a: fix empty body -- 640d24a: fix mapPath should use From -- 640d24a: exports -- 640d24a: use schema fork -- 640d24a: cleanup errors and use decodeSyc over parseSync -- 640d24a: update packages -- 640d24a: expose ParseResult -- 640d24a: update packages -- 640d24a: fup -- 640d24a: upd -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] -- Updated dependencies [640d24a] - - @effect-app/schema@0.150.0 - - @effect-app/core@0.97.0 - -## 0.107.0-next.45 - -### Patch Changes - -- d2bc3ac: update schema -- Updated dependencies [d2bc3ac] - - @effect-app/schema@0.150.0-next.30 - -## 0.107.0-next.44 - -### Patch Changes - -- update packages -- Updated dependencies - - @effect-app/schema@0.150.0-next.29 - - @effect-app/core@0.97.0-next.8 - -## 0.107.0-next.43 - -### Patch Changes - -- 19dc3b1: ext -- Updated dependencies [19dc3b1] - - @effect-app/schema@0.150.0-next.28 - -## 0.107.0-next.42 - -### Patch Changes - -- 09ed839: dynamic faker arb - -## 0.107.0-next.41 - -### Patch Changes - -- b6d4272: update packages -- Updated dependencies [0bc4207] -- Updated dependencies [b6d4272] - - @effect-app/schema@0.150.0-next.27 - - @effect-app/core@0.97.0-next.7 - -## 0.107.0-next.40 - -### Patch Changes - -- 14b33a9: cleanup errors and use decodeSyc over parseSync -- Updated dependencies [14b33a9] - - @effect-app/schema@0.150.0-next.26 - -## 0.107.0-next.39 - -### Patch Changes - -- Updated dependencies [aa1b654] - - @effect-app/schema@0.150.0-next.25 - -## 0.107.0-next.38 - -### Patch Changes - -- 64793a8: fix mapPath should use From - -## 0.107.0-next.37 - -### Patch Changes - -- 9e151ed: update packages -- Updated dependencies [9e151ed] - - @effect-app/schema@0.150.0-next.24 - - @effect-app/core@0.97.0-next.6 - -## 0.107.0-next.36 - -### Patch Changes - -- Updated dependencies [6b89c27] - - @effect-app/core@0.97.0-next.5 - - @effect-app/schema@0.150.0-next.23 - -## 0.107.0-next.35 - -### Patch Changes - -- 169c159: expose ParseResult -- Updated dependencies [169c159] - - @effect-app/schema@0.150.0-next.22 - -## 0.107.0-next.34 - -### Patch Changes - -- b46e30d: fup - -## 0.107.0-next.33 - -### Patch Changes - -- Updated dependencies [c57bb66] - - @effect-app/schema@0.150.0-next.21 - -## 0.107.0-next.32 - -### Patch Changes - -- 735dea9: improve rc - -## 0.107.0-next.31 - -### Patch Changes - -- 21482c6: cl - -## 0.107.0-next.30 - -### Patch Changes - -- eab24be: upd - -## 0.107.0-next.29 - -### Patch Changes - -- def06a7: fix things - -## 0.107.0-next.28 - -### Patch Changes - -- eb31797: fix empty body - -## 0.107.0-next.27 - -### Patch Changes - -- 65696a6: fix RequestId - -## 0.107.0-next.26 - -### Patch Changes - -- 777162f: improve -- Updated dependencies [777162f] - - @effect-app/schema@0.150.0-next.20 - -## 0.107.0-next.25 - -### Patch Changes - -- 0776693: fix stringid and clientfor - -## 0.107.0-next.24 - -### Patch Changes - -- 68eaf52: form and client fixes - -## 0.107.0-next.23 - -### Patch Changes - -- Updated dependencies [ca927a5] - - @effect-app/schema@0.150.0-next.19 - -## 0.107.0-next.22 - -### Patch Changes - -- 5f62f5a: upd -- Updated dependencies [5f62f5a] - - @effect-app/schema@0.150.0-next.18 - -## 0.107.0-next.21 - -### Patch Changes - -- Updated dependencies [53d4b3e] - - @effect-app/schema@0.150.0-next.17 - -## 0.107.0-next.20 - -### Patch Changes - -- Updated dependencies [63d4549] - - @effect-app/schema@0.150.0-next.16 - -## 0.107.0-next.19 - -### Patch Changes - -- b19ce70: S - -## 0.107.0-next.18 - -### Patch Changes - -- 92ff6b9: exports -- Updated dependencies [92ff6b9] - - @effect-app/schema@0.150.0-next.15 - -## 0.107.0-next.17 - -### Patch Changes - -- Updated dependencies [c234016] - - @effect-app/schema@0.150.0-next.14 - -## 0.107.0-next.16 - -### Patch Changes - -- Updated dependencies [5d53b26] - - @effect-app/schema@0.150.0-next.13 - -## 0.107.0-next.15 - -### Patch Changes - -- Updated dependencies [147bc14] - - @effect-app/schema@0.150.0-next.12 - -## 0.107.0-next.14 - -### Patch Changes - -- Updated dependencies [660d57a] - - @effect-app/schema@0.150.0-next.11 - -## 0.107.0-next.13 - -### Patch Changes - -- 20076e0: update -- Updated dependencies [20076e0] - - @effect-app/schema@0.150.0-next.10 - -## 0.107.0-next.12 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.97.0-next.4 - - @effect-app/schema@0.150.0-next.9 - -## 0.107.0-next.11 - -### Patch Changes - -- ac35466: use schema fork -- Updated dependencies [ac35466] - - @effect-app/schema@0.150.0-next.8 - -## 0.107.0-next.10 - -### Patch Changes - -- Updated dependencies [4f8ade3] - - @effect-app/core@0.97.0-next.3 - - @effect-app/schema@0.150.0-next.7 - -## 0.107.0-next.9 - -### Patch Changes - -- Updated dependencies [8897586] - - @effect-app/schema@0.150.0-next.6 - -## 0.107.0-next.8 - -### Patch Changes - -- c550ee5: updates -- Updated dependencies [c550ee5] - - @effect-app/schema@0.150.0-next.5 - - @effect-app/core@0.97.0-next.2 - -## 0.107.0-next.7 - -### Patch Changes - -- 82b3ca0: mm - -## 0.107.0-next.6 - -### Patch Changes - -- Updated dependencies [ace7afb] - - @effect-app/schema@0.150.0-next.4 - -## 0.107.0-next.5 - -### Patch Changes - -- 3320864: cleanup schema ext -- Updated dependencies [3320864] - - @effect-app/schema@0.150.0-next.3 - -## 0.107.0-next.4 - -### Patch Changes - -- Updated dependencies [fde3e90] - - @effect-app/schema@0.150.0-next.2 - - @effect-app/core@0.97.0-next.1 - -## 0.107.0-next.3 - -### Patch Changes - -- 3d9e1ab: clean -- 47d0151: faker - -## 0.107.0-next.2 - -### Patch Changes - -- c3de47e: f - -## 0.107.0-next.1 - -### Patch Changes - -- bd2130b: cleanup -- Updated dependencies [bd2130b] - - @effect-app/schema@0.150.0-next.1 - -## 0.107.0-next.0 - -### Minor Changes - -- 87a6ac2: update packages - -### Patch Changes - -- Updated dependencies [87a6ac2] - - @effect-app/schema2@0.1.0-next.0 - - @effect-app/core@0.97.0-next.0 - -## 0.106.1 - -### Patch Changes - -- efb854a: update packages -- Updated dependencies [efb854a] - - @effect-app/schema@0.90.1 - - @effect-app/core@0.96.1 - -## 0.106.0 - -### Minor Changes - -- 4330242: improve: TagClass - -## 0.105.0 - -### Minor Changes - -- c6efdeb: update packages - -### Patch Changes - -- Updated dependencies [c6efdeb] - - @effect-app/schema@0.90.0 - - @effect-app/core@0.96.0 - -## 0.104.1 - -### Patch Changes - -- cf6ae3c: update packages -- Updated dependencies [cf6ae3c] - - @effect-app/schema@0.89.1 - - @effect-app/core@0.95.1 - -## 0.104.0 - -### Minor Changes - -- 46bd7be: update packages - -### Patch Changes - -- Updated dependencies [46bd7be] - - @effect-app/schema@0.89.0 - - @effect-app/core@0.95.0 - -## 0.103.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.94.2 - - @effect-app/schema@0.88.3 - -## 0.103.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.94.1 - - @effect-app/schema@0.88.2 - -## 0.103.0 - -### Minor Changes - -- 92d7e37: update packages - -### Patch Changes - -- Updated dependencies [92d7e37] -- Updated dependencies - - @effect-app/core@0.94.0 - - @effect-app/schema@0.88.1 - -## 0.102.0 - -### Minor Changes - -- bump - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.93.0 - - @effect-app/schema@0.88.0 - -## 0.101.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.87.10 - -## 0.101.0 - -### Minor Changes - -- 67eb741: update packages - -### Patch Changes - -- Updated dependencies [67eb741] - - @effect-app/core@0.92.0 - - @effect-app/schema@0.87.9 - -## 0.100.1 - -### Patch Changes - -- 7f447d3: update packages -- Updated dependencies [7f447d3] - - @effect-app/core@0.91.1 - - @effect-app/schema@0.87.8 - -## 0.100.0 - -### Minor Changes - -- ad8154a: update packages - -### Patch Changes - -- Updated dependencies [ad8154a] - - @effect-app/core@0.91.0 - - @effect-app/schema@0.87.7 - -## 0.99.1 - -### Patch Changes - -- f33d0a1: feat: change request path to extends from `/${string}` for platform alignment -- Updated dependencies [f33d0a1] - - @effect-app/schema@0.87.6 - -## 0.99.0 - -### Minor Changes - -- f20991e: update packages - -### Patch Changes - -- Updated dependencies [f20991e] - - @effect-app/core@0.90.0 - - @effect-app/schema@0.87.5 - -## 0.98.3 - -### Patch Changes - -- Updated dependencies [687c313] - - @effect-app/schema@0.87.4 - -## 0.98.2 - -### Patch Changes - -- Updated dependencies [fcfea25] - - @effect-app/schema@0.87.3 - -## 0.98.1 - -### Patch Changes - -- Updated dependencies [e468bd0] - - @effect-app/schema@0.87.2 - -## 0.98.0 - -### Minor Changes - -- 98fbdb3: update packages - -### Patch Changes - -- Updated dependencies [98fbdb3] - - @effect-app/core@0.89.0 - - @effect-app/schema@0.87.1 - -## 0.97.0 - -### Minor Changes - -- 8b90f25: feat: add NumberAsInt, PositiveNumberAsInt - -### Patch Changes - -- Updated dependencies [8b90f25] - - @effect-app/schema@0.87.0 - -## 0.96.2 - -### Patch Changes - -- c3c5877: extend refinements with copy and clone - -## 0.96.1 - -### Patch Changes - -- a5b77fc: fix types - -## 0.96.0 - -### Minor Changes - -- 60ecffb: feat: copy/clone trait trial - -### Patch Changes - -- Updated dependencies [60ecffb] - - @effect-app/schema@0.86.0 - -## 0.95.0 - -### Minor Changes - -- fb332b4: update packages - -### Patch Changes - -- Updated dependencies [fb332b4] - - @effect-app/schema@0.85.0 - - @effect-app/core@0.88.0 - -## 0.94.0 - -### Minor Changes - -- 0069d09: Cleanup layers - -## 0.93.0 - -### Minor Changes - -- a681ab6: update packages - -### Patch Changes - -- Updated dependencies [a681ab6] - - @effect-app/schema@0.84.0 - - @effect-app/core@0.87.0 - -## 0.92.0 - -### Minor Changes - -- a9e8b8f: update packages - -### Patch Changes - -- Updated dependencies [169ccb0] -- Updated dependencies [a9e8b8f] - - @effect-app/core@0.86.0 - - @effect-app/schema@0.83.0 - -## 0.91.1 - -### Patch Changes - -- Updated dependencies [3cf98d6] - - @effect-app/schema@0.82.2 - -## 0.91.0 - -### Minor Changes - -- e0b2e24: fix: update for Secret - -### Patch Changes - -- Updated dependencies [e0b2e24] - - @effect-app/core@0.85.0 - - @effect-app/schema@0.82.1 - -## 0.90.0 - -### Minor Changes - -- ac36723: update dependencies - -### Patch Changes - -- Updated dependencies [ac36723] - - @effect-app/schema@0.82.0 - - @effect-app/core@0.84.0 - -## 0.89.0 - -### Minor Changes - -- da80c66: feat: add literalNumber - -### Patch Changes - -- Updated dependencies [da80c66] - - @effect-app/schema@0.81.0 - -## 0.88.9 - -### Patch Changes - -- Updated dependencies [8235a1a] - - @effect-app/core@0.83.3 - - @effect-app/schema@0.80.6 - -## 0.88.8 - -### Patch Changes - -- Updated dependencies [f6e1e15] - - @effect-app/schema@0.80.5 - -## 0.88.7 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.83.2 - - @effect-app/schema@0.80.4 - -## 0.88.6 - -### Patch Changes - -- Updated dependencies [8a81a8f] - - @effect-app/core@0.83.1 - - @effect-app/schema@0.80.3 - -## 0.88.5 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.83.0 - - @effect-app/schema@0.80.2 - -## 0.88.4 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.82.1 - - @effect-app/schema@0.80.1 - -## 0.88.3 - -### Patch Changes - -- Updated dependencies [4012f97] - - @effect-app/schema@0.80.0 - - @effect-app/core@0.82.0 - -## 0.88.2 - -### Patch Changes - -- 7f946cb: remove obsolete helpers -- Updated dependencies [7f946cb] - - @effect-app/core@0.81.1 - - @effect-app/schema@0.79.1 - -## 0.88.1 - -### Patch Changes - -- 65886f2: fix type sigs - -## 0.88.0 - -### Minor Changes - -- 16ffb5e: class service/repo support andThen, drop flatMap - -## 0.87.0 - -### Minor Changes - -- align v - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.81.0 - - @effect-app/schema@0.79.0 - -## 0.86.0 - -### Minor Changes - -- align v - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.80.0 - - @effect-app/schema@0.78.0 - -## 0.85.5 - -### Patch Changes - -- 4627526: improve string name - -## 0.85.4 - -### Patch Changes - -- Updated dependencies [081418b] - - @effect-app/schema@0.77.1 - -## 0.85.3 - -### Patch Changes - -- fix - -## 0.85.2 - -### Patch Changes - -- fix - -## 0.85.1 - -### Patch Changes - -- fix typo - -## 0.85.0 - -### Minor Changes - -- feat: support constructor input for clientFor requests - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.77.0 - -## 0.84.0 - -### Minor Changes - -- improve String schemas - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.76.0 - -## 0.83.0 - -### Minor Changes - -- fa908e8: move fluent-extensions - -## 0.82.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.75.3 - -## 0.82.2 - -### Patch Changes - -- Updated dependencies [086c2b1] - - @effect-app/schema@0.75.2 - -## 0.82.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.75.1 - -## 0.82.0 - -### Minor Changes - -- 68359de: more schema alignment - -### Patch Changes - -- Updated dependencies [68359de] - - @effect-app/schema@0.75.0 - -## 0.81.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.74.2 - -## 0.81.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.74.1 - -## 0.81.0 - -### Minor Changes - -- 771d7e6: Fix model name - -### Patch Changes - -- Updated dependencies [771d7e6] - - @effect-app/schema@0.74.0 - -## 0.80.1 - -### Patch Changes - -- Updated dependencies [af19954] - - @effect-app/core@0.79.1 - - @effect-app/schema@0.73.1 - -## 0.80.0 - -### Minor Changes - -- 1f6b9a1: update packages - -### Patch Changes - -- Updated dependencies [1f6b9a1] - - @effect-app/schema@0.73.0 - - @effect-app/core@0.79.0 - -## 0.79.0 - -### Minor Changes - -- c528c33: initial alignment of Schema with effect/schema - -### Patch Changes - -- Updated dependencies [c528c33] - - @effect-app/schema@0.72.0 - - @effect-app/core@0.78.0 - -## 0.78.2 - -### Patch Changes - -- d4a6be2: fix ext - -## 0.78.1 - -### Patch Changes - -- 4c9e60a: add basic fluent extensions for projects without tsplus - -## 0.78.0 - -### Minor Changes - -- da85f48: cleanup operators. add nonEmpty map variant - -### Patch Changes - -- ab7eddc: update packages -- Updated dependencies [ab7eddc] -- Updated dependencies [da85f48] - - @effect-app/core@0.77.0 - - @effect-app/schema@0.71.0 - -## 0.77.3 - -### Patch Changes - -- Updated dependencies [d462c64] - - @effect-app/schema@0.70.0 - - @effect-app/core@0.76.0 - -## 0.77.2 - -### Patch Changes - -- Updated dependencies [26a3bc8] - - @effect-app/schema@0.69.2 - -## 0.77.1 - -### Patch Changes - -- Updated dependencies [9efeed84] - - @effect-app/core@0.75.1 - - @effect-app/schema@0.69.1 - -## 0.77.0 - -### Minor Changes - -- 88f5695c: revert schema classes double as tag - -### Patch Changes - -- Updated dependencies [88f5695c] - - @effect-app/schema@0.69.0 - - @effect-app/core@0.75.0 - -## 0.76.0 - -### Minor Changes - -- 46719cec: feat: schema classes double as Tag - -### Patch Changes - -- Updated dependencies [46719cec] - - @effect-app/schema@0.68.0 - - @effect-app/core@0.74.0 - -## 0.75.2 - -### Patch Changes - -- Updated dependencies [5813c6b2] - - @effect-app/core@0.73.1 - - @effect-app/schema@0.67.2 - -## 0.75.1 - -### Patch Changes - -- a644e7b5: fix methods exports -- Updated dependencies [a644e7b5] - - @effect-app/schema@0.67.1 - -## 0.75.0 - -### Minor Changes - -- 6c180253: cleanup - -### Patch Changes - -- Updated dependencies [6c180253] - - @effect-app/schema@0.67.0 - - @effect-app/core@0.73.0 - -## 0.74.4 - -### Patch Changes - -- Updated dependencies [f436ce54] - - @effect-app/schema@0.66.0 - -## 0.74.3 - -### Patch Changes - -- 7ff1f6f9: update packages -- 04f6bafa: update packages -- Updated dependencies [7ff1f6f9] -- Updated dependencies [04f6bafa] - - @effect-app/core@0.72.1 - - @effect-app/schema@0.65.3 - -## 0.74.2 - -### Patch Changes - -- Updated dependencies [07c0dbfc] - - @effect-app/schema@0.65.2 - -## 0.74.1 - -### Patch Changes - -- Updated dependencies [a7a9f51f] - - @effect-app/schema@0.65.1 - -## 0.74.0 - -### Minor Changes - -- 0a4bfbc7: update packages - -### Patch Changes - -- Updated dependencies [0a4bfbc7] - - @effect-app/schema@0.65.0 - - @effect-app/core@0.72.0 - -## 0.73.5 - -### Patch Changes - -- 56583d9b: align span names -- Updated dependencies [baf5a2b7] - - @effect-app/core@0.71.5 - - @effect-app/schema@0.64.5 - -## 0.73.4 - -### Patch Changes - -- update packages -- Updated dependencies - - @effect-app/core@0.71.4 - - @effect-app/schema@0.64.4 - -## 0.73.3 - -### Patch Changes - -- Updated dependencies [99205b44] - - @effect-app/core@0.71.3 - - @effect-app/schema@0.64.3 - -## 0.73.2 - -### Patch Changes - -- 002cdb31: update packages -- Updated dependencies [002cdb31] - - @effect-app/core@0.71.2 - - @effect-app/schema@0.64.2 - -## 0.73.1 - -### Patch Changes - -- Updated dependencies [868919c8] - - @effect-app/schema@0.64.1 - - @effect-app/core@0.71.1 - -## 0.73.0 - -### Minor Changes - -- 90af6a41: update packages - -### Patch Changes - -- Updated dependencies [90af6a41] - - @effect-app/schema@0.64.0 - - @effect-app/core@0.71.0 - -## 0.72.9 - -### Patch Changes - -- de419614: enhance errors - -## 0.72.8 - -### Patch Changes - -- ed346253: Improve NotLoggedInError - -## 0.72.7 - -### Patch Changes - -- ff54e102: update packages -- Updated dependencies [ff54e102] - - @effect-app/core@0.70.2 - - @effect-app/schema@0.63.2 - -## 0.72.6 - -### Patch Changes - -- ef626697: improve client span - -## 0.72.5 - -### Patch Changes - -- 60615760: cleanup - -## 0.72.4 - -### Patch Changes - -- fix id - -## 0.72.3 - -### Patch Changes - -- f03b6b48: fix - -## 0.72.2 - -### Patch Changes - -- Updated dependencies [3f0dba21] - - @effect-app/core@0.70.1 - - @effect-app/schema@0.63.1 - -## 0.72.1 - -### Patch Changes - -- 8b0e01b4: workaround new Ref variance - -## 0.72.0 - -### Minor Changes - -- 283ebbe7: update packages - -### Patch Changes - -- Updated dependencies [283ebbe7] - - @effect-app/schema@0.63.0 - - @effect-app/core@0.70.0 - -## 0.71.1 - -### Patch Changes - -- d1a6193b: feat: support span across client and api -- Updated dependencies [d1a6193b] - - @effect-app/core@0.69.1 - - @effect-app/schema@0.62.5 - -## 0.71.0 - -### Minor Changes - -- 40a6c3b6: update packages - -### Patch Changes - -- Updated dependencies [40a6c3b6] - - @effect-app/core@0.69.0 - - @effect-app/schema@0.62.4 - -## 0.70.4 - -### Patch Changes - -- housekeeping: update packages -- Updated dependencies - - @effect-app/core@0.68.3 - - @effect-app/schema@0.62.3 - -## 0.70.3 - -### Patch Changes - -- e4185126: unification workaround - -## 0.70.2 - -### Patch Changes - -- 263a56b9: update packages -- Updated dependencies [263a56b9] - - @effect-app/core@0.68.2 - - @effect-app/schema@0.62.2 - -## 0.70.1 - -### Patch Changes - -- aa3c98d1: update packages -- Updated dependencies [aa3c98d1] - - @effect-app/schema@0.62.1 - - @effect-app/core@0.68.1 - -## 0.70.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.68.0 - - @effect-app/schema@0.62.0 - -## 0.69.10 - -### Patch Changes - -- Updated dependencies [9c347b64] - - @effect-app/schema@0.61.1 - -## 0.69.9 - -### Patch Changes - -- Updated dependencies [fdd19b0b] - - @effect-app/schema@0.61.0 - -## 0.69.8 - -### Patch Changes - -- d7dd36d4: update packages -- Updated dependencies [d7dd36d4] - - @effect-app/schema@0.60.5 - - @effect-app/core@0.67.5 - -## 0.69.7 - -### Patch Changes - -- 780a6718: update packages -- Updated dependencies [780a6718] - - @effect-app/core@0.67.4 - - @effect-app/schema@0.60.4 - -## 0.69.6 - -### Patch Changes - -- 57f506e7: feat: add demandJson for HttpClient - -## 0.69.5 - -### Patch Changes - -- 8c0b8270: improve json parse error - -## 0.69.4 - -### Patch Changes - -- 84e54d54: Update packages -- Updated dependencies [84e54d54] - - @effect-app/core@0.67.3 - - @effect-app/schema@0.60.3 - -## 0.69.3 - -### Patch Changes - -- 696a525e: update packages -- Updated dependencies [696a525e] - - @effect-app/schema@0.60.2 - - @effect-app/core@0.67.2 - -## 0.69.2 - -### Patch Changes - -- f8cb0efd: schemaJsonBody - -## 0.69.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.67.1 - - @effect-app/schema@0.60.1 - -## 0.69.0 - -### Minor Changes - -- housekeeping: update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.60.0 - - @effect-app/core@0.67.0 - -## 0.68.3 - -### Patch Changes - -- blurg - -## 0.68.2 - -### Patch Changes - -- 7118bf61: fix error type - -## 0.68.1 - -### Patch Changes - -- Updated dependencies [56a4418f] - - @effect-app/schema@0.59.0 - -## 0.68.0 - -### Minor Changes - -- 271e2c7a: yieldable errors - -### Patch Changes - -- Updated dependencies [271e2c7a] - - @effect-app/schema@0.58.0 - - @effect-app/core@0.66.0 - -## 0.67.10 - -### Patch Changes - -- Updated dependencies [89bae16e] - - @effect-app/core@0.65.5 - - @effect-app/schema@0.57.10 - -## 0.67.9 - -### Patch Changes - -- 838eeb10: update packages -- Updated dependencies [838eeb10] - - @effect-app/schema@0.57.9 - -## 0.67.8 - -### Patch Changes - -- Updated dependencies [0b279a80] - - @effect-app/core@0.65.4 - - @effect-app/schema@0.57.8 - -## 0.67.7 - -### Patch Changes - -- 50c7fe31: fix typo - -## 0.67.6 - -### Patch Changes - -- 7fc3cfbe: improve client api - -## 0.67.5 - -### Patch Changes - -- 26087368: update platform extensions -- Updated dependencies [26087368] - - @effect-app/core@0.65.3 - - @effect-app/schema@0.57.7 - -## 0.67.4 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.65.2 - - @effect-app/schema@0.57.6 - -## 0.67.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.65.1 - - @effect-app/schema@0.57.5 - -## 0.67.2 - -### Patch Changes - -- fix HttpClient global - -## 0.67.1 - -### Patch Changes - -- 924817d9: fix type - -## 0.67.0 - -### Minor Changes - -- 51b546c5: feat: Convert clientFor to use @effect/platform HttpClient - -### Patch Changes - -- Updated dependencies [51b546c5] - - @effect-app/core@0.65.0 - - @effect-app/schema@0.57.4 - -## 0.66.0 - -### Minor Changes - -- 5f3ae060: feat: add HttpClient from @effect/platform - -### Patch Changes - -- Updated dependencies [5f3ae060] - - @effect-app/core@0.64.0 - - @effect-app/schema@0.57.3 - -## 0.65.2 - -### Patch Changes - -- 87e29b7d: export httpclient -- Updated dependencies [87e29b7d] - - @effect-app/core@0.63.2 - - @effect-app/schema@0.57.2 - -## 0.65.1 - -### Patch Changes - -- fix tsplus.config for pnpm -- Updated dependencies - - @effect-app/core@0.63.1 - - @effect-app/schema@0.57.1 - -## 0.65.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.57.0 - - @effect-app/core@0.63.0 - -## 0.64.0 - -### Minor Changes - -- update packages, improve error reporting - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.62.0 - - @effect-app/schema@0.56.0 - -## 0.63.1 - -### Patch Changes - -- add date extensions - -## 0.63.0 - -### Minor Changes - -- add match and update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.55.0 - - @effect-app/core@0.61.0 - -## 0.62.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.54.0 - - @effect-app/core@0.60.0 - -## 0.61.0 - -### Minor Changes - -- housekeeping: update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.53.0 - - @effect-app/core@0.59.0 - -## 0.60.4 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.58.4 - - @effect-app/schema@0.52.4 - -## 0.60.3 - -### Patch Changes - -- Updated dependencies [6b002041] -- Updated dependencies - - @effect-app/core@0.58.3 - - @effect-app/schema@0.52.3 - -## 0.60.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.58.2 - - @effect-app/schema@0.52.2 - -## 0.60.1 - -### Patch Changes - -- improve tsc parsing performance 3x -- Updated dependencies - - @effect-app/core@0.58.1 - - @effect-app/schema@0.52.1 - -## 0.60.0 - -### Minor Changes - -- 4d2e68df: Switch to effect meta package - -### Patch Changes - -- Updated dependencies [4d2e68df] - - @effect-app/schema@0.52.0 - - @effect-app/core@0.58.0 - -## 0.59.7 - -### Patch Changes - -- fix tagclass type - -## 0.59.6 - -### Patch Changes - -- fix tagclass - -## 0.59.5 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.57.5 - - @effect-app/schema@0.51.5 - -## 0.59.4 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.57.4 - - @effect-app/schema@0.51.4 - -## 0.59.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.57.3 - - @effect-app/schema@0.51.3 - -## 0.59.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.57.2 - - @effect-app/schema@0.51.2 - -## 0.59.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.57.1 - - @effect-app/schema@0.51.1 - -## 0.59.0 - -### Minor Changes - -- c80b9e7a: Updated to latest Effect - -### Patch Changes - -- Updated dependencies [c80b9e7a] - - @effect-app/schema@0.51.0 - - @effect-app/core@0.57.0 - -## 0.58.2 - -### Patch Changes - -- fix type of E variants of clientFor - -## 0.58.1 - -### Patch Changes - -- update packages -- Updated dependencies - - @effect-app/core@0.56.1 - - @effect-app/schema@0.50.1 - -## 0.58.0 - -### Minor Changes - -- housekeeping: update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.50.0 - - @effect-app/core@0.56.0 - -## 0.57.0 - -### Minor Changes - -- aa9c3126: using the new faker-js package instead of the old cursed one - -## 0.56.1 - -### Patch Changes - -- e4609777: Use distributive omit - -## 0.56.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.55.0 - - @effect-app/schema@0.49.0 - -## 0.55.2 - -### Patch Changes - -- add pick and omit object extensions - -## 0.55.1 - -### Patch Changes - -- Update packages -- Updated dependencies - - @effect-app/schema@0.48.1 - - @effect-app/core@0.54.1 - -## 0.55.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.54.0 - - @effect-app/schema@0.48.0 - -## 0.54.0 - -### Minor Changes - -- Cleanup legacy cache - -## 0.53.2 - -### Patch Changes - -- EffectCache - -## 0.53.1 - -### Patch Changes - -- Add exports -- Updated dependencies - - @effect-app/core@0.53.1 - - @effect-app/schema@0.47.1 - -## 0.53.0 - -### Minor Changes - -- Update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.53.0 - - @effect-app/schema@0.47.0 - -## 0.52.0 - -### Minor Changes - -- Update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.52.0 - - @effect-app/schema@0.46.0 - -## 0.51.0 - -### Minor Changes - -- Update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.45.0 - - @effect-app/core@0.51.0 - -## 0.50.1 - -### Patch Changes - -- c78a54ff: Enhance TagClass - -## 0.50.0 - -### Minor Changes - -- update ids - -## 0.49.1 - -### Patch Changes - -- Add brandedStringId - -## 0.49.0 - -### Minor Changes - -- a - -### Patch Changes - -- Updated dependencies -- Updated dependencies - - @effect-app/schema@0.44.0 - - @effect-app/core@0.50.0 - -## 0.48.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.49.1 - - @effect-app/schema@0.43.3 - -## 0.48.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.43.2 - -## 0.48.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.43.1 - -## 0.48.0 - -### Minor Changes - -- Compiler test - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.49.0 - - @effect-app/schema@0.43.0 - -## 0.47.0 - -### Minor Changes - -- Update depdendencies - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.42.0 - - @effect-app/core@0.48.0 - -## 0.46.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.41.2 - -## 0.46.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.41.1 - -## 0.46.0 - -### Minor Changes - -- Cleanup schema ext location - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.41.0 - -## 0.45.0 - -### Minor Changes - -- Improve codegen - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.40.0 - -## 0.44.12 - -### Patch Changes - -- fix rest -- Updated dependencies - - @effect-app/schema@0.39.12 - -## 0.44.11 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.39.11 - -## 0.44.10 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.39.10 - -## 0.44.9 - -### Patch Changes - -- feat: property extensions -- Updated dependencies - - @effect-app/schema@0.39.9 - -## 0.44.8 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.39.8 - -## 0.44.7 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.39.7 - -## 0.44.6 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.39.6 - -## 0.44.5 - -### Patch Changes - -- fix -- Updated dependencies - - @effect-app/schema@0.39.5 - -## 0.44.4 - -### Patch Changes - -- fix global -- Updated dependencies - - @effect-app/core@0.47.1 - - @effect-app/schema@0.39.4 - -## 0.44.3 - -### Patch Changes - -- Improve schema prop api -- Updated dependencies - - @effect-app/schema@0.39.3 - -## 0.44.2 - -### Patch Changes - -- Improve withDefadult -- Updated dependencies - - @effect-app/schema@0.39.2 - -## 0.44.1 - -### Patch Changes - -- fix withDefault -- Updated dependencies - - @effect-app/schema@0.39.1 - -## 0.44.0 - -### Minor Changes - -- update dependencies - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.39.0 - - @effect-app/core@0.47.0 - -## 0.43.0 - -### Minor Changes - -- Improve Schema withDefault - -## 0.42.0 - -### Minor Changes - -- update dependencies - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.46.0 - - @effect-app/schema@0.38.0 - -## 0.41.0 - -### Minor Changes - -- Update dependencies - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.37.0 - - @effect-app/core@0.45.0 - -## 0.40.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.44.0 - - @effect-app/schema@0.36.1 - -## 0.40.0 - -### Minor Changes - -- support optional constructors for schema with none or only optional input. - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.36.0 - -## 0.39.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.35.0 - - @effect-app/core@0.43.0 - -## 0.38.0 - -### Minor Changes - -- update packages and cleanup - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.34.0 - - @effect-app/core@0.42.0 - -## 0.37.0 - -### Minor Changes - -- Update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.33.0 - - @effect-app/core@0.41.0 - -## 0.36.1 - -### Patch Changes - -- fix error channel of allLowerFirst - -## 0.36.0 - -### Minor Changes - -- AllLowerFirst should be Par - -## 0.35.0 - -### Minor Changes - -- update dependencies - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.40.0 - - @effect-app/schema@0.32.0 - -## 0.34.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.39.1 - - @effect-app/schema@0.31.1 - -## 0.34.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies -- Updated dependencies [ba4849c9] - - @effect-app/schema@0.31.0 - - @effect-app/core@0.39.0 - -## 0.33.4 - -### Patch Changes - -- Update packages -- Updated dependencies - - @effect-app/schema@0.30.2 - - @effect-app/core@0.38.1 - -## 0.33.3 - -### Patch Changes - -- fup - -## 0.33.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.30.1 - -## 0.33.1 - -### Patch Changes - -- fup - -## 0.33.0 - -### Minor Changes - -- improve min/max/range for numbers. - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.30.0 - -## 0.32.0 - -### Minor Changes - -- Properly identify number schemas - -## 0.31.0 - -### Minor Changes - -- Update packages -- feat(form): number vs string - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.29.0 - - @effect-app/core@0.38.0 - -## 0.30.2 - -### Patch Changes - -- FUP - -## 0.30.1 - -### Patch Changes - -- rename: servicesOrEffects to allLowerFirst - -## 0.30.0 - -### Minor Changes - -- Update to latest effect - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.28.0 - - @effect-app/core@0.37.0 - -## 0.29.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.36.1 - - @effect-app/schema@0.27.1 - -## 0.29.1 - -### Patch Changes - -- Extend Services extensions - -## 0.29.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.27.0 - - @effect-app/core@0.36.0 - -## 0.28.0 - -### Minor Changes - -- Update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.26.0 - - @effect-app/core@0.35.0 - -## 0.27.1 - -### Patch Changes - -- fix dep -- Updated dependencies - - @effect-app/schema@0.25.1 - -## 0.27.0 - -### Minor Changes - -- update packages - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.25.0 - - @effect-app/core@0.34.0 - -## 0.26.2 - -### Patch Changes - -- fix clientFor E - -## 0.26.1 - -### Patch Changes - -- fix - -## 0.26.0 - -### Minor Changes - -- Update packages, add support for class features to schema and optic - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.33.0 - - @effect-app/schema@0.24.0 - -## 0.25.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.32.3 - - @effect-app/schema@0.23.3 - -## 0.25.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.32.2 - - @effect-app/schema@0.23.2 - -## 0.25.1 - -### Patch Changes - -- Cleanup - -## 0.25.0 - -### Minor Changes - -- Improve Encoded codegen - -## 0.24.2 - -### Patch Changes - -- Fix compilation -- Updated dependencies - - @effect-app/core@0.32.1 - - @effect-app/schema@0.23.1 - -## 0.24.1 - -### Patch Changes - -- fix svc tag - -## 0.24.0 - -### Minor Changes - -- Update tsplus with companion instance extensions - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.23.0 - - @effect-app/core@0.32.0 - -## 0.23.7 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.31.0 - - @effect-app/schema@0.22.4 - -## 0.23.6 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.30.3 - - @effect-app/schema@0.22.3 - -## 0.23.5 - -### Patch Changes - -- fix service tag -- Updated dependencies - - @effect-app/core@0.30.2 - - @effect-app/schema@0.22.2 - -## 0.23.4 - -### Patch Changes - -- tag workaround 3 - -## 0.23.3 - -### Patch Changes - -- Workaround tag - -## 0.23.2 - -### Patch Changes - -- tag workaround -- Updated dependencies - - @effect-app/core@0.30.1 - - @effect-app/schema@0.22.1 - -## 0.23.1 - -### Patch Changes - -- Fix service tag - -## 0.23.0 - -### Minor Changes - -- Update Effect - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.30.0 - - @effect-app/schema@0.22.0 - -## 0.22.5 - -### Patch Changes - -- Updated dependencies [00d34a69] - - @effect-app/schema@0.21.0 - -## 0.22.4 - -### Patch Changes - -- Improve curry support for PreparedLens - -## 0.22.3 - -### Patch Changes - -- Fix - -## 0.22.2 - -### Patch Changes - -- Update packages -- Updated dependencies - - @effect-app/schema@0.20.1 - - @effect-app/core@0.29.1 - -## 0.22.1 - -### Patch Changes - -- d89f88cb: add assignTag, TagClass, ServiceTaggedClass - -## 0.22.0 - -### Minor Changes - -- b967055c: Replace fp-ts/core with effect/data - -### Patch Changes - -- Updated dependencies [b967055c] - - @effect-app/schema@0.20.0 - - @effect-app/core@0.29.0 - -## 0.21.14 - -### Patch Changes - -- 28d5530b: Fix Encoded - -## 0.21.13 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.28.0 - - @effect-app/schema@0.19.4 - -## 0.21.12 - -### Patch Changes - -- Updated dependencies [39da5db8] - - @effect-app/core@0.27.0 - - @effect-app/schema@0.19.3 - -## 0.21.11 - -### Patch Changes - -- Updated dependencies [4679321f] - - @effect-app/core@0.26.0 - - @effect-app/schema@0.19.2 - -## 0.21.10 - -### Patch Changes - -- Updated dependencies [d66406c9] - - @effect-app/core@0.25.1 - - @effect-app/schema@0.19.1 - -## 0.21.9 - -### Patch Changes - -- Updated dependencies [b385e928] - - @effect-app/schema@0.19.0 - - @effect-app/core@0.25.0 - -## 0.21.8 - -### Patch Changes - -- Updated dependencies [c4b4a20a] - - @effect-app/core@0.24.1 - - @effect-app/schema@0.18.7 - -## 0.21.7 - -### Patch Changes - -- Updated dependencies [3d49bb4a] - - @effect-app/schema@0.18.6 - -## 0.21.6 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.24.0 - - @effect-app/schema@0.18.5 - -## 0.21.5 - -### Patch Changes - -- Updated dependencies [8da4c8fc] - - @effect-app/core@0.23.1 - - @effect-app/schema@0.18.4 - -## 0.21.4 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.23.0 - - @effect-app/schema@0.18.3 - -## 0.21.3 - -### Patch Changes - -- c29ab9fa: default Effect.servicesWith\* to lowercase first letter services. - -## 0.21.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.22.2 - - @effect-app/schema@0.18.2 - -## 0.21.1 - -### Patch Changes - -- Updated dependencies [df13134f] - - @effect-app/core@0.22.1 - - @effect-app/schema@0.18.1 - -## 0.21.0 - -### Minor Changes - -- Update annotations, drop Aspects for now - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.22.0 - - @effect-app/schema@0.18.0 - -## 0.20.3 - -### Patch Changes - -- Updated dependencies [e3e768a5] - - @effect-app/core@0.21.3 - - @effect-app/schema@0.17.3 - -## 0.20.2 - -### Patch Changes - -- Updated dependencies [d9751183] - - @effect-app/core@0.21.2 - - @effect-app/schema@0.17.2 - -## 0.20.1 - -### Patch Changes - -- Updated dependencies [46d8b7e1] - - @effect-app/core@0.21.1 - - @effect-app/schema@0.17.1 - -## 0.20.0 - -### Minor Changes - -- 338f1f19: Update packages - -### Patch Changes - -- Updated dependencies [338f1f19] - - @effect-app/schema@0.17.0 - - @effect-app/core@0.21.0 - -## 0.19.3 - -### Patch Changes - -- Updated dependencies [00ac8993] - - @effect-app/core@0.20.2 - - @effect-app/schema@0.16.3 - -## 0.19.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.20.1 - - @effect-app/schema@0.16.2 - -## 0.19.1 - -### Patch Changes - -- 02997e8f: FUP -- Updated dependencies [02997e8f] - - @effect-app/schema@0.16.1 - -## 0.19.0 - -### Minor Changes - -- 4d49f303: Upgrade to @effect/data - -### Patch Changes - -- Updated dependencies [4d49f303] - - @effect-app/schema@0.16.0 - - @effect-app/core@0.20.0 - -## 0.18.2 - -### Patch Changes - -- ca8b9a4e: Fix Function flow - -## 0.18.1 - -### Patch Changes - -- Updated dependencies [f8d8e4ec] - - @effect-app/core@0.19.0 - - @effect-app/schema@0.15.6 - -## 0.18.0 - -### Minor Changes - -- Update annotations and leverage ForceLazy on Effect \_\_call - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.18.0 - - @effect-app/schema@0.15.5 - -## 0.17.5 - -### Patch Changes - -- 912ba66c: Update packages -- Updated dependencies [912ba66c] - - @effect-app/schema@0.15.4 - - @effect-app/core@0.17.4 - -## 0.17.4 - -### Patch Changes - -- Updated dependencies [407cc13e] - - @effect-app/core@0.17.3 - - @effect-app/schema@0.15.3 - -## 0.17.3 - -### Patch Changes - -- e9e498ee: Update packages -- Updated dependencies [e9e498ee] - - @effect-app/schema@0.15.2 - - @effect-app/core@0.17.2 - -## 0.17.2 - -### Patch Changes - -- 33d2a60f: Cleanup - -## 0.17.1 - -### Patch Changes - -- Updated dependencies [8b10e403] - - @effect-app/core@0.17.1 - - @effect-app/schema@0.15.1 - -## 0.17.0 - -### Minor Changes - -- 9f9b2d32: Update effect and fp-ts - -### Patch Changes - -- Updated dependencies [9f9b2d32] - - @effect-app/schema@0.15.0 - - @effect-app/core@0.17.0 - -## 0.16.1 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.16.1 - - @effect-app/schema@0.14.1 - -## 0.16.0 - -### Minor Changes - -- 67716050: Update to TS5 beta - -### Patch Changes - -- Updated dependencies [67716050] - - @effect-app/schema@0.14.0 - - @effect-app/core@0.16.0 - -## 0.15.1 - -### Patch Changes - -- 5a69e08a: Reclaim Duration -- Updated dependencies [5a69e08a] - - @effect-app/core@0.15.1 - - @effect-app/schema@0.13.1 - -## 0.15.0 - -### Minor Changes - -- 25ec7aff: Reclaim the Option - -### Patch Changes - -- Updated dependencies [25ec7aff] - - @effect-app/schema@0.13.0 - - @effect-app/core@0.15.0 - -## 0.14.2 - -### Patch Changes - -- Updated dependencies [90d6e600] - - @effect-app/core@0.14.2 - - @effect-app/schema@0.12.9 - -## 0.14.1 - -### Patch Changes - -- Updated dependencies [8f97d979] - - @effect-app/core@0.14.1 - - @effect-app/schema@0.12.8 - -## 0.14.0 - -### Minor Changes - -- e4730778: Test dual api for Optic - -### Patch Changes - -- Updated dependencies [e4730778] - - @effect-app/core@0.14.0 - - @effect-app/schema@0.12.7 - -## 0.13.0 - -### Minor Changes - -- e7b4a0b9: Cleanup extensions - -### Patch Changes - -- Updated dependencies [e7b4a0b9] - - @effect-app/core@0.13.0 - - @effect-app/schema@0.12.6 - -## 0.12.5 - -### Patch Changes - -- Updated dependencies [a1bbd31f] - - @effect-app/schema@0.12.5 - -## 0.12.4 - -### Patch Changes - -- Updated dependencies [ffd11b1c] - - @effect-app/schema@0.12.4 - - @effect-app/core@0.12.4 - -## 0.12.3 - -### Patch Changes - -- 602ffbce: Update packages -- Updated dependencies [602ffbce] - - @effect-app/schema@0.12.3 - - @effect-app/core@0.12.3 - -## 0.12.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.12.2 - - @effect-app/schema@0.12.2 - -## 0.12.1 - -### Patch Changes - -- Disable trace -- Updated dependencies - - @effect-app/schema@0.12.1 - - @effect-app/core@0.12.1 - -## 0.12.0 - -### Minor Changes - -- Update to effect and fp-ts 0.1 line - -### Patch Changes - -- Updated dependencies - - @effect-app/schema@0.12.0 - - @effect-app/core@0.12.0 - -## 0.11.6 - -### Patch Changes - -- Fix ServiceBus should inherit runtime context - -## 0.11.5 - -### Patch Changes - -- e9a667fe: Add Operations - -## 0.11.4 - -### Patch Changes - -- 387dea6e: Fix Cache MapKey - -## 0.11.3 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.11.3 - - @effect-app/schema@0.11.3 - -## 0.11.2 - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.11.2 - - @effect-app/schema@0.11.2 - -## 0.11.1 - -### Patch Changes - -- 5ee19e1e: Improve exports -- Updated dependencies [5ee19e1e] - - @effect-app/core@0.11.1 - - @effect-app/schema@0.11.1 - -## 0.11.0 - -### Minor Changes - -- Remove @effect-ts/\* and replace monocle with fp-ts/optic - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.11.0 - - @effect-app/schema@0.11.0 - -## 0.10.2 - -### Patch Changes - -- Updated dependencies [cc4b37ea] - - @effect-app/core@0.10.2 - - @effect-app/schema@0.10.2 - -## 0.10.1 - -### Patch Changes - -- Updated dependencies [c4b95c0d] - - @effect-app/core@0.10.1 - - @effect-app/schema@0.10.1 - -## 0.10.0 - -### Minor Changes - -- Removed more of @effect-ts/core legacy - -### Patch Changes - -- Updated dependencies - - @effect-app/core@0.10.0 - - @effect-app/schema@0.10.0 - -## 0.9.0 - -### Minor Changes - -- 7d445f3e: Adopt Equivalence and Equal from @effect/data - -### Patch Changes - -- Updated dependencies [7d445f3e] - - @effect-app/schema@0.9.0 - - @effect-app/core@0.9.0 - -## 0.8.0 - -### Minor Changes - -- bba14672: Update to latest @effect/io and @effect/data - -### Patch Changes - -- Updated dependencies [bba14672] - - @effect-app/schema@0.8.0 - - @effect-app/core@0.8.0 - -## 0.7.1 - -### Patch Changes - -- Cleanup infra packages diff --git a/packages/effect-app/package.json b/packages/effect-app/package.json deleted file mode 100644 index 161723461..000000000 --- a/packages/effect-app/package.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "effect-app", - "version": "4.0.2", - "license": "MIT", - "type": "module", - "repository": { - "type": "git", - "url": "https://github.com/effect-app/libs.git", - "directory": "packages/effect-app" - }, - "dependencies": { - "@tsconfig/strictest": "^2.0.8", - "nanoid": "^5.1.11", - "path-parser": "^6.1.0", - "query-string": "^9.4.0", - "ts-toolbelt": "^9.6.0", - "validator": "^13.15.35" - }, - "devDependencies": { - "@faker-js/faker": "^8.4.1", - "@types/node": "25.9.1", - "@types/validator": "^13.15.10", - "fast-check": "^4.9.0", - "typescript": "~6.0.3", - "vitest": "^5.0.5" - }, - "peerDependencies": { - "effect": "^4.0.5" - }, - "typesVersions": { - "*": { - "*": [ - "dist/*" - ] - } - }, - "exports": { - ".": { - "types": "./src/index.ts", - "default": "./src/index.ts" - }, - "./internal/*": null, - "./*": { - "types": "./src/*.ts", - "default": "./src/*.ts" - } - }, - "publishConfig": { - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./internal/*": null, - "./*": { - "types": "./dist/*.d.ts", - "default": "./dist/*.js" - } - }, - "directory": ".publish", - "linkDirectory": false - }, - "scripts": { - "watch": "pnpm build:tsc -w", - "build:tsc": "pnpm clean-dist && pnpm check && node ../../scripts/rewrite-dist-declaration-imports.mjs dist", - "check": "tsgo --build", - "build:tsc-src": "pnpm clean-dist && tsc --build ./tsconfig.src.json && node ../../scripts/rewrite-dist-declaration-imports.mjs dist", - "build:src": "pnpm build:tsc-src", - "build": "pnpm build:tsc", - "watch2": "pnpm clean-dist && NODE_OPTIONS=--max-old-space-size=6144 tsgo -w", - "clean": "rm -rf dist", - "clean-dist": "sh ../../scripts/clean-dist.sh", - "circular": "pnpm circular:src && pnpm circular:dist", - "circular:src": "madge --circular --ts-config ./tsconfig.json --extensions ts ./src", - "circular:dist": "madge --circular --extensions js ./dist", - "compile": "NODE_OPTIONS=--max-old-space-size=6144 tsgo --noEmit", - "lint": "oxlint --type-aware ./src && pnpm exec dprint check --config ../../dprint.jsonc .", - "lint-fix": "oxlint --type-aware --fix ./src && pnpm exec dprint fmt --config ../../dprint.jsonc .", - "test": "vitest", - "test:run": "pnpm run test run --passWithNoTests", - "testsuite": "pnpm lint && pnpm circular && pnpm run test:run", - "ncu": "ncu", - "pub": "pnpm publish --access public", - "prepack": "node ../../scripts/stagePublishPackage.mjs prepack", - "postpack": "node ../../scripts/stagePublishPackage.mjs postpack", - "postpublish": "node ../../scripts/stagePublishPackage.mjs postpublish" - }, - "gitHead": "bd8e27eea3eff97db8739d577d67e7336c078d28", - "sideEffects": [ - "./dist/faker.js", - "./faker", - "./src/faker", - "./dist/faker", - "./dist/src/faker.js", - "./dist/faker.js" - ] -} diff --git a/packages/effect-app/src/Array.ts b/packages/effect-app/src/Array.ts deleted file mode 100644 index 3ec7dca36..000000000 --- a/packages/effect-app/src/Array.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { NonEmptyArray, NonEmptyReadonlyArray } from "effect/Array" -import * as Array from "effect/Array" -import * as Effect from "effect/Effect" -import { dual, type Predicate } from "./Function.ts" -import * as Option from "./Option.ts" - -export const groupByT = dual< - ( - f: (a: NoInfer) => Key - ) => (as: ReadonlyArray) => Array]>, - ( - as: ReadonlyArray, - f: (a: A) => Key - ) => Array]> ->(2, ( - as: ReadonlyArray, - f: (a: A) => Key -): Array]> => { - const r = new Map>() - for (const a of as) { - const k = f(a) - if (r.has(k)) { - r.get(k)!.push(a) - } else { - r.set(k, [a]) - } - } - return [...r.entries()] -}) - -export const groupByTNonEmpty = dual< - ( - f: (a: NoInfer) => Key - ) => (as: NonEmptyReadonlyArray) => NonEmptyArray]>, - ( - as: NonEmptyReadonlyArray, - f: (a: A) => Key - ) => NonEmptyArray]> ->(2, ( - as: NonEmptyReadonlyArray, - f: (a: A) => Key -): NonEmptyArray]> => { - const r = new Map>() - for (const a of as) { - const k = f(a) - if (r.has(k)) { - r.get(k)!.push(a) - } else { - r.set(k, [a]) - } - } - return [...r.entries()] as unknown as NonEmptyArray]> -}) - -// const a = [1, 2, 3] as const -// const b = [1, 2, 3] - -// const a1 = groupByTNonEmpty(a, (x) => x % 2) // $ExpectType NonEmptyReadonlyArray]> -// const b1 = groupByT(b, (x) => x % 2) // $ExpectType Array]> - -export function randomElement(a: NonEmptyReadonlyArray): A -export function randomElement(a: ReadonlyArray): A | undefined -export function randomElement(a: ReadonlyArray): A | undefined { - return a[Math.floor(Math.random() * a.length)] -} - -export function filterWith(self: ReadonlyArray, predicates: ReadonlyArray>) { - return self.filter((_) => predicates.every((f) => f(_))) -} - -export function forEachEffectNA(as: NonEmptyReadonlyArray, f: (a: A) => Effect.Effect) { - return Effect.map( - Effect.forEach(as, f), - (_) => Option.getOrNull(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(_)) - ) -} - -export * from "effect/Array" diff --git a/packages/effect-app/src/Chunk.ts b/packages/effect-app/src/Chunk.ts deleted file mode 100644 index bde5c8a49..000000000 --- a/packages/effect-app/src/Chunk.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { pipe } from "./Function.ts" -import * as Option from "./Option.ts" - -import * as Chunk from "effect/Chunk" -import * as Array from "./Array.ts" - -export * from "effect/Chunk" - -export function groupByTChunk_(c: Chunk.Chunk, f: (a: A) => Key) { - return pipe(Chunk.toReadonlyArray(c), Array.groupByT(f), Chunk.fromIterable) -} - -/** - * Returns the first `Some` produced by `f`. - * - * Effect's `Chunk.findFirst` takes a predicate, not an `Option`-returning function. - */ -export function findFirstMap( - f: (a: A) => Option.Option -) { - return (as: Chunk.Chunk) => { - const ass = Chunk.toReadonlyArray(as) - const len = ass.length - for (let i = 0; i < len; i++) { - const v = f(ass[i]!) - if (Option.isSome(v)) { - return v - } - } - return Option.none() - } -} diff --git a/packages/effect-app/src/Config.ts b/packages/effect-app/src/Config.ts deleted file mode 100644 index 1dc6d489e..000000000 --- a/packages/effect-app/src/Config.ts +++ /dev/null @@ -1,7 +0,0 @@ -// `Config.nested` previously needed a local override because effect v3's `nested` -// did not run the provider's `mapInput`/`constantCase` over the nested namespace -// segment (see commit "fix Config.nested mapping"). As of effect 4.0.0-beta.84 the -// upstream `nested` threads the prefix through the lookup path, so the provider -// transform covers it — the override is obsolete and the old internals it used are -// gone. Re-export the native module directly. -export * from "effect/Config" diff --git a/packages/effect-app/src/Config/SecretURL.ts b/packages/effect-app/src/Config/SecretURL.ts deleted file mode 100644 index 684db9396..000000000 --- a/packages/effect-app/src/Config/SecretURL.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @since 1.0.0 - */ -import type * as Chunk from "effect/Chunk" -import * as Config from "effect/Config" -import type * as Equal from "effect/Equal" -import type * as Redacted from "effect/Redacted" -import * as internal from "./internal/configSecretURL.ts" - -// /** -// * @since 1.0.0 -// * @category symbols -// */ -// export const SecretURLTypeId: unique symbol = internal.SecretURLTypeId - -// /** -// * @since 1.0.0 -// * @category symbols -// */ -// export type SecretURLTypeId = typeof SecretURLTypeId - -/** - * @since 1.0.0 - * @category models - */ -// eslint-disable-next-line import/namespace -export interface SecretURL extends Redacted.Redacted, Equal.Equal { - /** @internal */ - readonly raw: Array -} - -export interface SecretURLOps {} - -export const SecretURL: SecretURLOps = {} - -/** - * @since 1.0.0 - */ -export declare namespace SecretURL {} - -/** - * @since 1.0.0 - * @category refinements - * @deprecated - */ -export const isSecretURL: (u: unknown) => u is SecretURL = internal.isSecretURL - -/** - * @since 1.0.0 - * @category constructors - */ -export const make: (bytes: Array) => SecretURL = internal.make - -/** - * @since 1.0.0 - * @category constructors - */ -export const fromChunk: (chunk: Chunk.Chunk) => SecretURL = internal.fromChunk - -/** - * @since 1.0.0 - * @category constructors - */ -export const fromString: (text: string) => SecretURL = internal.fromString - -/** - * @since 1.0.0 - * @category getters - */ -export const value: (self: SecretURL) => string = internal.value - -/** - * @since 1.0.0 - * @category unsafe - */ -export const unsafeWipe: (self: SecretURL) => void = internal.unsafeWipe - -export const secretURL = (name?: string): Config.Config => { - return Config.map(Config.NonEmptyString(name), fromString) -} diff --git a/packages/effect-app/src/Config/internal/configSecretURL.ts b/packages/effect-app/src/Config/internal/configSecretURL.ts deleted file mode 100644 index 36a3aa5b7..000000000 --- a/packages/effect-app/src/Config/internal/configSecretURL.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as Chunk from "effect/Chunk" -import * as Redacted from "effect/Redacted" -import type * as SecretURL from "../SecretURL.ts" - -/** @internal */ -export const isSecretURL = (u: unknown): u is SecretURL.SecretURL => { - return Redacted.isRedacted(u) && typeof (u as any).raw !== "undefined" -} - -/** @internal */ -export const make = (bytes: Array): SecretURL.SecretURL => { - const secret = Object.assign( - Redacted.make(bytes.map((byte) => String.fromCharCode(byte)).join("")), - { raw: undefined as any } as const - ) - let protocol = "unknown" - try { - const url = new URL(bytes.map((byte) => String.fromCharCode(byte)).join("")) - protocol = url.protocol.substring(0, url.protocol.length - 1) - } catch { - const matches = protocol.match(/^([^:]+):\/\//) - if (matches !== null) { - protocol = matches[1]! - } - } - Object.defineProperty(secret, "toJSON", { - enumerable: false, - value() { - return ({ - _tag: "SecretURL", - protocol - }) - } - }) - Object.defineProperty(secret, "toString", { - enumerable: false, - value() { - return `SecretURL(${protocol}://)` - } - }) - Object.defineProperty(secret, "raw", { - enumerable: false, - value: bytes - }) - return secret -} - -/** @internal */ -export const fromChunk = (chunk: Chunk.Chunk): SecretURL.SecretURL => { - return make(Chunk.toReadonlyArray(chunk).map((char) => char.charCodeAt(0))) -} - -/** @internal */ -export const fromString = (text: string): SecretURL.SecretURL => { - return make(text.split("").map((char) => char.charCodeAt(0))) -} - -/** @internal */ -export const value = (self: SecretURL.SecretURL): string => { - return self.raw.map((byte) => String.fromCharCode(byte)).join("") -} - -/** @internal */ -export const unsafeWipe = (self: SecretURL.SecretURL): void => { - for (let i = 0; i < self.raw.length; i++) { - self.raw[i] = 0 - } -} diff --git a/packages/effect-app/src/ConfigProvider.ts b/packages/effect-app/src/ConfigProvider.ts deleted file mode 100644 index 881a627a2..000000000 --- a/packages/effect-app/src/ConfigProvider.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Local `nested` override removed: it existed to make the nested namespace honour the -// provider's `mapInput`/`constantCase` (see commit "fix Config.nested mapping"), which -// effect 4.0.0-beta.84 now does natively. The old override also relied on internals -// (`provider.get`/`mapInput`/`prefix`) that no longer exist. Re-export native directly. -export * from "effect/ConfigProvider" diff --git a/packages/effect-app/src/Context.ts b/packages/effect-app/src/Context.ts deleted file mode 100644 index 8846a8fa8..000000000 --- a/packages/effect-app/src/Context.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable import/namespace */ - -import * as CTX from "effect/Context" -import type * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" -import type * as Scope from "effect/Scope" -import type * as Types from "effect/Types" - -export * from "effect/Context" - -export interface Opaque extends CTX.Key { - of(this: void, self: Shape): Self - context(self: Shape): CTX.Context - // a version that leverages the Shape -> Self conversion - toLayer: ( - eff: Effect.Effect - ) => Layer.Layer> - use(f: (service: Shape) => Effect.Effect): Effect.Effect - useSync(f: (service: Shape) => A): Effect.Effect -} - -// export interface OpaqueMake -// extends CTX.Service -// { -// // temp while sorting out https://github.com/Effect-TS/effect-smol/pull/1534 -// of(self: Shape): Self -// contextMap2(self: Shape): CTX.Context -// // a version that leverages the Shape -> Self conversion -// toLayer: { -// ( -// eff: Effect.Effect -// ): Layer.Layer> -// (): Layer.Layer> -// } -// } - -export function assignTag( - key: string, - creationError?: Error -) { - return (cls: S): S & Opaque => { - const tag = CTX.Service(key) - let fields = tag - if (Reflect.ownKeys(cls).includes("key")) { - const { key, ...rest } = tag - fields = rest as any - } - const t = Object.assign(cls, Object.getPrototypeOf(tag), fields) - if (!creationError) { - const limit = Error.stackTraceLimit - Error.stackTraceLimit = 2 - creationError = new Error() - Error.stackTraceLimit = limit - } - // the stack is used to get the location of the tag definition, if a service is not found in the registry - Object.defineProperty(t, "stack", { - get() { - return creationError!.stack - } - }) - return t - } -} - -/** Accessor for a service method that returns a plain value. Wraps via `useSync`. */ -export const accessFn = < - Self extends object, - Shape extends Record, - K extends keyof Shape ->( - Tag: Opaque, - key: K -): Shape[K] extends (...args: [...infer Args]) => infer A ? (...args: Readonly) => Effect.Effect - : never => ((...args: Array) => Tag.useSync((s: any) => s[key](...args))) as any - -/** Accessor for a service method that returns an Effect. Delegates via `use`. */ -export const accessEffectFn = < - Self extends object, - Shape extends Record, - K extends keyof Shape ->( - Tag: Opaque, - key: K -): Shape[K] extends (...args: [...infer Args]) => Effect.Effect - ? (...args: Readonly) => Effect.Effect - : never => ((...args: Array) => Tag.use((s: any) => s[key](...args))) as any - -/** Accessor for a service property (constant). Wraps via `useSync`. */ -export const accessCn = < - Self extends object, - Shape extends Record, - K extends keyof Shape ->( - Tag: Opaque, - key: K -): Effect.Effect => Tag.useSync((s) => s[key]) - -/** Accessor for a service property that is an Effect. Delegates via `use`. */ -export const accessEffectCn = < - Self extends object, - Shape extends Record, - K extends keyof Shape ->( - Tag: Opaque, - key: K -): Shape[K] extends Effect.Effect ? Effect.Effect - : never => Tag.use((s: any) => s[key]) as any - -export const TypeId = "~Context.Opaque" - -// export function Opaque(key: Key) { -// return () => { -// const limit = Error.stackTraceLimit -// Error.stackTraceLimit = 2 -// const creationError = new Error() -// Error.stackTraceLimit = limit -// const c: abstract new(_: never) => Shape & { readonly [TypeId]: Key } = class {} as any - -// return assignTag(key, creationError)(c) -// } -// } - -export interface OpaqueClass - extends Opaque -{ - new(_: never): Shape & { readonly [TypeId]: Identifier } - readonly key: Identifier -} - -// export interface OpaqueClassMake -// extends OpaqueMake -// { -// new(_: never): Shape & { readonly [TypeId]: Identifier } -// readonly key: Identifier -// } - -export const Opaque: { - (): < - const Identifier extends string, - E, - R = Types.unassigned, - Args extends ReadonlyArray = never - >( - id: Identifier, - options?: { - readonly make: ((...args: Args) => Effect.Effect) | Effect.Effect | undefined - } - ) => - & OpaqueClass - & ([Types.unassigned] extends [R] ? unknown - : { - readonly make: [Args] extends [never] ? Effect.Effect - : (...args: Args) => Effect.Effect - }) - (): < - const Identifier extends string, - Make extends Effect.Effect | ((...args: any) => Effect.Effect) - >( - id: Identifier, - options: { - readonly make: Make - } - ) => - & OpaqueClass< - Self, - Identifier, - Make extends - | Effect.Effect - | ((...args: infer _Args) => Effect.Effect) ? _A - : never - > - & { readonly make: Make } -} = () => (id: string, options: any) => { - const svc = CTX.Service()(id, options) as any - return Object.assign(svc, { - toLayer: (eff: Effect.Effect) => { - return Layer.effect(svc, eff) - } - }) -} diff --git a/packages/effect-app/src/DataDependencies.ts b/packages/effect-app/src/DataDependencies.ts deleted file mode 100644 index 920fc7cb2..000000000 --- a/packages/effect-app/src/DataDependencies.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { NonEmptyReadonlyArray } from "effect/Array" -import * as Ref from "effect/Ref" -import * as Stream from "effect/Stream" -import * as Context from "./Context.ts" -import * as Effect from "./Effect.ts" -import * as RequestScopedDependencies from "./RequestScopedDependencies.ts" -import * as S from "./Schema.ts" - -export const DataDependency = S.Union([ - S.Struct({ - type: S.Literal("repo"), - name: S.String, - ids: S.optional(S.NonEmptyArray(S.String)) - }), - S.Struct({ - type: S.Literal("signal"), - name: S.String, - ids: S.optional(S.NonEmptyArray(S.String)) - }) -]) -export type DataDependency = S.Schema.Type - -export const DataDependencies = S.ReadonlySet(DataDependency) -export type DataDependencies = S.Schema.Type - -export const empty = (): DataDependencies => new Set() - -export const DataDependencySet = S.Struct({ - reads: DataDependencies, - writes: DataDependencies -}) -export type DataDependencySet = S.Schema.Type - -export interface DataDependencyRecorderService { - readonly read: (dependency: DataDependency) => Effect.Effect - readonly write: (dependency: DataDependency) => Effect.Effect - readonly get: Effect.Effect - readonly drain: Effect.Effect - readonly drainWrites: Effect.Effect -} - -const sameDomain = (a: DataDependency, b: DataDependency) => a.type === b.type && a.name === b.name - -const dependenciesOverlap = (a: DataDependency, b: DataDependency) => { - if (!sameDomain(a, b)) return false - if (a.ids === undefined || b.ids === undefined) return true - return a.ids.some((id) => b.ids?.includes(id)) -} - -const containsDependency = (dependencies: ReadonlySet, dependency: DataDependency) => { - for (const _ of dependencies) { - if (dependenciesOverlap(_, dependency)) return true - } - return false -} - -const appendDependency = (dependency: DataDependency) => (dependencies: DataDependencies): DataDependencies => { - const existing = [...dependencies].find((_) => sameDomain(_, dependency)) - if (existing === undefined) return new Set([...dependencies, dependency]) - if (existing.ids === undefined) return dependencies - if (dependency.ids === undefined) { - return new Set([...dependencies].filter((_) => _ !== existing).concat(dependency)) - } - const first = existing.ids[0] - const ids: NonEmptyReadonlyArray = [ - first, - ...new Set([...existing.ids.slice(1), ...dependency.ids].filter((id) => id !== first)) - ] - if (ids.length === existing.ids.length) return dependencies - const merged = existing.type === "repo" ? repo(existing.name, ids) : signal(existing.name, ids) - return new Set([...dependencies].filter((_) => _ !== existing).concat(merged)) -} - -export const merge = (...groups: ReadonlyArray): DataDependencies => { - let merged = empty() - for (const group of groups) { - for (const dependency of group) merged = appendDependency(dependency)(merged) - } - return merged -} - -export const DataDependencyRecorder = RequestScopedDependencies.make( - "effect-app/DataDependencyRecorder", - Effect.gen(function*() { - const readsRef = yield* Ref.make(empty()) - const writesRef = yield* Ref.make(empty()) - return makeDataDependencyRecorder(readsRef, writesRef) - }) -) -export type DataDependencyRecorder = typeof DataDependencyRecorder - -export type DataDependencyRecorderNotStartedError = RequestScopedDependencies.RequestScopedDependencyNotStartedError - -export const getDataDependencyRecorder = DataDependencyRecorder.current - -/** - * Guarantee a recorder is in scope for `self`, providing a fresh one per request when none is - * (e.g. a top-level client call with no parent query/command tracking its dependencies). An - * existing recorder is inherited, so nested calls forward into their parent rather than shadow it. - */ -export const ensureDataDependencyRecorder = (self: Effect.Effect): Effect.Effect => - Effect.flatMap( - DataDependencyRecorder, - (current) => current === "root" ? Effect.provide(self, DataDependencyRecorder.layer) : self - ) - -/** Stream variant of {@link ensureDataDependencyRecorder}. */ -export const ensureDataDependencyRecorderStream = (self: Stream.Stream): Stream.Stream => - Stream.unwrap( - Effect.map( - DataDependencyRecorder, - (current) => current === "root" ? Stream.provide(self, DataDependencyRecorder.layer) : self - ) - ) - -export const makeDataDependencyRecorder = ( - readsRef: Ref.Ref, - writesRef: Ref.Ref -): DataDependencyRecorderService => ({ - read: (dependency) => Ref.update(readsRef, appendDependency(dependency)), - write: (dependency) => Ref.update(writesRef, appendDependency(dependency)), - get: Effect.all({ - reads: Ref.get(readsRef), - writes: Ref.get(writesRef) - }), - drain: Effect.all({ - reads: Ref.getAndSet(readsRef, empty()), - writes: Ref.getAndSet(writesRef, empty()) - }), - drainWrites: Ref.getAndSet(writesRef, empty()) -}) - -export const repo = (name: string, ids?: NonEmptyReadonlyArray): DataDependency => - ids === undefined ? { type: "repo", name } : { type: "repo", name, ids } -export const signal = (name: string, ids?: NonEmptyReadonlyArray): DataDependency => - ids === undefined ? { type: "signal", name } : { type: "signal", name, ids } - -const RepoReadScope = Context.Reference("effect-app/DataDependencies/RepoReadScope", { - defaultValue: () => undefined -}) - -export const readRepo = (name: string) => - Effect.flatMap( - RepoReadScope, - (scope) => read(scope?.type === "repo" && scope.name === name ? scope : repo(name)) - ) - -export const withRepoReadScope = - (name: string, ids: NonEmptyReadonlyArray) => - (self: Effect.Effect): Effect.Effect => - Effect.provideService(self, RepoReadScope, repo(name, ids)) - -export const QueryReadDependenciesMetaKey = "effect-app.query.readDependencies" - -export const read = (dependency: DataDependency) => - getDataDependencyRecorder.pipe(Effect.flatMap((_) => _.read(dependency))) - -export const write = (dependency: DataDependency) => - getDataDependencyRecorder.pipe(Effect.flatMap((_) => _.write(dependency))) - -export const intersects = ( - a: ReadonlySet, - b: ReadonlySet -) => { - for (const dependency of a) { - if (containsDependency(b, dependency)) return true - } - return false -} - -export const isNonEmpty = (dependencies: DataDependencies) => dependencies.size > 0 diff --git a/packages/effect-app/src/DateTime.ts b/packages/effect-app/src/DateTime.ts deleted file mode 100644 index 9b0bdad7b..000000000 --- a/packages/effect-app/src/DateTime.ts +++ /dev/null @@ -1,473 +0,0 @@ -import * as DateTime from "effect/DateTime" - -/** - * The same instant as `date`, zoned with the process timezone. - * - * A JavaScript `Date` stores no zone. This attaches `DateTime.zoneMakeLocal()` without moving the instant. - */ -export const makeZonedLocal = (date: Date): DateTime.Zoned => - DateTime.makeZonedUnsafe(date, { timeZone: DateTime.zoneMakeLocal() }) - -/** - * Zones `date` with the process timezone, runs the functions, then returns the instant with `DateTime.toDateUtc`. - * - * Accepts as many functions as `pipe`. The last function must return a `DateTime`. Calendar parts follow the process timezone, as `Date#setDate` does. - */ -export function pipeZonedLocal(date: Date): Date -export function pipeZonedLocal( - date: Date, - ab: (self: DateTime.Zoned) => A -): Date -export function pipeZonedLocal( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B -): Date -export function pipeZonedLocal( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L = never, - M extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L, - mn: (l: L) => M -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L = never, - M = never, - N extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L, - mn: (l: L) => M, - no: (m: M) => N -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L = never, - M = never, - N = never, - O extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L, - mn: (l: L) => M, - no: (m: M) => N, - op: (n: N) => O -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L = never, - M = never, - N = never, - O = never, - P extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L, - mn: (l: L) => M, - no: (m: M) => N, - op: (n: N) => O, - pq: (o: O) => P -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L = never, - M = never, - N = never, - O = never, - P = never, - Q extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L, - mn: (l: L) => M, - no: (m: M) => N, - op: (n: N) => O, - pq: (o: O) => P, - qr: (p: P) => Q -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L = never, - M = never, - N = never, - O = never, - P = never, - Q = never, - R extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L, - mn: (l: L) => M, - no: (m: M) => N, - op: (n: N) => O, - pq: (o: O) => P, - qr: (p: P) => Q, - rs: (q: Q) => R -): Date -export function pipeZonedLocal< - A = never, - B = never, - C = never, - D = never, - E = never, - F = never, - G = never, - H = never, - I = never, - J = never, - K = never, - L = never, - M = never, - N = never, - O = never, - P = never, - Q = never, - R = never, - S extends DateTime.DateTime = never ->( - date: Date, - ab: (self: DateTime.Zoned) => A, - bc: (a: A) => B, - cd: (b: B) => C, - de: (c: C) => D, - ef: (d: D) => E, - fg: (e: E) => F, - gh: (f: F) => G, - hi: (g: G) => H, - ij: (h: H) => I, - jk: (i: I) => J, - kl: (j: J) => K, - lm: (k: K) => L, - mn: (l: L) => M, - no: (m: M) => N, - op: (n: N) => O, - pq: (o: O) => P, - qr: (p: P) => Q, - rs: (q: Q) => R, - st: (r: R) => S -): Date -export function pipeZonedLocal(date: Date, ...steps: ReadonlyArray<(value: any) => any>): Date { - let current: unknown = makeZonedLocal(date) - for (const step of steps) { - current = step(current) - } - if (!DateTime.isDateTime(current)) { - throw new TypeError("pipeZonedLocal must finish on a DateTime") - } - return DateTime.toDateUtc(current) -} diff --git a/packages/effect-app/src/Effect.ts b/packages/effect-app/src/Effect.ts deleted file mode 100644 index b84056095..000000000 --- a/packages/effect-app/src/Effect.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable prefer-destructuring */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars - -import * as Effect from "effect/Effect" -import * as Option from "effect/Option" -import * as Ref from "effect/Ref" -import type { Semaphore } from "effect/Semaphore" -import * as Struct from "effect/Struct" -import type * as Context from "./Context.ts" -import { curry } from "./Function.ts" - -export * from "effect/Effect" -// v4: Effect interface not re-exported by `export *` due to local binding collision -export type { Effect } from "effect/Effect" - -export function flatMapOption( - self: Effect.Effect, E, R>, - fm: (a: A) => Effect.Effect -): Effect.Effect, E | E2, R | R2> { - return Effect.flatMap(self, (d) => - Option.match(d, { - onNone: () => Effect.sync(() => Option.none()), - onSome: (_) => Effect.map(fm(_), Option.some) - })) -} - -export function tapOption( - self: Effect.Effect, E, R>, - fm: (a: A) => Effect.Effect -): Effect.Effect, E | E2, R | R2> { - return Effect.flatMap(self, (d) => - Option.match(d, { - onNone: () => Effect.sync(() => Option.none()), - onSome: (_) => Effect.map(fm(_), () => Option.some(_)) - })) -} - -export function zipRightOption( - self: Effect.Effect, E, R>, - fm: Effect.Effect -) { - return Effect.flatMap(self, (d) => - Option.match(d, { - onNone: () => Effect.sync(() => Option.none()), - onSome: (_) => Effect.map(fm, () => Option.some(_)) - })) -} - -export function mapOption( - self: Effect.Effect, E, R>, - fm: (a: A) => A2 -): Effect.Effect, E, R> { - return Effect.map(self, (d) => - Option.match(d, { - onNone: () => Option.none(), - onSome: (_) => Option.some(fm(_)) - })) -} - -/** - * Takes [A, B], applies it to a curried Effect function, - * taps the Effect, returning A. - */ -export function tupleTap( - f: (b: B) => (a: A) => Effect.Effect -) { - return (t: readonly [A, B]) => Effect.sync(() => t[0]).pipe(Effect.tap(f(t[1]))) -} - -/** - * Takes [A, B], applies it to an Effect function, - * taps the Effect, returning A. - */ -export function tupleTap_(f: (a: A, b: B) => Effect.Effect) { - return tupleTap(curry(f)) -} - -export function ifDiffR(f: (i: I) => Effect.Effect) { - return (n: I, orig: I) => ifDiff_(n, orig, f) -} - -export function ifDiff_( - n: I, - orig: I, - f: (i: I) => Effect.Effect -) { - return n !== orig ? f(n) : Effect.void -} - -export function ifDiff(n: I, orig: I) { - return (f: (i: I) => Effect.Effect) => ifDiff_(n, orig, f) -} - -/** - * Ref has atomic modify support if synchronous, for Effect we need a Semaphore. - */ -export function modifyWithPermitWithEffect(ref: Ref.Ref, semaphore: Semaphore) { - const withPermit = semaphore.withPermits(1) - return (mod: (a: A) => Effect.Effect) => - withPermit( - Effect - .flatMap(Ref.get(ref), mod) - .pipe( - Effect.tap(([, _]) => Ref.set(ref, _)), - Effect.map(([_]) => _) - ) - ) -} - -type ServiceA = T extends Effect.Effect ? S - : T extends Context.Service ? S - : never -type ServiceR = T extends Effect.Effect ? R - : T extends Context.Service ? I - : never -type ServiceE = T extends Effect.Effect ? E : never -// type Values = T extends { [s: string]: infer S } ? ServiceA : never -type ValuesR = T extends { [s: string]: infer S } ? ServiceR : never -type ValuesE = T extends { [s: string]: infer S } ? ServiceE : never - -/** - * Due to tsplus unification (tsplus unify tag), when trying to use the Effect type in a type constraint - * the compiler will cause basically anything to match. as such, use this type instead. - * ```ts - * const a = < - * SVC extends Record< - * string, - * ((req: number) => Effect.Effect) | Effect.Effect - * > - * >(svc: SVC) => svc - * - * const b = a({ str: "" }) // valid, but shouldn't be! - * ``` - */ -// eslint-disable-next-line import/namespace -export interface EffectUnunified extends Effect.Effect {} - -export type LowerFirst = S extends `${infer First}${infer Rest}` ? `${Lowercase}${Rest}` - : S -export type LowerServices | Effect.Effect>> = { - [key in keyof T as LowerFirst]: ServiceA -} - -export function allLower | Effect.Effect>>( - services: T -) { - return Effect.all( - Struct.keys(services).reduce((prev, cur) => { - const svc = services[cur]! - prev[(cur[0]!.toLowerCase() + cur.slice(1)) as unknown as LowerFirst] = svc - return prev - }, {} as any), - { concurrency: "unbounded" } - ) as any as Effect.Effect, ValuesE, ValuesR> -} - -export function allLowerWith | Effect.Effect>, A>( - services: T, - fn: (services: LowerServices) => A -) { - return Effect.map(allLower(services), fn) -} - -export function allLowerWithEffect< - T extends Record | Effect.Effect>, - R, - E, - A ->( - services: T, - fn: (services: LowerServices) => Effect.Effect -) { - return Effect.flatMap(allLower(services), fn) -} - -/** - * Recovers from all errors. - */ -export function catchAllMap(f: (e: E) => A2) { - return (self: Effect.Effect): Effect.Effect => - Effect.catch(self, (err: E) => Effect.sync(() => f(err))) -} diff --git a/packages/effect-app/src/Emailer.ts b/packages/effect-app/src/Emailer.ts deleted file mode 100644 index 1fe5eab07..000000000 --- a/packages/effect-app/src/Emailer.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as Data from "effect/Data" -import type { NonEmptyReadonlyArray } from "./Array.ts" -import * as Context from "./Context.ts" -import type * as Effect from "./Effect.ts" -import type { Email } from "./Schema.ts" - -export class SendMailError extends Data.TaggedError("SendMailError")<{ - readonly raw: Error -}> {} - -export class Emailer extends Context.Opaque Effect.Effect -}>()("effect-app/Emailer") {} - -export type EmailData = Email | { - name?: string - email: Email -} - -export interface EmailContentPart { - type: string - value: string -} - -export type EmailRecipients = EmailData | NonEmptyReadonlyArray - -export interface EmailMsgBase { - readonly to: EmailRecipients - readonly cc?: EmailRecipients - readonly bcc?: EmailRecipients - readonly from: EmailData - readonly replyTo?: EmailData - readonly subject?: string - /** - * should multiple `to` addresess be considered multiple emails? - * defaults to `true`, not to leak email addresses - */ - readonly isMultiple?: boolean -} - -export type EmailContent = - | { text: string } - | { html: string } - | { templateId: string } - | { content: NonEmptyReadonlyArray } - -export type EmailMsg = - & EmailMsgBase - & EmailContent - -export type EmailMsgOptionalFrom = Omit & Partial> & EmailContent diff --git a/packages/effect-app/src/Function.ts b/packages/effect-app/src/Function.ts deleted file mode 100644 index 567367d23..000000000 --- a/packages/effect-app/src/Function.ts +++ /dev/null @@ -1,168 +0,0 @@ -export * from "effect/Function" -export type { Predicate, Refinement } from "effect/Predicate" - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export function tupledCurry(f: (b: B) => (a: A) => C) { - return (t: [A, B]) => f(t[1])(t[0]) -} - -export function reverseCurry(f: (b: B) => (a: A) => C) { - return (a: A) => (b: B) => f(b)(a) -} - -export function curry(f: (a: A, b: B) => C) { - return (b: B) => (a: A) => f(a, b) -} - -export function uncurry(f: (b: B) => (a: A) => C) { - return (a: A, b: B) => f(b)(a) -} - -// It's better to have these expanded, so that on the call site you understand the functions better. -// type OptMagix = ((b?: B) => (a: A) => C) & { -// /** -// * Uncurried version of the parent function -// */ -// _: (a: A, b?: B) => C -// /** -// * Reverse curried version of the parent function -// */ -// r: (a: A) => (b?: B) => C -// } -// type Magix = ((b: B) => (a: A) => C) & { -// /** -// * Uncurried version of the parent function -// */ -// _: (a: A, b: B) => C -// /** -// * Reverse curried version of the parent function -// */ -// r: (a: A) => (b: B) => C -// } - -export function curriedMagix( - f: (b?: B) => (a: A) => C -): ((b?: B) => (a: A) => C) & { - /** - * Uncurried version of the parent function - */ - _: (a: A, b?: B) => C - /** - * Reverse curried version of the parent function - */ - r: (a: A) => (b?: B) => C -} -export function curriedMagix( - f: (b: B) => (a: A) => C -): ((b: B) => (a: A) => C) & { - /** - * Uncurried version of the parent function - */ - _: (a: A, b: B) => C - /** - * Reverse curried version of the parent function - */ - r: (a: A) => (b: B) => C -} -export function curriedMagix(f: (b: B) => (a: A) => C) { - return Object.assign(f, { - /** - * Uncurried version of the parent function - */ - _: uncurry(f), - /** - * Reverse curried version of the parent function - */ - r: reverseCurry(f) - }) -} - -export function uncurriedMagix( - f: (a: A, b?: B) => C -): ((b?: B) => (a: A) => C) & { - /** - * Uncurried version of the parent function - */ - _: (a: A, b?: B) => C - /** - * Reverse curried version of the parent function - */ - r: (a: A) => (b?: B) => C -} -export function uncurriedMagix( - f: (a: A, b: B) => C -): ((b: B) => (a: A) => C) & { - /** - * Uncurried version of the parent function - */ - _: (a: A, b: B) => C - /** - * Reverse curried version of the parent function - */ - r: (a: A) => (b: B) => C -} -export function uncurriedMagix(f: (a: A, b: B) => C) { - const curried = curry(f) - return Object.assign(curried, { - /** - * Uncurried version of the parent function - */ - _: f, - /** - * Reverse curried version of the parent function - */ - r: reverseCurry(curried) - }) -} - -export function reverseCurriedMagix( - f: (a: A) => (b: B) => C -): ((b?: B) => (a: A) => C) & { - /** - * Uncurried version of the parent function - */ - _: (a: A, b?: B) => C - /** - * Reverse curried version of the parent function - */ - r: (a: A) => (b?: B) => C -} -export function reverseCurriedMagix( - f: (a?: A) => (b: B) => C -): ((b: B) => (a: A) => C) & { - /** - * Uncurried version of the parent function - */ - _: (a: A, b: B) => C - /** - * Reverse curried version of the parent function - */ - r: (a: A) => (b: B) => C -} -export function reverseCurriedMagix(f: (a: A) => (b: B) => C) { - const curried = reverseCurry(f) - return Object.assign(curried, { - /** - * Uncurried version of the parent function - */ - _: uncurry(curried), - /** - * Reverse curried version of the parent function - */ - r: f - }) -} - -export function lazy(f: () => A): () => A { - let tmp: A | undefined - let done = false - return () => { - if (done) { - return tmp! - } - tmp = f() - done = true - return tmp - } -} diff --git a/packages/effect-app/src/Inputify.type.ts b/packages/effect-app/src/Inputify.type.ts deleted file mode 100644 index a47022685..000000000 --- a/packages/effect-app/src/Inputify.type.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { BuiltInObject } from "effect-app/utils" - -import type { Primitive } from "./Widen.type.ts" - -// Get rid of Date | string, and replace with Date | null -export type Inputify = Date extends T ? string extends T ? Date | null - : Date | null - : [T] extends [BuiltInObject] | [Primitive] ? T - : [T] extends [Array] ? { [K in keyof T]: Inputify } - : [T] extends [ReadonlyArray] ? { [K in keyof T]: Inputify } - : [T] extends [object] ? { [K in keyof T]: Inputify } - : T diff --git a/packages/effect-app/src/Layer.ts b/packages/effect-app/src/Layer.ts deleted file mode 100644 index 6f1c820b2..000000000 --- a/packages/effect-app/src/Layer.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type * as Array from "effect/Array" -import * as Effect from "effect/Effect" -import { dual } from "effect/Function" -import * as Layer from "effect/Layer" -import type * as Scope from "effect/Scope" -import type * as Types from "effect/Types" -import type * as Context from "./Context.ts" -import { type EffectGenUtils } from "./utils/gen.ts" - -export * from "effect/Layer" - -type Make = DependenciesOpt & (MakeEff | MakeGenNo | MakeGen) - -type MakeEff = { - readonly make: Effect.Effect -} -type MakeGen = { - readonly make: () => Generator, S> -} -type MakeGenNo = { - readonly make: () => Generator -} -type MakeErr = Opts extends { make: () => any } ? EffectGenUtils.Error : never -type MakeContext = Opts extends { make: () => any } ? EffectGenUtils.Context : never - -type DependenciesOpt = { dependencies?: Array.NonEmptyReadonlyArray } -type Dependencies = { dependencies: Array.NonEmptyReadonlyArray } - -// Local replacements for removed Effect.Service.MakeDeps* types -type MakeDepsE = Opts extends { dependencies: ReadonlyArray> } ? E : never -type MakeDepsOut = Opts extends { dependencies: ReadonlyArray> } ? Out : never - -type PackedLayers = - & Layer.Layer< - I, - MakeErr | MakeDepsE, - Exclude, Scope.Scope | MakeDepsOut> - > - & { - withoutDependencies: Layer.Layer, Exclude, Scope.Scope>> - } - -type PackedOrUnpackedLayer = Opts extends Dependencies ? PackedLayers & {} - : Layer.Layer, MakeContext> - -export const make: { - ( - tag: Context.Service - ): , any, any>>( - options: Opts - ) => PackedOrUnpackedLayer - , any, any>>( - tag: Context.Service, - options: Opts - ): PackedOrUnpackedLayer -} = dual(2, (tag, options) => { - const effect = options.make[Symbol.toStringTag] === "GeneratorFunction" - ? Effect.fnUntraced(options.make)() - : options.make - const withoutDependencies = Layer.effect(tag, effect) - if (options.dependencies) { - return Object.assign( - withoutDependencies.pipe(Layer.provide(options.dependencies)), - { withoutDependencies } - ) - } else { - return withoutDependencies - } -}) diff --git a/packages/effect-app/src/Model.ts b/packages/effect-app/src/Model.ts deleted file mode 100644 index fdfa6b568..000000000 --- a/packages/effect-app/src/Model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./Model/dsl.ts" -export * as Q from "./Model/query.ts" -export { makeRepo } from "./Model/Repository.ts" -export { repositoryDependency } from "./Model/Repository.ts" -export { type RegisteredRepository, RepositoryRegistry, RepositoryRegistryLive } from "./Model/Repository.ts" diff --git a/packages/effect-app/src/Model/Repository.ts b/packages/effect-app/src/Model/Repository.ts deleted file mode 100644 index f68b0b3e0..000000000 --- a/packages/effect-app/src/Model/Repository.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { repositoryDependency } from "./Repository/dependency.ts" -export * from "./Repository/ext.ts" -export * from "./Repository/legacy.ts" -export { makeRepo } from "./Repository/makeRepo.ts" -export * from "./Repository/Registry.ts" -export * from "./Repository/service.ts" -export * from "./Repository/validation.ts" diff --git a/packages/effect-app/src/Model/Repository/Registry.ts b/packages/effect-app/src/Model/Repository/Registry.ts deleted file mode 100644 index 6fb1a675b..000000000 --- a/packages/effect-app/src/Model/Repository/Registry.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { DatabaseError } from "../../client/errors.ts" -import * as Context from "../../Context.ts" -import * as Effect from "../../Effect.ts" - -export interface RegisteredRepository { - readonly seedNamespace: (namespace: string) => Effect.Effect -} - -const make = Effect.sync(() => { - const repos = new Map() - return { - register(modelName: string, repo: RegisteredRepository) { - repos.set(modelName, repo) - }, - seedNamespace: (namespace: string) => - Effect.suspend(() => - Effect.forEach( - repos.values(), - (r) => r.seedNamespace(namespace), - { concurrency: "unbounded", discard: true } - ) - ), - get entries(): ReadonlyMap { - return repos - } - } -}) - -export class RepositoryRegistry extends Context.Opaque void - readonly seedNamespace: (namespace: string) => Effect.Effect - readonly entries: ReadonlyMap -}>()("effect-app/RepositoryRegistry", { make }) {} - -export const RepositoryRegistryLive = RepositoryRegistry.toLayer(RepositoryRegistry.make) diff --git a/packages/effect-app/src/Model/Repository/dependency.ts b/packages/effect-app/src/Model/Repository/dependency.ts deleted file mode 100644 index adc7e64d9..000000000 --- a/packages/effect-app/src/Model/Repository/dependency.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as S from "../../Schema.ts" -import * as SchemaAST from "../../SchemaAST.ts" - -const RepositoryDependencyAnnotation = "effect-app/Repository/dependency" - -/** Marks an encoded model field as an identity alias for repository invalidation. */ -export const repositoryDependency = (schema: Schema): Schema["Rebuild"] => - S.annotateEncoded({ [RepositoryDependencyAnnotation]: true })(schema) - -/** @internal */ -export const repositoryDependencyPaths = (schema: S.Schema): readonly string[] => { - const visit = (ast: SchemaAST.AST, path: readonly string[]): readonly string[] => { - const annotations = ast.checks?.at(-1)?.annotations ?? ast.annotations - if (annotations?.[RepositoryDependencyAnnotation] === true) return [path.join(".")] - if (SchemaAST.isDeclaration(ast)) return ast.typeParameters.flatMap((parameter) => visit(parameter, path)) - if (SchemaAST.isUnion(ast)) return ast.types.flatMap((member) => visit(member, path)) - if (SchemaAST.isObjects(ast)) { - return ast.propertySignatures.flatMap((property) => - typeof property.name === "string" ? visit(property.type, [...path, property.name]) : [] - ) - } - if (SchemaAST.isArrays(ast)) { - return [ - ...ast.elements.flatMap((element, index) => visit(element, [...path, String(index)])), - ...ast.rest.flatMap((element) => visit(element, [...path, "-1"])) - ] - } - return [] - } - - return [...new Set(visit(SchemaAST.toEncoded(schema.ast), []))] -} diff --git a/packages/effect-app/src/Model/Repository/ext.ts b/packages/effect-app/src/Model/Repository/ext.ts deleted file mode 100644 index d8fedfbef..000000000 --- a/packages/effect-app/src/Model/Repository/ext.ts +++ /dev/null @@ -1,374 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -import * as Exit from "effect/Exit" -import * as Request from "effect/Request" -import * as RequestResolver from "effect/RequestResolver" -import * as Array from "../../Array.ts" -import type { NonEmptyArray } from "../../Array.ts" -import { type DatabaseError, type InvalidStateError, NotFoundError, type OptimisticConcurrencyException } from "../../client/errors.ts" -import * as Effect from "../../Effect.ts" -import * as Option from "../../Option.ts" -import { type FixEnv, type PureEnv, runTerm } from "../../Pure.ts" -import { AnyPureDSL } from "../dsl.ts" -import type { FieldValues } from "../filter/types.ts" -import type { Query, QueryEnd, QueryWhere } from "../query.ts" -import * as Q from "../query.ts" -import type { Repository } from "./service.ts" - -interface BatchOptions { - readonly batch?: true | number -} - -const asReadonlyArray = (itemOrItems: T | ReadonlyArray): ReadonlyArray => - globalThis.Array.isArray(itemOrItems) - ? itemOrItems as ReadonlyArray - : [itemOrItems as T] - -const getBatchSize = (batch?: true | number) => - batch === true - ? 100 - : typeof batch === "number" && Number.isFinite(batch) && batch > 0 - ? Math.floor(batch) - : undefined - -export const extendRepo = < - T, - Encoded extends FieldValues, - Evt, - ItemType extends string, - IdKey extends keyof T & keyof Encoded, - RSchema, - RPublish, - RProvided = never ->( - repo: Repository -) => { - const get = (id: T[IdKey]) => - repo.find(id).pipe( - Effect.flatMap(Option.match({ - onNone: () => Effect.fail(new NotFoundError({ type: repo.itemType, id })), - onSome: Effect.succeed - })) - ) - function saveManyWithPure_< - R, - A, - E, - S1 extends T, - S2 extends T - >( - items: Iterable, - pure: Effect.Effect> - ) { - return saveAllWithEffectInt( - runTerm(pure, [...items]) - ) - } - - function saveWithPure_< - R, - A, - E, - S1 extends T, - S2 extends T - >( - item: S1, - pure: Effect.Effect> - ) { - return saveAllWithEffectInt( - runTerm(pure, item) - .pipe(Effect - .map(([item, events, a]) => [[item], events, a])) - ) - } - - function saveAllWithEffectInt< - P extends T, - R, - E, - A - >( - gen: Effect.Effect, Iterable, A], E, R> - ): Effect.Effect< - A, - E | InvalidStateError | OptimisticConcurrencyException | DatabaseError, - R | RSchema | RPublish - > { - return Effect.flatMap(gen, ([items, events, a]) => repo.saveAndPublish(items, events).pipe(Effect.map(() => a))) - } - - function saveManyWithPureBatched_< - R, - A, - E, - S1 extends T, - S2 extends T - >( - items: Iterable, - pure: Effect.Effect>, - batchSize = 100 - ) { - return Effect.forEach( - Array.chunksOf(items, batchSize), - (batch) => - saveAllWithEffectInt( - runTerm(pure, batch) - ) - ) - } - - type PureStoreError = InvalidStateError | OptimisticConcurrencyException | DatabaseError - - const queryAndSavePure: { - ( - q: ( - q: Query - ) => QueryEnd, - pure: Effect.Effect> - ): Effect.Effect< - A, - PureStoreError | NotFoundError | E2, - | RSchema - | RPublish - | Exclude - }> - > - ( - q: ( - q: Query - ) => - | Query - | QueryWhere - | QueryEnd, - pure: Effect.Effect> - ): Effect.Effect< - A, - PureStoreError | E2, - | RSchema - | RPublish - | Exclude - }> - > - ( - q: ( - q: Query - ) => - | Query - | QueryWhere - | QueryEnd, - pure: Effect.Effect>, - batch: "batched" | number - ): Effect.Effect< - A[], - PureStoreError | E2, - | RSchema - | RPublish - | Exclude - }> - > - } = ((q: any, pure: any, batch?: "batched" | number) => - // Overload dispatch: query returns T | T[]; pure helpers are generic on item shape. - // Runtime path is query → pure term → saveAndPublish (which raises DatabaseError). - repo.query(q).pipe( - Effect.andThen((_) => - Array.isArray(_) - ? batch === undefined - ? saveManyWithPure_(_ as T[], pure) - : saveManyWithPureBatched_(_ as T[], pure, batch === "batched" ? 100 : batch) - : saveWithPure_(_ as T, pure) - ) - )) as typeof queryAndSavePure - - const saveManyWithPure: { - ( - items: Iterable, - pure: Effect.Effect> - ): Effect.Effect< - A, - PureStoreError | E, - | RSchema - | RPublish - | Exclude - }> - > - ( - items: Iterable, - pure: Effect.Effect>, - batch: "batched" | number - ): Effect.Effect< - A[], - PureStoreError | E, - | RSchema - | RPublish - | Exclude - }> - > - } = ((items: Iterable, pure: any, batch?: "batched" | number) => - batch - ? Effect.forEach( - Array.chunksOf([...items], batch === "batched" ? 100 : batch), - (batchItems) => - saveAllWithEffectInt( - runTerm(pure, batchItems as any) - ) - ) - : saveAllWithEffectInt( - runTerm(pure, [...items] as any) - )) as typeof saveManyWithPure - - const byIdAndSaveWithPure: { - ( - id: T[IdKey], - pure: Effect.Effect> - ): Effect.Effect< - A, - PureStoreError | NotFoundError | E, - | RSchema - | RPublish - | Exclude - }> - > - } = (id, pure) => get(id).pipe(Effect.flatMap((item) => saveWithPure_(item, pure))) - - // query can raise DatabaseError; NotFound is completed per-entry. Dual package - // round-trips may still surface other store errors via failCause — keep channel honest. - type Req = - & Request.Request | DatabaseError> - & { _tag: `Get${ItemType}`; id: T[IdKey] } - const _request = Request.tagged(`Get${repo.itemType}`) - - const requestResolver = RequestResolver - .make(( - entries: NonEmptyArray>, - _key: unknown - ) => - (repo.query(Q.where(repo.idKey as any, "in" as any, entries.map((_) => _.request.id)) as any) as Effect.Effect< - readonly T[], - DatabaseError - >) - .pipe( - Effect.andThen((items) => - Effect.forEach(entries, (entry) => - Request.complete( - Array - .findFirst(items, (_) => _[repo.idKey] === entry.request.id) - .pipe(Option.match({ - onNone: () => Exit.fail(new NotFoundError({ type: repo.itemType, id: entry.request.id })), - onSome: Exit.succeed - })) - )(entry), { discard: true }) - ), - Effect - .catchCause((cause) => - Effect.forEach(entries, (entry) => Request.complete(Exit.failCause(cause))(entry), { discard: true }) - ) - ) - ) - .pipe( - RequestResolver.batchN(20) - ) - - const exts = { - request: (id: T[IdKey]) => Effect.request(_request({ id }), requestResolver), - get, - log: (evt: Evt) => AnyPureDSL.log(evt), - /** - * Enables chunked writes for large batches via `options.batch`. - * Note: batching breaks transactional properties because chunks are saved independently. - */ - save: (itemOrItems: T | ReadonlyArray, options?: BatchOptions) => { - const items = asReadonlyArray(itemOrItems) - if (!Array.isReadonlyArrayNonEmpty(items)) { - return Effect.void - } - const batchSize = getBatchSize(options?.batch) - if (batchSize === undefined) { - return repo.saveAndPublish(items) - } - return Effect.forEach( - Array.chunksOf(items, batchSize), - (batch) => repo.saveAndPublish(batch), - { discard: true } - ) - }, - saveWithEvents: (events: Iterable) => (...items: NonEmptyArray) => repo.saveAndPublish(items, events), - /** - * Enables chunked deletes for large batches via `options.batch`. - * Note: batching breaks transactional properties because chunks are removed independently. - */ - remove: (itemOrItems: T | ReadonlyArray, options?: BatchOptions) => { - const items = asReadonlyArray(itemOrItems) - if (!Array.isReadonlyArrayNonEmpty(items)) { - return Effect.void as Effect.Effect - } - const batchSize = getBatchSize(options?.batch) - if (batchSize === undefined) { - return repo.removeAndPublish(items) - } - return Effect.forEach( - Array.chunksOf(items, batchSize), - (batch) => repo.removeAndPublish(batch), - { discard: true } - ) - }, - /** - * Enables chunked deletes for large batches via `options.batch`. - * Note: batching breaks transactional properties because chunks are removed independently. - */ - removeById: (idOrIds: T[IdKey] | ReadonlyArray, options?: BatchOptions) => { - const ids = asReadonlyArray(idOrIds) - if (!Array.isReadonlyArrayNonEmpty(ids)) { - return Effect.void as Effect.Effect - } - const batchSize = getBatchSize(options?.batch) - if (batchSize === undefined) { - return repo.removeById(ids) - } - return Effect.forEach( - Array.chunksOf(ids, batchSize), - (batch) => repo.removeById(batch), - { discard: true } - ) - }, - queryAndSavePure, - saveManyWithPure, - byIdAndSaveWithPure, - saveWithPure: < - R, - A, - E, - S1 extends T, - S2 extends T - >( - item: S1, - pure: Effect.Effect> - ) => - saveAllWithEffectInt( - runTerm(pure, item) - .pipe(Effect.map(([item, events, a]) => [[item], events, a])) - ) - } - - return { - ...repo, - ...exts - } as Repository & typeof exts -} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface ExtendedRepository< - T, - Encoded extends FieldValues, - Evt, - ItemType extends string, - IdKey extends keyof T & keyof Encoded, - RSchema, - RPublish, - RProvided = never -> extends ReturnType> {} diff --git a/packages/effect-app/src/Model/Repository/internal/internal.ts b/packages/effect-app/src/Model/Repository/internal/internal.ts deleted file mode 100644 index 99bdf2b4d..000000000 --- a/packages/effect-app/src/Model/Repository/internal/internal.ts +++ /dev/null @@ -1,987 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as Equivalence from "effect/Equivalence" -import { flow, pipe } from "effect/Function" -import * as HashMap from "effect/HashMap" -import * as HashSet from "effect/HashSet" -import * as Metric from "effect/Metric" -import * as Pipeable from "effect/Pipeable" -import * as Ref from "effect/Ref" -import * as Result from "effect/Result" -import type * as Scope from "effect/Scope" -import * as Unify from "effect/Unify" -import * as Array from "../../../Array.ts" -import type { NonEmptyReadonlyArray } from "../../../Array.ts" -import * as Chunk from "../../../Chunk.ts" -import { NotFoundError } from "../../../client/errors.ts" -import * as Context from "../../../Context.ts" -import * as DataDependencies from "../../../DataDependencies.ts" -import * as Effect from "../../../Effect.ts" -import { flatMapOption } from "../../../Effect.ts" -import * as Option from "../../../Option.ts" -import * as S from "../../../Schema.ts" -import { type Codec, NonNegativeInt } from "../../../Schema.ts" -import * as SchemaAST from "../../../SchemaAST.ts" -import { setupRequestContextFromCurrent } from "../../../setupRequest.ts" -import { type FilterArgs, getContextMap, type PersistenceModelType, type StoreConfig, storeId, StoreMaker } from "../../../Store.ts" -import type { FilterResult } from "../../filter/filterApi.ts" -import type { FieldValues } from "../../filter/types.ts" -import * as Q from "../../query.ts" -import { repositoryDependencyPaths } from "../dependency.ts" -import type { ChangeFeed, ChangeFeedEvent, Repository } from "../service.ts" -import { ValidationError, ValidationResult } from "../validation.ts" - -const dedupe = Array.dedupeWith(Equivalence.String) - -// ms buckets: dense under 100ms (common path), then mid-tail and multi-second stalls. -// Rare 0.5–1s+ encodes (fat aggregates) must not collapse into a single overflow bin. -const schemaDurationBoundaries = [ - 0.1, - 0.5, - 1, - 2, - 5, - 10, - 25, - 50, - 100, - 150, - 200, - 300, - 500, - 750, - 1_000, - 1_500, - 2_000, - 5_000 -] -// Event-loop-relevant stall floor: encodes/decodes above this are rare but block Node. -const SCHEMA_SLOW_MS = 100 -const schemaDecodeDuration = Metric.histogram("app.schema.decode.duration", { boundaries: schemaDurationBoundaries }) -const schemaEncodeDuration = Metric.histogram("app.schema.encode.duration", { boundaries: schemaDurationBoundaries }) -const schemaItemCount = Metric.histogram("app.schema.item_count", { - boundaries: [0, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1_000, 5_000] -}) -const schemaSlow = Metric.counter("app.schema.slow", { - description: `Repository schema encode/decode slower than ${SCHEMA_SLOW_MS}ms`, - incremental: true -}) - -const entityStateFromItems = (items: readonly unknown[]): string | undefined => { - const first = items[0] - if (first === null || typeof first !== "object" || !("_tag" in first)) return undefined - const tag = (first as { readonly _tag: unknown })._tag - return typeof tag === "string" ? tag : undefined -} - -const timeSchema = ( - operation: "decode" | "encode", - entity: string, - queryMode: "aggregate" | "collect" | "project" | "transform" | undefined, - itemCount: number, - entityState?: string -) => -(self: Effect.Effect): Effect.Effect => - Effect.clockWith((clock) => { - const startedAt = clock.currentTimeNanosUnsafe() - const attributes = { - "app.entity": entity, - "app.schema.operation": operation, - ...(queryMode !== undefined && { "app.query.mode": queryMode }), - ...(entityState !== undefined && { "app.entity.state": entityState }) - } - return Effect.onExit(self, () => { - const durationMs = Number(clock.currentTimeNanosUnsafe() - startedAt) / 1_000_000 - const slow = durationMs >= SCHEMA_SLOW_MS - return Effect.all([ - Effect.annotateCurrentSpan({ - [`app.schema.${operation}.duration_ms`]: durationMs, - "app.schema.item_count": itemCount, - "app.schema.slow": slow, - ...(queryMode !== undefined && { "app.query.mode": queryMode }), - ...(entityState !== undefined && { "app.entity.state": entityState }) - }), - Metric.update( - Metric.withAttributes( - operation === "decode" ? schemaDecodeDuration : schemaEncodeDuration, - attributes - ), - durationMs - ), - Metric.update(Metric.withAttributes(schemaItemCount, attributes), itemCount), - ...(slow - ? [Metric.update(Metric.withAttributes(schemaSlow, attributes), 1)] as const - : []) - ], { discard: true }) - }) - }) - -/** - * A base implementation to create a repository. - */ -export function makeRepoInternal< - Evt = never ->() { - return < - ItemType extends string, - R, - Encoded extends FieldValues, - T, - IdKey extends keyof T & keyof Encoded - >( - name: ItemType, - schema: S.Codec, - mapTo: (e: Encoded, etag: string | undefined) => PersistenceModelType, - idKey: IdKey - ) => { - type PM = PersistenceModelType - function mapToPersistenceModel( - e: Encoded, - getEtag: (id: string) => string | undefined - ): PM { - return mapTo(e, getEtag(e[idKey])) - } - - function mapReverse( - { _etag, ...e }: PM, - setEtag: (id: string, eTag: string | undefined) => void - ): Encoded { - setEtag((e as any)[idKey], _etag) - // the store already applied `jitM` and decoded JSON→Encoded - return e as unknown as Encoded - } - - const mkStore = makeStore()(name, schema, mapTo, idKey) - - function make( - args: [Evt] extends [never] ? { - schemaContext?: Context.Context - makeInitial?: Effect.Effect | undefined - dependencyIds?: (item: T) => NonEmptyReadonlyArray - additionalWriteDependencies?: (item: T) => ReadonlyArray - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - } - : { - schemaContext?: Context.Context - publishEvents: (evt: NonEmptyReadonlyArray) => Effect.Effect - makeInitial?: Effect.Effect | undefined - dependencyIds?: (item: T) => NonEmptyReadonlyArray - additionalWriteDependencies?: (item: T) => ReadonlyArray - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - } - ) { - return Effect - .gen(function*() { - const rctx: Context.Context = args.schemaContext ?? Context.empty() as any - const provideRctx = Effect.provide(rctx) - const encodeMany = (items: readonly T[]) => - S.encodeEffect(S.Array(schema))(items).pipe( - provideRctx, - timeSchema("encode", name, undefined, items.length, entityStateFromItems(items)) - ) - const decode = flow(S.decodeEffectConcurrently(schema), provideRctx) - const decodeMany = flow( - S.decodeEffectConcurrently(S.Array(schema)), - provideRctx - ) - - const store = yield* mkStore(args.makeInitial, args.config) - const dependencyPaths = repositoryDependencyPaths(schema) - const recordRead = DataDependencies.readRepo(name) - const entityDependency = (ids: NonEmptyReadonlyArray) => - DataDependencies.repo(name, [String(ids[0]), ...ids.slice(1).map(String)]) - const valuesAtPath = (value: unknown, path: string): readonly unknown[] => { - if (path.length === 0) return [value] - const [head, ...tail] = path.split(".") - if (head === undefined) return [value] - if (head === "-1") { - return globalThis.Array.isArray(value) - ? value.flatMap((item) => valuesAtPath(item, tail.join("."))) - : [] - } - return typeof value === "object" && value !== null - ? valuesAtPath((value as Record)[head], tail.join(".")) - : [] - } - const dependencyIds = (item: T): NonEmptyReadonlyArray => { - const configured = dependencyPaths - .flatMap((path) => valuesAtPath(item, String(path))) - .filter((value): value is string => typeof value === "string") ?? [] - const ids = args.dependencyIds?.(item) ?? [String(item[idKey]), ...configured] - return [ids[0], ...ids.slice(1)] - } - const itemDependencies = (item: T) => { - const ids = dependencyIds(item) - return [ - DataDependencies.repo(name, ids), - ...(args.additionalWriteDependencies?.(item) ?? []) - ] - } - const queryDependencyIds = (filter: readonly FilterResult[] | undefined): readonly string[] => { - if (!filter) return [] - const paths = new Set([String(idKey), ...dependencyPaths]) - const visit = (items: readonly FilterResult[]): readonly string[] => { - if (items.some((item) => item.t === "or" || item.t === "or-scope")) return [] - return items.flatMap((item) => { - if ("result" in item) return visit(item.result) - if (!paths.has(item.path)) return [] - const value: unknown = item.value - if (item.op === "eq" && typeof value === "string") return [value] - if (item.op === "in" && globalThis.Array.isArray(value)) { - return value.filter((entry): entry is string => typeof entry === "string") - } - return [] - }) - } - return [...new Set(visit(filter))] - } - const recordEntityRead = (id: T[IdKey]) => DataDependencies.read(entityDependency([id])) - const recordEntityWrite = (ids: NonEmptyReadonlyArray) => - DataDependencies.write(entityDependency(ids)) - const recordItemWrite = (items: NonEmptyReadonlyArray) => - Effect.forEach( - DataDependencies.merge(new Set(items.flatMap(itemDependencies))), - DataDependencies.write, - { discard: true } - ) - const cms = Effect.map(getContextMap.pipe(Effect.orDie), (_) => ({ - get: (id: string) => _.get(`${name}.${id}`), - set: (id: string, etag: string | undefined) => _.set(`${name}.${id}`, etag) - })) - - const pub = "publishEvents" in args - ? args.publishEvents - : () => Effect.void - - type ChangeFeedHandler = (evt: ChangeFeedEvent) => Effect.Effect - const changeFeedByNamespace = yield* Ref.make(HashMap.empty>()) - const changeFeedWildcard = yield* Ref.make(HashSet.empty()) - // clear all handlers when the repository's scope closes - yield* Effect.addFinalizer(() => - Effect.all([ - Ref.set(changeFeedByNamespace, HashMap.empty>()), - Ref.set(changeFeedWildcard, HashSet.empty()) - ], { discard: true }) - ) - const changeFeed: ChangeFeed = { - publish: ([items, op]) => - Effect.gen(function*() { - const ns = yield* storeId - const evt: ChangeFeedEvent = [items, op, ns] - const map = yield* Ref.get(changeFeedByNamespace) - const wild = yield* Ref.get(changeFeedWildcard) - const targeted = Option.getOrElse(HashMap.get(map, ns), () => HashSet.empty()) - const all = HashSet.union(targeted, wild) - yield* Effect.forEach(all, (h) => h(evt), { concurrency: "unbounded", discard: true }) - }), - subscribe: (handler, options) => { - const ns = options?.namespace - if (ns === undefined) { - return Effect.acquireRelease( - Ref.update(changeFeedWildcard, HashSet.add(handler)), - () => Ref.update(changeFeedWildcard, HashSet.remove(handler)) - ) - } - return Effect.acquireRelease( - Ref.update(changeFeedByNamespace, (m) => { - const cur = Option.getOrElse(HashMap.get(m, ns), () => HashSet.empty()) - return HashMap.set(m, ns, HashSet.add(cur, handler)) - }), - () => - Ref.update(changeFeedByNamespace, (m) => { - const cur = Option.getOrElse(HashMap.get(m, ns), () => HashSet.empty()) - const next = HashSet.remove(cur, handler) - return HashSet.size(next) === 0 ? HashMap.remove(m, ns) : HashMap.set(m, ns, next) - }) - ) - } - } - - const allE = cms - .pipe(Effect.flatMap((cm) => Effect.map(store.all, (_) => _.map((_) => mapReverse(_, cm.set))))) - - const all = Effect - .flatMap( - allE, - (items) => - decodeMany(items).pipe( - Effect.orDie, - timeSchema("decode", name, undefined, items.length) - ) - ) - .pipe( - Effect.tap(() => recordRead), - Effect.map((_) => _ as T[]), - Effect.withSpan("Repository.all", { - kind: "client", - attributes: { "app.entity": name } - }, { captureStackTrace: false }) - ) - - const fieldsSchema = schema as unknown as { fields: any } - // assumes the id field never needs a service... - const i = ("fields" in fieldsSchema ? S.Struct(fieldsSchema["fields"]) as unknown as typeof schema : schema) - .pipe((_) => { - let ast = _.ast - if (ast._tag === "Declaration") ast = ast.typeParameters[0]! - - const pickIdFromAst = (a: SchemaAST.AST) => { - // Unwrap Declaration (e.g. TaggedClass) to get the underlying Objects AST - let inner = a - if (inner._tag === "Declaration") inner = inner.typeParameters[0]! - // Pick from the original AST to preserve the full encoding chain (e.g. decodeTo transformations). - // Using toEncoded would lose transformation info needed to encode Type -> Encoded. - if (SchemaAST.isObjects(inner)) { - const field = inner.propertySignatures.find((_) => _.name === idKey) - if (field) { - return S.Struct({ [idKey]: S.make(field.type) }) as unknown as Codec - } - } - return S.make(a) as unknown as Codec - } - - return ast._tag === "Union" - // we need to get the Objects (TypeLiteral), in case of class it has encoding chain... - ? S.Union( - ast.types.map((_) => pickIdFromAst(_)) - ) - : pickIdFromAst(ast) - }) - const encodeId = flow(S.encodeEffect(i), provideRctx) - const encodeIdOnly = (id: string) => - encodeId({ [idKey]: id } as any).pipe( - Effect.map((_: Record) => _[idKey as string] as Encoded[IdKey]) - ) - const findEId = Effect.fnUntraced(function*(id: Encoded[IdKey]) { - yield* Effect.annotateCurrentSpan({ "app.entity.id": id }) - - return yield* Effect.flatMap( - store.find(id), - (item) => - Effect.gen(function*() { - const { set } = yield* cms - return item.pipe(Option.map((_) => mapReverse(_, set))) - }) - ) - }) - // TODO: select the particular field, instead of as struct - const findE = Effect.fnUntraced(function*(id: T[IdKey]) { - yield* Effect.annotateCurrentSpan({ "app.entity.id": id }) - - return yield* pipe( - encodeId({ [idKey]: id } as any), - Effect.orDie, - Effect.map((_) => (_ as any)[idKey]), - Effect.flatMap(findEId) - ) - }) - - const loadExistingItems = (ids: ReadonlyArray) => - Effect - .forEach( - ids, - (id) => - findE(id).pipe( - Effect.flatMap(Option.match({ - onNone: () => Effect.succeed(Option.none()), - onSome: (encoded) => decode(encoded).pipe(Effect.orDie, Effect.map(Option.some)) - })) - ) - ) - .pipe(Effect.map(Array.getSomes)) - - const find = Effect.fn("Repository.find", { - kind: "client", - attributes: { "app.entity": name } - })(function*(id: T[IdKey]) { - yield* recordEntityRead(id) - yield* Effect.annotateCurrentSpan({ "app.entity.id": id }) - return yield* flatMapOption(findE(id), (_) => Effect.orDie(decode(_))) - }) - - const saveAllE = (a: Iterable) => - flatMapOption( - Effect - .sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)([...a])), - (a) => - Effect.gen(function*() { - const { get, set } = yield* cms - const items = a.map((_) => mapToPersistenceModel(_, get)) - const ret = yield* store.batchSet(items) - ret.forEach((_) => set(_[idKey], _._etag)) - }) - ) - .pipe(Effect.asVoid) - - const saveAll = (a: Iterable) => - encodeMany(Array.fromIterable(a)) - .pipe( - Effect.orDie, - Effect.andThen(saveAllE) - ) - - const saveAndPublish = Effect.fn("Repository.saveAndPublish", { attributes: { "app.entity": name } })( - function*(items: Iterable, events: Iterable = []) { - const it = Chunk.fromIterable(items) - if (Chunk.isNonEmpty(it)) { - const values = Chunk.toReadonlyArray(it) - const previous = args.additionalWriteDependencies || args.dependencyIds || dependencyPaths.length > 0 - ? yield* loadExistingItems(values.map((item) => item[idKey])) - : [] - yield* recordItemWrite([values[0], ...values.slice(1), ...previous]) - } - const evts = [...events] - yield* Effect.annotateCurrentSpan({ - "app.entity.ids": Chunk.map(it, (_) => _[idKey]), - "app.event.count": evts.length - }) - return yield* saveAll(it) - .pipe( - Effect.andThen(Effect.sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(evts))), - // TODO: for full consistency the events should be stored within the same database transaction, and then picked up. - (_) => flatMapOption(_, pub), - Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"] as [T[], "save" | "remove"])), - Effect.asVoid - ) - } - ) - - const removeAndPublish = Effect.fn("Repository.removeAndPublish", { attributes: { "app.entity": name } })( - function*(a: Iterable, events: Iterable = []) { - const { set } = yield* cms - const it = [...a] - if (Array.isReadonlyArrayNonEmpty(it)) { - yield* recordItemWrite(it) - } - const evts = [...events] - yield* Effect.annotateCurrentSpan({ - "app.entity.ids": it.map((_) => _[idKey]), - "app.event.count": evts.length - }) - const items = yield* encodeMany(it).pipe(Effect.orDie) - if (Array.isReadonlyArrayNonEmpty(items)) { - yield* store.batchRemove( - items.map((_) => (_[idKey])), - args.config?.partitionValue?.(items[0]) - ) - for (const e of items) { - set(e[idKey], undefined) - } - yield* Effect - .sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(evts)) - // TODO: for full consistency the events should be stored within the same database transaction, and then picked up. - .pipe((_) => flatMapOption(_, pub)) - - yield* changeFeed.publish([it, "remove"] as [T[], "save" | "remove"]) - } - } - ) - - const removeById = Effect.fn("Repository.removeById", { attributes: { "app.entity": name } })( - function*(idOrIds: T[IdKey] | ReadonlyArray) { - const ids = globalThis.Array.isArray(idOrIds) - ? idOrIds as readonly T[IdKey][] - : [idOrIds as T[IdKey]] - if (!Array.isReadonlyArrayNonEmpty(ids)) { - return - } - yield* recordEntityWrite(ids) - if (args.additionalWriteDependencies || args.dependencyIds || dependencyPaths.length > 0) { - const previous = yield* loadExistingItems(ids) - if (Array.isReadonlyArrayNonEmpty(previous)) yield* recordItemWrite(previous) - } - const { set } = yield* cms - const eids = yield* Effect.forEach(ids, (_) => encodeIdOnly(_ as any)).pipe(Effect.orDie) - yield* Effect.annotateCurrentSpan({ "app.entity.ids": eids }) - yield* store.batchRemove(eids) - for (const id of eids) { - set(id, undefined) - } - yield* changeFeed.publish([[], "remove"] as [T[], "save" | "remove"]) - } - ) - - const parseMany = Effect.fnUntraced( - function*(items: readonly PM[]) { - const cm = yield* cms - return yield* decodeMany(items.map((_) => mapReverse(_, cm.set))).pipe( - Effect.orDie, - timeSchema("decode", name, "transform", items.length) - ) - } - ) - const decodeManyCache = new WeakMap< - S.Codec, - (i: readonly any[]) => Effect.Effect - >() - const getDecodeMany = (s: S.Codec) => { - let dec = decodeManyCache.get(s) - if (!dec) { - dec = S.decodeEffectConcurrently(S.Array(s)) - decodeManyCache.set(s, dec) - } - return dec - } - const parseMany2 = Effect.fnUntraced( - function*(items: readonly PM[], schema: S.Codec) { - const cm = yield* cms - return yield* getDecodeMany(schema)(items.map((_) => mapReverse(_, cm.set))).pipe( - Effect.orDie, - timeSchema("decode", name, "transform", items.length) - ) - } - ) - const filter = (args: FilterArgs) => - store - .filter( - // always enforce id and _etag because they are system fields, required for etag tracking etc - { - ...args, - select: args.select - ? dedupe([...args.select, idKey, "_etag" as any]) - : undefined - } as typeof args - ) - .pipe( - Effect.tap((items) => - Effect.map(cms, ({ set }) => items.forEach((_) => set((_ as Encoded)[idKey], (_ as PM)._etag))) - ) - ) - - // TODO: For raw we should use S.from, and drop the R... - const query: { - ( - q: Q.QueryProjection - ): Effect.Effect> - ( - q: Q.QAll, NoInfer, A, R> - ): Effect.Effect> - } = ((q: Q.QAll) => { - const a = Q.toFilter(q, schema) - const scopedIds = queryDependencyIds(a.filter) - const recordQueryRead = Array.isReadonlyArrayNonEmpty(scopedIds) - ? DataDependencies.read(DataDependencies.repo(name, scopedIds)) - : recordRead - // Mode dispatch — see `Q.project` JSDoc for the contract: - // aggregate: GROUP BY + aggregate functions at DB level; decode raw rows with schema; SchemaError surfaces. - // project : decode raw encoded rows with schema; no PM reverse-mapping; SchemaError surfaces. - // collect : same as project, but schema yields Option and None rows are dropped. - // transform: PM reverse-map (re-inject _etag/PM state from cms cache) then decode; orDie. - const eff = a.mode === "aggregate" - ? store - // `a.select` contains `{ key, aggregate }` items not expressible in FilterFunc's - // `U extends keyof Encoded` generic. Cast is unavoidable until FilterFunc supports aggregate mode. - .filter(a as any) - // Decode raw aggregate rows directly — no PM reverse-mapping, no id/_etag needed. - .pipe( - Effect.andThen( - (items) => - S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema ?? schema)))(items).pipe( - provideRctx, - timeSchema("decode", name, "aggregate", items.length) - ) - ) - ) - : a.mode === "project" - ? filter(a) - // TODO: mapFrom but need to support per field and dependencies - .pipe( - Effect.andThen( - (items) => - S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema ?? schema)))(items).pipe( - provideRctx, - timeSchema("decode", name, "project", items.length) - ) - ) - ) - : a.mode === "collect" - ? filter(a) - // TODO: mapFrom but need to support per field and dependencies - .pipe( - Effect.flatMap((items) => - S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema)))(items).pipe( - Effect.map(Array.getSomes), - provideRctx, - timeSchema("decode", name, "collect", items.length) - ) - ) - ) - : Effect.flatMap( - filter(a), - (_) => - Unify.unify( - a.schema - // TODO: partial may not match? - ? parseMany2(_ as any, a.schema as any) - : parseMany(_ as any) - ) - ) - return pipe( - a.ttype === "one" - ? Effect.flatMap( - eff, - flow( - Array.head, - Option.match({ - onNone: () => Effect.fail(new NotFoundError({ id: "query", /* TODO */ type: name })), - onSome: Effect.succeed - }) - ) - ) - : a.ttype === "count" - ? Effect - .map(eff, (_) => NonNegativeInt(_.length)) - .pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))) - : eff, - Effect.tap((r) => - Effect.annotateCurrentSpan({ - "app.query.ttype": a.ttype, - "app.query.mode": a.mode, - "db.response.returned_rows": Array.isArray(r) ? r.length : 1 - }) - ), - Effect.tap(() => recordQueryRead), - Effect.withSpan("Repository.query", { - kind: "client", - attributes: { "app.entity": name } - }, { captureStackTrace: false }) - ) - }) as any - - const validateSample = Effect.fn("Repository.validateSample", { attributes: { "app.entity": name } })( - function*(options?: { - percentage?: number - maxItems?: number - }) { - const percentage = options?.percentage ?? 0.1 // default 10% - const maxItems = options?.maxItems - - // 1. get all IDs with projection (bypasses main schema decode) - const allIds = yield* store - .filter({ - t: null as unknown as Encoded, - select: [idKey as keyof Encoded] - }) - .pipe( - // a projection never decodes a whole document, so the store's - // document decode cannot fail here - Effect.catchTag("SchemaError", (e) => Effect.die(e)), - Effect.withSpan("Repository.filter", { - kind: "client", - attributes: { "app.entity": name } - }, { captureStackTrace: false }) - ) - - // 2. random subset - const shuffled = [...allIds].sort(() => Math.random() - 0.5) - const sampleSize = Math.min( - maxItems ?? Infinity, - Math.ceil(allIds.length * percentage) - ) - const sample = shuffled.slice(0, sampleSize) - - // 3. validate each item - const errors: ValidationError[] = [] - - for (const item of sample) { - const id = item[idKey] - const stored = yield* store.find(id).pipe( - Effect.result, - Effect.withSpan("Repository.find", { - kind: "client", - attributes: { "app.entity": name, "app.entity.id": id } - }, { captureStackTrace: false }) - ) - - // a store-boundary failure: `defaultValues` + `jitM` did not - // produce a document the Encoded shape accepts, so the store - // failed with `S.SchemaError` instead of returning one. Report - // that document and keep sampling - it used to kill the run. - if (Result.isFailure(stored)) { - const failure = stored.failure - if (!S.isSchemaError(failure)) return yield* Effect.fail(failure) - errors.push( - ValidationError.make({ - id, - // the store failed before it could hand back a document - rawData: undefined, - jitMResult: undefined, - error: failure - }) - ) - continue - } - - const rawResult = stored.success - if (Option.isNone(rawResult)) continue - - // the store merged `defaultValues`, applied `jitM` on the raw - // JSON document and decoded JSON→Encoded, so this is exactly - // what the repository decodes into the domain type. - const rawData = rawResult.value as Encoded - - // the repository's own decode of a document the store accepted - const decodeResult = yield* S.decodeEffectConcurrently(schema)(rawData).pipe( - Effect.result, - provideRctx - ) - - if (Result.isFailure(decodeResult)) { - errors.push( - ValidationError.make({ - id, - rawData, - jitMResult: rawData, - error: decodeResult.failure - }) - ) - } - } - - return ValidationResult.make({ - total: NonNegativeInt(allIds.length), - sampled: NonNegativeInt(sample.length), - valid: NonNegativeInt(sample.length - errors.length), - errors - }) - } - ) - - // The store fails with `S.SchemaError` when a stored document does not - // decode - see `Store.all` / `Store.find`. The internals above let that - // failure propagate; producing the public members is the single - // boundary where it becomes a defect, because these signatures declare - // only `DatabaseError`. `queryRaw`, `query` and `mapped` already - // declare `S.SchemaError`, and `validateSample` reports it per - // document instead of dying. - const r = { - changeFeed, - itemType: name, - idKey, - find: (id: T[IdKey]) => find(id).pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))), - all: all.pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))), - saveAndPublish: (items: Iterable, events?: Iterable) => - saveAndPublish(items, events).pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))), - removeAndPublish, - removeById: (idOrIds: T[IdKey] | ReadonlyArray) => - removeById(idOrIds).pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))), - seedNamespace: (namespace: string) => store.seedNamespace(namespace), - validateSample, - queryRaw(schema: S.Codec, q: Q.RawQuery) { - return store.queryRaw(q).pipe( - Effect.flatMap((items) => - S.decodeEffectConcurrently(S.Array(S.toCodecJson(schema)))(items as readonly S.Json[]).pipe( - timeSchema("decode", name, undefined, items.length) - ) - ), - Effect.tap(() => recordRead), - Effect.withSpan("Repository.queryRaw", { - kind: "client", - attributes: { "app.entity": name } - }, { captureStackTrace: false }) - ) - }, - query(q: any) { - // eslint-disable-next-line prefer-rest-params - return query(typeof q === "function" ? Pipeable.pipeArguments(Q.make(), arguments) : q) as any - }, - /** - * @internal - */ - mapped: (schema: S.Codec) => { - const dec = S.decodeEffectConcurrently(schema) - const encMany = S.encodeEffect(S.Array(schema)) - const decMany = S.decodeEffectConcurrently(S.Array(schema)) - const spanAttrs = { kind: "client" as const, attributes: { "app.entity": name } } - return { - all: allE.pipe( - Effect.flatMap((items) => - decMany(items).pipe( - timeSchema("decode", name, undefined, items.length) - ) - ), - Effect.tap(() => recordRead), - Effect.map((_) => _ as any[]), - Effect.withSpan("Repository.mapped.all", spanAttrs, { captureStackTrace: false }) - ), - find: (id: T[IdKey]) => - flatMapOption(findE(id), dec).pipe( - Effect.tap(() => recordEntityRead(id)), - Effect.withSpan("Repository.mapped.find", { - ...spanAttrs, - attributes: { ...spanAttrs.attributes, "app.entity.id": id } - }, { captureStackTrace: false }) - ), - // query: (q: any) => { - // const a = Q.toFilter(q) - - // return filter(a) - // .pipe( - // Effect.flatMap(decMany), - // Effect.map((_) => _ as any[]), - // Effect.withSpan("Repository.mapped.query [effect-app/infra]", { - // captureStackTrace: false, - // attributes: { - // "repository.model_name": name, - // query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter.build() } - // } - // }) - // ) - // }, - save: (...xes: any[]) => - Effect - .flatMap( - encMany(xes).pipe( - timeSchema("encode", name, undefined, xes.length, entityStateFromItems(xes)) - ), - (_) => saveAllE(_) - ) - .pipe( - Effect.tap(() => - Array.isReadonlyArrayNonEmpty(xes) - ? recordEntityWrite([xes[0][idKey], ...xes.slice(1).map((_) => _[idKey])]) - : Effect.void - ), - Effect.withSpan("Repository.mapped.save", spanAttrs, { captureStackTrace: false }) - ) - } - } - } - return r as Repository, RPublish, RCtx> - }) - .pipe(Effect - // .withSpan("Repository.make [effect-app/infra]", { attributes: { "repository.model_name": name } }) - .withLogSpan("Repository.make: " + name)) - } - - return { - make, - Q: Q.make() - } - } -} - -const pluralize = (s: string) => - s.endsWith("s") - ? s + "es" - : s.endsWith("y") - ? s.substring(0, s.length - 1) + "ies" - : s + "s" - -export function makeStore() { - return < - ItemType extends string, - R, - E, - T, - IdKey extends keyof Encoded - >( - name: ItemType, - schema: S.Codec, - mapTo: (e: E, etag: string | undefined) => Encoded, - idKey: IdKey - ) => { - function makeStore( - makeInitial?: Effect.Effect, - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - ) { - function encodeToEncoded() { - const getEtag = () => undefined - return (t: T) => - S.encodeEffect(schema)(t).pipe( - Effect.orDie, - Effect.map((_) => mapToPersistenceModel(_, getEtag)) - ) - } - - function mapToPersistenceModel( - e: E, - getEtag: (id: string) => string | undefined - ): Encoded { - return mapTo(e, getEtag((e as any)[idKey] as string)) - } - - return Effect.gen(function*() { - const { make } = yield* StoreMaker - - const store = yield* make( - pluralize(name), - idKey, - makeInitial - ? makeInitial - .pipe( - Effect.flatMap(Effect.forEach(encodeToEncoded())), - setupRequestContextFromCurrent("Repository.makeInitial [effect-app/infra]", { - attributes: { "app.entity": name } - }) - ) - : undefined, - { - ...config, - schema, - partitionValue: config?.partitionValue - ?? ((_) => "primary") /*(isIntegrationEvent(r) ? r.companyId : r.id*/ - } - ) - - return store - }) - } - - return makeStore - } -} - -export interface Repos< - T, - Encoded extends { id: string }, - RSchema, - Evt, - ItemType extends string, - IdKey extends keyof T, - RPublish -> { - make( - args: [Evt] extends [never] ? { - makeInitial?: Effect.Effect | undefined - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - } - : { - publishEvents: (evt: NonEmptyReadonlyArray) => Effect.Effect - makeInitial?: Effect.Effect | undefined - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - } - ): Effect.Effect< - Repository, - E, - StoreMaker | RInitial | R2 | Scope.Scope - > - makeWith( - args: [Evt] extends [never] ? { - makeInitial?: Effect.Effect | undefined - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - } - : { - publishEvents: (evt: NonEmptyReadonlyArray) => Effect.Effect - makeInitial?: Effect.Effect | undefined - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - }, - f: (r: Repository) => Out - ): Effect.Effect - readonly Q: ReturnType> - readonly type: Repository -} diff --git a/packages/effect-app/src/Model/Repository/legacy.ts b/packages/effect-app/src/Model/Repository/legacy.ts deleted file mode 100644 index bac20bb3e..000000000 --- a/packages/effect-app/src/Model/Repository/legacy.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import type { DatabaseError, OptimisticConcurrencyException } from "../../client/errors.ts" -import type * as Effect from "../../Effect.ts" -import type * as Option from "../../Option.ts" -import type * as S from "../../Schema.ts" - -export interface Mapped1 { - all: Effect.Effect - save: (...xes: readonly A[]) => Effect.Effect - find: (id: A[IdKey]) => Effect.Effect, S.SchemaError | DatabaseError, R> -} - -// TODO: auto use project, and select fields from the From side of schema only -export interface Mapped2 { - all: Effect.Effect -} - -export interface Mapped { - (schema: S.Codec): Mapped1 - // TODO: constrain on Encoded2 having to contain only fields that fit Encoded - (schema: S.Codec): Mapped2 -} - -export interface MM { - (schema: S.Codec): Effect.Effect, never, Repo> - // TODO: constrain on Encoded2 having to contain only fields that fit Encoded - (schema: S.Codec): Effect.Effect, never, Repo> -} diff --git a/packages/effect-app/src/Model/Repository/makeRepo.ts b/packages/effect-app/src/Model/Repository/makeRepo.ts deleted file mode 100644 index 30979b03b..000000000 --- a/packages/effect-app/src/Model/Repository/makeRepo.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-object-type */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -// import type { ParserEnv } from "../../Schema/custom/Parser.ts" - -import type * as Scope from "effect/Scope" -import type { NonEmptyReadonlyArray } from "../../Array.ts" -import type * as Context from "../../Context.ts" -import type * as DataDependencies from "../../DataDependencies.ts" -import * as Effect from "../../Effect.ts" -import type * as S from "../../Schema.ts" -import type { JsonRecord, StoreConfig, StoreMaker } from "../../Store.ts" -import type { FieldValues } from "../filter/types.ts" -import { type ExtendedRepository, extendRepo } from "./ext.ts" -import { makeRepoInternal } from "./internal/internal.ts" -import { RepositoryRegistry } from "./Registry.ts" -import type { Repository } from "./service.ts" - -export interface RepositoryOptions< - IdKey extends keyof T & keyof Encoded, - Encoded extends FieldValues, - T, - ItemType extends string, - Evt = never, - RPublish = never, - E = never, - RInitial = never, - RCtx = never, - RSchema = never -> { - /** - * Specify the idKey of the Type side, if it's different from the default "id". - * Does not change the Encoded side, which is always "id" to support database drivers. - * At this time as queries are operating on the Encoded side, the queries must still specify "id" regardless. - */ - idKey: IdKey - /** - * just in time Migration: for complex migrations that aren't just default simple values - * use the config.defaultValues instead for simple default values - * - * Runs at the **store boundary** on the raw JSON document, after - * `config.defaultValues` have been merged in and *before* the JSON→Encoded - * decode. It sees the document as stored - a `Date` field as an ISO string, - * a `ReadonlyMap` as an array of pairs, an explicit `null` as `null` - and - * must return JSON again, not native Encoded values. - * - * @see StoreConfig.jitM - */ - jitM?: (json: JsonRecord) => JsonRecord - config?: Omit, "partitionValue"> & { - partitionValue?: (e?: Encoded) => string - } - /** - * Optional handler to be able to publish events after successfull save. - */ - publishEvents?: (evt: NonEmptyReadonlyArray) => Effect.Effect - /** - * Optional creator for initial data in the table when it's created for the first itme. - */ - makeInitial?: Effect.Effect | undefined - /** - * Optional context to be provided to Schema decode/encode. - * Useful for effectful transformations like XWithItems, where items is a transformation retrieving elements from another database table or other source. - */ - schemaContext?: Context.Context - - /** IDs, including aliases, that identify an entity for dependency invalidation. */ - dependencyIds?: (item: T) => NonEmptyReadonlyArray - - /** - * Additional derived queries or resources affected by writing an item. - * Saves invalidate dependencies derived from both the previous and next item; - * removals derive them from the removed item. - */ - additionalWriteDependencies?: (item: T) => ReadonlyArray - - overrides?: ( - repo: Repository, RPublish, RCtx> - ) => Repository, RPublish, RCtx> -} - -/** - * Create a repository instance. - * @param itemType an identifier used for the table name and e.g NotFoundError - * @param schema the Schema used for this Repository - * @param options @see RepositoryOptions - * @returns a Repository - */ -export const makeRepo: { - < - ItemType extends string, - RSchema, - Encoded extends FieldValues, - T, - IdKey extends keyof T & keyof Encoded, - E = never, - Evt = never, - RInitial = never, - RPublish = never, - RCtx = never - >( - itemType: ItemType, - schema: S.Codec, - options: RepositoryOptions - ): Effect.Effect< - ExtendedRepository, RPublish, RCtx>, - E, - RInitial | StoreMaker | RepositoryRegistry | Scope.Scope - > - < - ItemType extends string, - RSchema, - Encoded extends FieldValues, - T extends { id: unknown }, - E = never, - Evt = never, - RInitial = never, - RPublish = never, - RCtx = never - >( - itemType: ItemType, - schema: S.Codec, - options: Omit, "idKey"> - ): Effect.Effect< - ExtendedRepository, RPublish, RCtx>, - E, - RInitial | StoreMaker | RepositoryRegistry | Scope.Scope - > -} = < - ItemType extends string, - RSchema, - Encoded extends FieldValues, - T, - IdKey extends keyof T & keyof Encoded, - E = never, - RInitial = never, - RPublish = never, - Evt = never, - RCtx = never ->( - itemType: ItemType, - schema: S.Codec, - options: Omit, "idKey"> & { - idKey?: IdKey - } -) => - Effect.gen(function*() { - const mkRepo = makeRepoInternal()( - itemType, - schema, - (e, _etag) => ({ ...e, _etag }), - options.idKey ?? "id" as any - ) - // `jitM` belongs to the store: it migrates the raw JSON document before the - // store decodes JSON→Encoded, so it travels as part of the store config. - const { jitM, ...rest } = options - let r = yield* mkRepo.make( - (jitM ? { ...rest, config: { ...rest.config, jitM } } : rest) as any - ) - if (options.overrides) r = options.overrides(r) - const repo = extendRepo(r) - const registry = yield* RepositoryRegistry - registry.register(itemType, repo) - return repo - }) diff --git a/packages/effect-app/src/Model/Repository/service.ts b/packages/effect-app/src/Model/Repository/service.ts deleted file mode 100644 index 875a0cd17..000000000 --- a/packages/effect-app/src/Model/Repository/service.ts +++ /dev/null @@ -1,676 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type * as Scope from "effect/Scope" -import type { DatabaseError, InvalidStateError, NotFoundError, OptimisticConcurrencyException } from "../../client/errors.ts" -import type * as Effect from "../../Effect.ts" -import type * as Option from "../../Option.ts" -import type * as S from "../../Schema.ts" -import type { NonNegativeInt } from "../../Schema/numbers.ts" -import type { FieldValues, IsNever, ResolveFirstLevel } from "../filter/types.ts" -import type { QAll, Query, QueryProjection, RawQuery } from "../query.ts" -import type { Mapped } from "./legacy.ts" -import type { ValidationResult } from "./validation.ts" - -/** - * Event emitted by a repository's ChangeFeed. - * - * Tuple shape: `[items, op, namespace]` — `namespace` is the `storeId` - * context value at publish time (defaults to `"primary"`). - */ -export type ChangeFeedEvent = readonly [items: T[], op: "save" | "remove", namespace: string] - -/** - * Synchronous broadcast channel for repository change events. - * - * `publish` only completes after every matching handler's Effect has - * finished — callers must await all handlers before continuing. Subscribers - * can target a single namespace via `options.namespace`, or omit it to - * receive events from every namespace (wildcard). - * - * Handlers are auto-removed when the subscriber's Scope closes; the full - * handler set is cleared when the repository's own scope closes. - */ -export interface ChangeFeed { - readonly publish: (evt: [T[], "save" | "remove"]) => Effect.Effect - readonly subscribe: ( - handler: (evt: ChangeFeedEvent) => Effect.Effect, - options?: { readonly namespace?: string } - ) => Effect.Effect -} - -export interface Repository< - T, - Encoded extends FieldValues, - Evt, - ItemType extends string, - IdKey extends keyof T, - RSchema, - RPublish, - RProvided = never -> { - readonly itemType: ItemType - readonly idKey: IdKey - readonly find: (id: T[IdKey]) => Effect.Effect, DatabaseError, RSchema> - readonly all: Effect.Effect - readonly saveAndPublish: ( - items: Iterable, - events?: Iterable - ) => Effect.Effect - readonly changeFeed: ChangeFeed - readonly removeAndPublish: ( - items: Iterable, - events?: Iterable - ) => Effect.Effect - - readonly removeById: ( - idOrIds: T[IdKey] | ReadonlyArray - ) => Effect.Effect - - readonly queryRaw: ( - schema: S.Codec, - raw: RawQuery - ) => Effect.Effect - - /** - * Explicitly seed a namespace. Primary is seeded eagerly on initialization. - * Non-primary namespaces must be seeded explicitly before use. - */ - readonly seedNamespace: (namespace: string) => Effect.Effect - - readonly query: { - // ending with projection - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - E extends boolean = false - >( - q: ( - initial: Query - ) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: ( - _: $A - ) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: ( - _: $B - ) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - $C = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: ( - _: $C - ) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - $C = never, - $D = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: ( - _: $D - ) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: (_: $F) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - $G = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: (_: $F) => $G, - q8: (_: $G) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - $G = never, - $H = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: (_: $F) => $G, - q8: (_: $G) => $H, - q9: (_: $H) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - < - A, - R, - From extends FieldValues, - TType extends "one" | "many" | "count" = "many", - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - $G = never, - $H = never, - $I = never, - E extends boolean = false - >( - q1: ( - initial: Query - ) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: (_: $F) => $G, - q8: (_: $G) => $H, - q9: (_: $H) => $I, - q10: (_: $I) => QueryProjection - ): Effect.Effect< - TType extends "many" ? readonly A[] : TType extends "count" ? NonNegativeInt : A, - | (TType extends "many" ? never : NotFoundError) - | (TType extends "count" ? never : S.SchemaError) - | DatabaseError, - Exclude | RSchema - > - - // ending with generic query - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - E extends boolean = false - >( - q: (initial: Query) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: ( - _: $A - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: ( - _: $A - ) => $B, - q3: ( - _: $B - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - $C = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: ( - _: $C - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - $C = never, - $D = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: ( - _: $D - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: ( - _: $E - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: ( - _: $F - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - $G = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: (_: $F) => $G, - q8: ( - _: $G - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray - : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - $G = never, - $H = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: (_: $F) => $G, - q8: (_: $G) => $H, - q9: ( - _: $H - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray - : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - < - R = never, - TType extends "one" | "many" = "many", - EncodedRefined extends Encoded = Encoded, - $A = never, - $B = never, - $C = never, - $D = never, - $E = never, - $F = never, - $G = never, - $H = never, - $I = never, - E extends boolean = false - >( - q1: (initial: Query) => $A, - q2: (_: $A) => $B, - q3: (_: $B) => $C, - q4: (_: $C) => $D, - q5: (_: $D) => $E, - q6: (_: $E) => $F, - q7: (_: $F) => $G, - q8: (_: $G) => $H, - q9: (_: $H) => $I, - q10: ( - _: $I - ) => QAll, R, TType, E> - ): Effect.Effect< - TType extends "many" ? DistributeQueryIfExclusiveOnArray - : RefineTHelper, - (TType extends "many" ? never : NotFoundError) | DatabaseError, - Exclude | RSchema - > - } - - /** @deprecated use query */ - readonly mapped: Mapped - - /** - * Validates a random sample of repository items by applying jitM and schema decode. - * Useful for testing that migrations and schema changes work correctly on existing data. - */ - readonly validateSample: (options?: { - /** percentage of items to sample (0.0-1.0), default 0.1 (10%) */ - percentage?: number - /** optional maximum number of items to sample */ - maxItems?: number - }) => Effect.Effect -} - -type DistributeQueryIfExclusiveOnArray = [Exclusive] extends [true] - ? T extends any ? RefineTHelper extends infer R ? IsNever extends true ? never - : readonly R[] - : never - : never - : readonly RefineTHelper[] - -type NullableRefined = { - // EncodedRefined may be a union, so if you just keyof you'll get just common keys - // p.s. NullableRefined is homomorphic in T so it distributes itself over T - [k in keyof T]: [null] extends [T[k]] ? [null] extends [Extract[k]] ? T[k] - : Exclude - : T[k] -} - -// for DX purposes -type ShouldRecursiveExtractTagged = true extends { - [k in keyof T]: "_tag" extends keyof T[k] - ? k extends keyof EncodedRefined ? "_tag" extends keyof EncodedRefined[k] ? true - : false - : false - : false -}[keyof T] ? true - : false - -// support is not 100% but we have to make compromises -type RecursiveExtractTagged = ShouldRecursiveExtractTagged extends true ? { - [k in keyof T]: "_tag" extends keyof T[k] - ? k extends keyof EncodedRefined ? "_tag" extends keyof EncodedRefined[k] ? ExtractTagged - : T[k] - : T[k] - : T[k] - } - : T - -type ExtractTagged_ = EncodedRefined extends { _tag: string } - ? T extends { _tag: string } ? Extract - : T - : T - -type ExtractTagged = ExtractTagged_ extends infer R - ? RecursiveExtractTagged, EncodedRefined> - : never - -type ShouldRecursiveExtractIded = true extends { - [k in keyof T]: "id" extends keyof T[k] ? k extends keyof EncodedRefined ? "id" extends keyof EncodedRefined[k] ? true - : false - : false - : false -}[keyof T] ? true - : false - -type RecusiveExtractIded = ShouldRecursiveExtractIded extends true ? { - [k in keyof T]: "id" extends keyof T[k] - ? k extends keyof EncodedRefined ? "id" extends keyof EncodedRefined[k] ? ExtractIded - : T[k] - : T[k] - : T[k] - } - : T - -type ExtractIded_ = EncodedRefined extends { id: string } - ? T extends { id: string } ? Extract - : T - : T - -type ExtractIded = ExtractIded_ extends infer R - ? RecusiveExtractIded, EncodedRefined> - : never - -export type RefineTHelper = ResolveFirstLevel< - NullableRefined< - ExtractIded, EncodedRefined>, - EncodedRefined - > -> diff --git a/packages/effect-app/src/Model/Repository/validation.ts b/packages/effect-app/src/Model/Repository/validation.ts deleted file mode 100644 index 25b34b058..000000000 --- a/packages/effect-app/src/Model/Repository/validation.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as S from "../../Schema.ts" -import { NonNegativeInt } from "../../Schema.ts" - -/** - * Represents a single validation error when decoding a repository item. - * Contains full context for debugging: the stored data and the decode error. - */ -export class ValidationError extends S.Opaque()(S.Struct({ - /** the id of the item that failed validation */ - id: S.Unknown, - /** - * the data as returned by the store: `defaultValues` merged, `jitM` applied - * to the raw JSON document, and JSON decoded to the Encoded shape - */ - rawData: S.Unknown, - /** - * @deprecated identical to {@link rawData}. `jitM` now runs inside the store, - * before the JSON→Encoded decode, so the repository never sees the document - * as it was before the migration. - */ - jitMResult: S.Unknown, - /** the S.SchemaError from schema decode */ - error: S.Unknown -})) {} - -/** - * Result of validating a sample of repository items. - */ -export class ValidationResult extends S.Opaque()(S.Struct({ - /** total number of items in the repository */ - total: NonNegativeInt, - /** number of items that were sampled for validation */ - sampled: NonNegativeInt, - /** number of items that passed validation */ - valid: NonNegativeInt, - /** list of validation errors with full context */ - errors: S.Array(ValidationError) -})) {} diff --git a/packages/effect-app/src/Model/dsl.ts b/packages/effect-app/src/Model/dsl.ts deleted file mode 100644 index 0cfef0a68..000000000 --- a/packages/effect-app/src/Model/dsl.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { isReadonlyArrayNonEmpty, type NonEmptyReadonlyArray } from "../Array.ts" -import * as Effect from "../Effect.ts" -import * as Option from "../Option.ts" -import { type FixEnv, get, logMany, type Pure, type PureEnvEnv, type PureLogT, set } from "../Pure.ts" - -export interface PureDSL { - get: Pure - set: (s: S2) => Pure - log: (...w: W[]) => PureLogT -} - -export const AnyPureDSL: PureDSL = { - get: get(), - set: set as any, - log: (...evt: any[]) => logMany(evt) -} - -const anyDSL = makeDSL() - -export type AllDSL = - & (( - pure: (dsl: PureDSL) => Effect.Effect - ) => Effect.Effect>) - & AllDSLExt - -export interface AllDSLExt { - modify: ( - pure: (items: readonly S1[], dsl: PureDSL) => Effect.Effect - ) => Effect.Effect> - update: ( - pure: (items: readonly S1[], log: (...evt: Evt[]) => PureLogT) => Effect.Effect - ) => Effect.Effect> - updateWith: ( - upd: (item: readonly S1[]) => readonly S2[] - ) => Effect.Effect> -} - -export function makeAllDSL() { - const dsl: AllDSL = anyDSL - return dsl -} - -export type OneDSL = - & (( - pure: (dsl: PureDSL) => Effect.Effect - ) => Effect.Effect>) - & OneDSLExt - -export interface OneDSLExt { - modify: ( - pure: (items: S1, dsl: PureDSL) => Effect.Effect - ) => Effect.Effect | PureEnvEnv> - update: ( - pure: (items: S1, log: (...evt: Evt[]) => PureLogT) => Effect.Effect - ) => Effect.Effect> - updateWith: ( - upd: (item: S1) => S2 - ) => Effect.Effect> -} - -export function makeOneDSL(): OneDSL { - return anyDSL -} - -export function makeDSL() { - const dsl: PureDSL = AnyPureDSL - - function modify< - R, - E, - A - >( - pure: ( - items: S1, - dsl: PureDSL - ) => Effect.Effect - ): Effect.Effect> { - return dsl.get.pipe(Effect.flatMap((items) => pure(items, dsl))) as any - } - - function update< - R, - E - >( - pure: ( - items: S1, - log: (...evt: Evt[]) => PureLogT - ) => Effect.Effect - ): Effect.Effect> { - return dsl.get.pipe(Effect.flatMap((items) => pure(items, dsl.log).pipe(Effect.tap(dsl.set)))) as any - } - - function withDSL< - R, - A, - E - >( - pure: (dsl: PureDSL) => Effect.Effect - ): Effect.Effect> { - return pure(AnyPureDSL) as any - } - - function updateWith(upd: (item: S1) => S2) { - return update((_: S1) => Effect.sync(() => upd(_))) - } - - return Object.assign( - withDSL, - { - modify, - update, - updateWith - } - ) -} - -export interface DSLExt extends ReturnType> {} - -export function ifAny(fn: (items: NonEmptyReadonlyArray) => Effect.Effect) { - return (items: Iterable) => - Effect.flatMapOption(Effect.sync(() => Option.liftPredicate(isReadonlyArrayNonEmpty)([...items])), fn) -} - -export function ifAny_( - items: Iterable, - fn: (items: NonEmptyReadonlyArray) => Effect.Effect -) { - return Effect.flatMapOption(Effect.sync(() => Option.liftPredicate(isReadonlyArrayNonEmpty)([...items])), fn) -} diff --git a/packages/effect-app/src/Model/filter/filterApi.ts b/packages/effect-app/src/Model/filter/filterApi.ts deleted file mode 100644 index 81c5eb250..000000000 --- a/packages/effect-app/src/Model/filter/filterApi.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { type RelationDirection } from "../query.ts" - -export type InOps = - | "in" - | "notIn" - -export type OtherOps = - | "endsWith" - | "startsWith" - | "notEndsWith" - | "notStartsWith" - | "contains" - | "notContains" - | "includes" - | "notIncludes" - | "includes-any" - | "notIncludes-any" - | "includes-all" - | "notIncludes-all" - | "hasKey" - | "notHasKey" - | "hasValue" - | "notHasValue" - | "hasKeyValue" - | "notHasKeyValue" - | "hasKey-any" - | "notHasKey-any" - | "hasKey-all" - | "notHasKey-all" - | "hasValue-any" - | "notHasValue-any" - | "hasValue-all" - | "notHasValue-all" - | "hasKeyValue-any" - | "notHasKeyValue-any" - | "hasKeyValue-all" - | "notHasKeyValue-all" - | "eq" - | "neq" - | "gt" - | "gte" - | "lt" - | "lte" - -export type Ops = OtherOps | InOps - -export type FilterScopes = { - t: "or-scope" - result: readonly FilterResult[] - relation: RelationDirection -} | { - t: "and-scope" - result: readonly FilterResult[] - relation: RelationDirection -} | { - t: "where-scope" - result: readonly FilterResult[] - relation: RelationDirection -} - -export type FilterR = { - op: Ops - - path: string - value: unknown -} - -export type FilterResult = - | { - t: "where" - } & FilterR - | { - t: "or" - } & FilterR - | { - t: "and" - } & FilterR - | FilterScopes diff --git a/packages/effect-app/src/Model/filter/types.ts b/packages/effect-app/src/Model/filter/types.ts deleted file mode 100644 index d4cca62cf..000000000 --- a/packages/effect-app/src/Model/filter/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -// codegen:start {preset: barrel, include: ./types/*.ts} -export * from "./types/errors.ts" -export * from "./types/fields.ts" -export * from "./types/utils.ts" -export * from "./types/validator.ts" -// codegen:end diff --git a/packages/effect-app/src/Model/filter/types/errors.ts b/packages/effect-app/src/Model/filter/types/errors.ts deleted file mode 100644 index f6501cb94..000000000 --- a/packages/effect-app/src/Model/filter/types/errors.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { FieldValues, InternalFieldName } from "./fields.ts" -import type { BrowserNativeObject, IsAny, LiteralUnion, Merge } from "./utils.ts" -import type { RegisterOptions, ValidateResult } from "./validator.ts" - -export type Message = string - -export type MultipleFieldErrors = - & { - [K in keyof RegisterOptions]?: ValidateResult - } - & { - [key: string]: ValidateResult - } - -export type FieldError = { - type: LiteralUnion - root?: FieldError - // ref?: Ref - types?: MultipleFieldErrors - message?: Message -} - -export type ErrorOption = { - message?: Message - type?: LiteralUnion - types?: MultipleFieldErrors -} - -export type DeepRequired = T extends BrowserNativeObject | Blob ? T - : { - [K in keyof T]-?: NonNullable> - } - -export type FieldErrorsImpl = { - [K in keyof T]?: T[K] extends BrowserNativeObject | Blob ? FieldError - : T[K] extends object ? Merge> - : FieldError -} - -export type FieldErrors = Partial< - FieldValues extends IsAny ? any - : FieldErrorsImpl> -> - -export type InternalFieldErrors = Partial< - Record -> diff --git a/packages/effect-app/src/Model/filter/types/fields.ts b/packages/effect-app/src/Model/filter/types/fields.ts deleted file mode 100644 index 73808a451..000000000 --- a/packages/effect-app/src/Model/filter/types/fields.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { IsFlatObject } from "./utils.ts" -import type { RegisterOptions } from "./validator.ts" - -export type InternalFieldName = string - -export type FieldName = IsFlatObject extends true - ? Extract - : string - -// export type CustomElement = { -// name: FieldName -// type?: string -// value?: any -// disabled?: boolean -// checked?: boolean -// options?: HTMLOptionsCollection -// files?: FileList | null -// focus?: Noop -// } - -export type FieldValue = TFieldValues[InternalFieldName] - -export type FieldValues = Record - -export type NativeFieldValue = - | string - | number - | boolean - | null - | undefined - | unknown[] - -// export type FieldElement = -// | HTMLInputElement -// | HTMLSelectElement -// | HTMLTextAreaElement -// | CustomElement - -// export type Ref = FieldElement - -export type Field = { - _f: { - // ref: Ref - name: InternalFieldName - // refs?: HTMLInputElement[] - mount?: boolean - } & RegisterOptions -} - -export type FieldRefs = Partial> diff --git a/packages/effect-app/src/Model/filter/types/path/common.ts b/packages/effect-app/src/Model/filter/types/path/common.ts deleted file mode 100644 index 40324c0d0..000000000 --- a/packages/effect-app/src/Model/filter/types/path/common.ts +++ /dev/null @@ -1,404 +0,0 @@ -import type { IsAny, IsNever } from "../utils.ts" - -/** - * Type alias to `string` which describes a lodash-like path through an object. - * E.g. `'foo.bar.0.baz'` - */ -export type PathString = string - -/** - * Type which can be traversed through with a {@link PathString}. - * I.e. objects, arrays, and tuples - */ -export type Traversable = object - -/** - * Type to query whether an array type T is a tuple type. - * @typeParam T - type which may be an array or tuple - * @example - * ``` - * IsTuple<[number]> = true - * IsTuple = false - * ``` - */ -export type IsTuple> = number extends T["length"] ? false - : true - -/** - * Type which can be used to index an array or tuple type. - */ -export type ArrayKey = number - -/** - * Type which can be used to index an object. - */ -export type Key = string - -/** - * Type to assert that a type is a {@link Key}. - * @typeParam T - type which may be a {@link Key} - */ -export type AsKey = Extract - -/** - * Type to convert a type to a {@link Key}. - * @typeParam T - type which may be converted to a {@link Key} - */ -export type ToKey = T extends ArrayKey ? `${T}` : AsKey - -/** - * Type which describes a path through an object - * as a list of individual {@link Key}s. - */ -export type PathTuple = Key[] - -/** - * Type to assert that a type is a {@link PathTuple}. - * @typeParam T - type which may be a {@link PathTuple} - */ -export type AsPathTuple = Extract - -/** - * Type to intersect a union type. - * See https://fettblog.eu/typescript-union-to-intersection/ - * @typeParam U - union - * @example - * ``` - * UnionToIntersection<{ foo: string } | { bar: number }> - * = { foo: string; bar: number } - * ``` - */ -export type UnionToIntersection = ( - U extends any ? (_: U) => any : never -) extends (_: infer I) => any ? I - : never - -/** - * Type which appends a {@link Key} to the {@link PathTuple} only if it is not - * blank, i.e. not the empty string. - * @typeParam PT - path - * @typeParam K - key - * @example - * ``` - * AppendNonBlankKey<['foo'], 'bar'> = ['foo', 'bar'] - * AppendNonBlankKey<['foo'], ''> = ['foo'] - * ``` - */ -type AppendNonBlankKey = K extends "" ? PT - : [...PT, K] - -/** - * Type to implement {@link SplitPathString} tail recursively. - * @typeParam PS - remaining {@link PathString} which should be split into its - * individual {@link Key}s - * @typeParam PT - accumulator of the {@link Key}s which have been split from - * the original {@link PathString} already - */ -type SplitPathStringImpl< - PS extends PathString, - PT extends PathTuple -> = PS extends `${infer K}.${infer R}` ? SplitPathStringImpl> - : AppendNonBlankKey - -/** - * Type to split a {@link PathString} into a {@link PathTuple}. - * The individual {@link Key}s may be empty strings. - * @typeParam PS - {@link PathString} which should be split into its - * individual {@link Key}s - * @example - * ``` - * SplitPathString<'foo'> = ['foo'] - * SplitPathString<'foo.bar.0.baz'> = ['foo', 'bar', '0', 'baz'] - * SplitPathString<'.'> = [] - * ``` - */ -export type SplitPathString = SplitPathStringImpl< - PS, - [] -> - -/** - * Type to implement {@link JoinPathTuple} tail-recursively. - * @typeParam PT - remaining {@link Key}s which needs to be joined - * @typeParam PS - accumulator of the already joined {@link Key}s - */ -type JoinPathTupleImpl< - PT extends PathTuple, - PS extends PathString -> = PT extends [infer K, ...infer R] ? JoinPathTupleImpl, `${PS}.${AsKey}`> - : PS - -/** - * Type to join a {@link PathTuple} to a {@link PathString}. - * @typeParam PT - {@link PathTuple} which should be joined. - * @example - * ``` - * JoinPathTuple<['foo']> = 'foo' - * JoinPathTuple<['foo', 'bar', '0', 'baz']> = 'foo.bar.0.baz' - * JoinPathTuple<[]> = never - * ``` - */ -export type JoinPathTuple = PT extends [ - infer K, - ...infer R -] ? JoinPathTupleImpl, AsKey> - : never - -/** - * Type which converts all keys of an object to {@link Key}s. - * @typeParam T - object type - * @example - * ``` - * MapKeys<{0: string}> = {'0': string} - * ``` - */ -type MapKeys = { [K in keyof T as ToKey]: T[K] } - -/** - * Type to access a type by a key. - * - Returns undefined if it can't be indexed by that key. - * - Returns null if the type is null. - * - Returns undefined if the type is not traversable. - * @typeParam T - type which is indexed by the key - * @typeParam K - key into the type - * ``` - * TryAccess<{foo: string}, 'foo'> = string - * TryAccess<{foo: string}, 'bar'> = undefined - * TryAccess = null - * TryAccess = undefined - * ``` - */ -type TryAccess = K extends keyof T ? T[K] - : T extends null ? null - : undefined - -/** - * Type to access an array type by a key. - * Returns undefined if the key is non-numeric. - * @typeParam T - type which is indexed by the key - * @typeParam K - key into the type - * ``` - * TryAccessArray = string - * TryAccessArray = undefined - * ``` - */ -type TryAccessArray< - T extends ReadonlyArray, - K extends Key -> = K extends `${ArrayKey}` ? T[number] : TryAccess - -/** - * Type to evaluate the type which the given key points to. - * @typeParam T - type which is indexed by the key - * @typeParam K - key into the type - * @example - * ``` - * EvaluateKey<{foo: string}, 'foo'> = string - * EvaluateKey<[number, string], '1'> = string - * EvaluateKey = string - * ``` - */ -export type EvaluateKey = T extends ReadonlyArray ? IsTuple extends true ? TryAccess - : TryAccessArray - : TryAccess, K> - -/** - * Type to evaluate the type which the given path points to. - * @typeParam T - deeply nested type which is indexed by the path - * @typeParam PT - path into the deeply nested type - * @example - * ``` - * EvaluatePath<{foo: {bar: string}}, ['foo', 'bar']> = string - * EvaluatePath<[number, string], ['1']> = string - * EvaluatePath = number - * EvaluatePath = undefined - * ``` - */ -export type EvaluatePath = PT extends [ - infer K, - ...infer R -] ? EvaluatePath>, AsPathTuple> - : T - -/** - * Type which given a tuple type returns its own keys, i.e. only its indices. - * @typeParam T - tuple type - * @example - * ``` - * TupleKeys<[number, string]> = '0' | '1' - * ``` - */ -export type TupleKeys> = Exclude< - keyof T, - keyof any[] -> - -/** - * Type which extracts all numeric keys from an object. - * @typeParam T - type - * @example - * ``` - * NumericObjectKeys<{0: string, '1': string, foo: string}> = '0' | '1' - * ``` - */ -type NumericObjectKeys = ToKey< - Extract -> - -/** - * Type which extracts all numeric keys from an object, tuple, or array. - * If a union is passed, it evaluates to the overlapping numeric keys. - * @typeParam T - type - * @example - * ``` - * NumericKeys<{0: string, '1': string, foo: string}> = '0' | '1' - * NumericKeys = `${number}` - * NumericKeys<[string, number]> = '0' | '1' - * NumericKeys<{0: string, '1': string} | [number] | number[]> = '0' - * ``` - */ -export type NumericKeys = UnionToIntersection< - T extends ReadonlyArray ? IsTuple extends true ? [TupleKeys] - : [ToKey] - : [NumericObjectKeys] ->[never] - -/** - * Type which extracts all keys from an object. - * If a union is passed, it evaluates to the overlapping keys. - * @typeParam T - object type - * @example - * ``` - * ObjectKeys<{foo: string, bar: string}, string> = 'foo' | 'bar' - * ObjectKeys<{foo: string, bar: number}, string> = 'foo' - * ``` - */ -export type ObjectKeys = Exclude< - ToKey, - `${string}.${string}` | "" -> - -/** - * Type to check whether a type's property matches the constraint type - * and return its key. Converts the key to a {@link Key}. - * @typeParam T - type whose property should be checked - * @typeParam K - key of the property - * @typeParam U - constraint type - * @example - * ``` - * CheckKeyConstraint<{foo: string}, 'foo', string> = 'foo' - * CheckKeyConstraint<{foo: string}, 'foo', number> = never - * CheckKeyConstraint = `${number}` - * ``` - */ -export type CheckKeyConstraint = K extends any ? EvaluateKey extends U ? K - : never - : never - -/** - * Type which evaluates to true when the type is an array or tuple or is a union - * which contains an array or tuple. - * @typeParam T - type - * @example - * ``` - * ContainsIndexable<{foo: string}> = false - * ContainsIndexable<{foo: string} | number[]> = true - * ``` - */ -export type ContainsIndexable = IsNever< - Extract> -> extends true ? false - : true - -/** - * Type to implement {@link Keys} for non-nullable values. - * @typeParam T - non-nullable type whose property should be checked - */ -type KeysImpl = [T] extends [Traversable] ? ContainsIndexable extends true ? NumericKeys - : ObjectKeys - : never - -/** - * Type to find all properties of a type that match the constraint type - * and return their keys. - * If a union is passed, it evaluates to the overlapping keys. - * @typeParam T - type whose property should be checked - * @typeParam U - constraint type - * @example - * ``` - * Keys<{foo: string, bar: string}, string> = 'foo' | 'bar' - * Keys<{foo?: string, bar?: string}> = 'foo' | 'bar' - * Keys<{foo: string, bar: number}, string> = 'foo' - * Keys<[string, number], string> = '0' - * Keys = `${number}` - * Keys<{0: string, '1': string} | [number] | number[]> = '0' - * ``` - */ -export type Keys = IsAny extends true ? Key - : IsNever extends true ? Key - : IsNever> extends true ? never - : CheckKeyConstraint>, U> - -/** - * Type to check whether a {@link Key} is present in a type. - * If a union of {@link Key}s is passed, all {@link Key}s have to be present - * in the type. - * @typeParam T - type which is introspected - * @typeParam K - key - * @example - * ``` - * HasKey<{foo: string}, 'foo'> = true - * HasKey<{foo: string}, 'bar'> = false - * HasKey<{foo: string}, 'foo' | 'bar'> = false - * ``` - */ -export type HasKey = IsNever>> - -/** - * Type to implement {@link ValidPathPrefix} tail recursively. - * @typeParam T - type which the path should be checked against - * @typeParam PT - path which should exist within the given type - * @typeParam VPT - accumulates the prefix of {@link Key}s which have been - * confirmed to exist already - */ -type ValidPathPrefixImpl< - T, - PT extends PathTuple, - VPT extends PathTuple -> = PT extends [infer K, ...infer R] ? HasKey> extends true ? ValidPathPrefixImpl< - EvaluateKey>, - AsPathTuple, - AsPathTuple<[...VPT, K]> - > - : VPT - : VPT - -/** - * Type to find the longest path prefix which is still valid, - * i.e. exists within the given type. - * @typeParam T - type which the path should be checked against - * @typeParam PT - path which should exist within the given type - * @example - * ``` - * ValidPathPrefix<{foo: {bar: string}}, ['foo', 'bar']> = ['foo', 'bar'] - * ValidPathPrefix<{foo: {bar: string}}, ['foo', 'ba']> = ['foo'] - * ``` - */ -export type ValidPathPrefix = ValidPathPrefixImpl< - T, - PT, - [] -> - -/** - * Type to check whether a path through a type exists. - * @typeParam T - type which the path should be checked against - * @typeParam PT - path which should exist within the given type - * @example - * ``` - * HasPath<{foo: {bar: string}}, ['foo', 'bar']> = true - * HasPath<{foo: {bar: string}}, ['foo', 'ba']> = false - * ``` - */ -export type HasPath = ValidPathPrefix extends PT ? true - : false diff --git a/packages/effect-app/src/Model/filter/types/path/eager.ts b/packages/effect-app/src/Model/filter/types/path/eager.ts deleted file mode 100644 index 26e8ef271..000000000 --- a/packages/effect-app/src/Model/filter/types/path/eager.ts +++ /dev/null @@ -1,329 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { expectTypeOf } from "@effect/vitest" -import type { FieldValues } from "../fields.ts" -import type { BrowserNativeObject, Equals, IsLiteral, IsNever, Primitive } from "../utils.ts" - -import type { ArrayKey, IsTuple, TupleKeys } from "./common.ts" - -/** - * Helper type for recursively constructing paths through a type. - * `Seen` carries the set of object types already entered on this branch so - * recursion terminates on self-referential types (see {@link Path}). - */ -type PathImpl = V extends - | Primitive - | BrowserNativeObject ? `${K}` - // Stop before re-entering a type already on this branch. Recursive types - // (notably `Json`, the encoded form of `Schema.Defect()`: - // `JsonObject = { [x: string]: Json }`) would otherwise descend forever and - // blow past TS's instantiation limit => TS2589. The key itself stays a valid - // leaf path; only the unbounded descent is cut. - : [V] extends [Seen] ? `${K}` - : `${K}` | `${K}.${Path}` - -/** - * Type which eagerly collects all paths through a type - * @typeParam T - type which should be introspected - * @typeParam Seen - object types already entered on this recursion branch; - * used to terminate on self-referential types - * @example - * ``` - * Path<{foo: {bar: string}}> = 'foo' | 'foo.bar' - * ``` - */ -export type Path = T extends ReadonlyArray ? IsTuple extends true ? { - [K in TupleKeys]-?: PathImpl | "length" - }[TupleKeys] - : PathImpl | "length" - : { - [K in keyof T]-?: PathImpl - }[keyof T] - -/** - * See {@link Path} - */ -export type FieldPath = Path - -{ - // Regression: self-referential types must terminate instead of blowing past - // TS's instantiation limit (TS2589). `JsonLike` mirrors effect's `Json` — the - // encoded form of `Schema.Defect()` — whose `{ [x: string]: Json }` index - // signature previously made `Path` recurse forever. These aliases failing to - // compile (rather than resolving to a string union) signals the regression. - type JsonLike = - | string - | number - | boolean - | null - | ReadonlyArray - | { readonly [x: string]: JsonLike } - type _RawDefect = Path<{ id: string; raw: JsonLike }> - type _NestedDefect = Path<{ a: { b: { error: { raw: JsonLike } } } }> - type _ArrayOfDefect = Path<{ items: ReadonlyArray<{ raw: JsonLike }> }> - // finite paths are still produced - expectTypeOf<"id">().toExtend<_RawDefect>() - expectTypeOf<"raw">().toExtend<_RawDefect>() - expectTypeOf<"a.b.error.raw">().toExtend<_NestedDefect>() - expectTypeOf<`items.${number}.raw`>().toExtend<_ArrayOfDefect>() -} - -/** - * Type to evaluate the type which the given path points to. - * @typeParam T - deeply nested type which is indexed by the path - * @typeParam P - path into the deeply nested type - * @example - * ``` - * PathValue<{foo: {bar: string}}, 'foo.bar'> = string - * PathValue<[number, string], '1'> = string - * ``` - */ -/* dprint-ignore-start */ -export type PathValue> = - T extends any - ? P extends `${infer K}.${infer R}` - ? K extends keyof T - ? R extends Path - ? PathValue - : never - : K extends `${ArrayKey}` - ? T extends ReadonlyArray - ? PathValue> - : never - : never - : P extends keyof T - ? T[P] - : P extends `${ArrayKey}` - ? T extends ReadonlyArray - ? V - : never - : never - : never -/* dprint-ignore-end */ - -/** - * See {@link PathValue} - */ -export type FieldPathValue< - TFieldValues extends FieldValues, - TFieldPath extends FieldPath -> = PathValue - -/* dprint-ignore-start */ -export type SetPathValue, X> = - T extends any - ? P extends `${infer K}.${infer R}` - ? K extends keyof T - ? R extends Path - ? { [_ in keyof T]: _ extends K ? SetPathValue : T[_] } - : never - : K extends `${ArrayKey}` - ? T extends ReadonlyArray - ? { [_ in keyof T]: SetPathValue, X> } - : never - : never - : P extends keyof T - ? { [_ in keyof T]: _ extends P ? X : T[_] } - : P extends `${ArrayKey}` - ? T extends ReadonlyArray - ? { [_ in keyof T]: X } - : never - : never - : never -/* dprint-ignore-end */ - -export type SetFieldPathValue< - TFieldValues extends FieldValues, - TFieldPath extends FieldPath, - X -> = SetPathValue - -/* dprint-ignore-start */ -export type RefinePathValue, X extends string | number | boolean | null | bigint, Exclde extends boolean = false> = - T extends any - // recursive cases - ? P extends `${infer K}.${infer R}` - ? K extends keyof T - ? R extends Path - ? { [_ in keyof T]: _ extends K ? RefinePathValue : T[_] } - : never - : K extends `${ArrayKey}` - ? T extends ReadonlyArray - ? { [_ in keyof T]: RefinePathValue, X, Exclde> } - : never - : never - // base cases - : P extends keyof T - ? X extends T[P] - ? ({ [_ in keyof T]: _ extends P - ? Equals< - Exclde, - true, - Exclude, - IsLiteral>> - > - : T[_] - }) extends infer $T - ? Equals, true, never, $T> - : never - : Equals - : P extends `${ArrayKey}` - ? T extends ReadonlyArray - ? X extends V - ? { [_ in keyof T]: Equals< - Exclde, - true, - Exclude, - IsLiteral>> - > } - : never - : never - : never - : never -/* dprint-ignore-end */ - -export type EraseNeverContainingUnionElements = T extends any ? { - [K in keyof T]: IsNever extends true ? true : false -} extends infer $NC ? true extends $NC[keyof $NC] ? never : T -: never : never - -export type RefineFieldPathValue< - TFieldValues extends FieldValues, - TFieldPath extends FieldPath, - X extends string | number | boolean | null | bigint, - Exclde extends boolean = false -> = EraseNeverContainingUnionElements> - -{ - type test1 = RefineFieldPathValue<{ a: { b: "tag1"; v1: string } | { b: "tag2"; v2: number } }, "a.b", "tag1"> - expectTypeOf().toEqualTypeOf<{ a: { b: "tag1"; v1: string } }>() - - type test2 = RefineFieldPathValue<{ b: "tag1"; v1: string } | { b: "tag2"; v2: number }, "b", "tag1"> - expectTypeOf().toEqualTypeOf<{ b: "tag1"; v1: string }>() - - type test3 = RefineFieldPathValue<{ b: "tag1" | "tag2" }, "b", "tag1"> - expectTypeOf().toEqualTypeOf<{ b: "tag1" }>() - - type test4 = RefineFieldPathValue<{ b: ("tag1" | "tag2")[] }, `b.${number}`, "tag1"> - expectTypeOf().toEqualTypeOf<{ b: "tag1"[] }>() - - type test5 = RefineFieldPathValue<{ b: "tag1"; v1: unknown } | { b: "tag2"; v2: unknown }, "b", "tag1"> - expectTypeOf().toEqualTypeOf<{ b: "tag1"; v1: unknown }>() - - type test6 = RefineFieldPathValue< - { a: { b: "tag1"; v1: string } | { b: "tag2"; v2: number } } | { something: "else " }, - "a.b", - "tag2" - > - expectTypeOf().toEqualTypeOf<{ a: { b: "tag2"; v2: number } }>() - - type test7 = RefineFieldPathValue<{ b: string | null }, "b", "tag1"> - expectTypeOf().toEqualTypeOf<{ b: string }>() - - type test8 = RefineFieldPathValue<{ b: string | null }, "b", null> - expectTypeOf().toEqualTypeOf<{ b: null }>() - - type test1E = RefineFieldPathValue<{ a: { b: "tag1"; v1: string } | { b: "tag2"; v2: number } }, "a.b", "tag1", true> - expectTypeOf().toEqualTypeOf<{ a: { b: "tag2"; v2: number } }>() - - type test2E = RefineFieldPathValue<{ b: "tag1"; v1: string } | { b: "tag2"; v2: number }, "b", "tag2", true> - expectTypeOf().toEqualTypeOf<{ b: "tag1"; v1: string }>() - - type test3E = RefineFieldPathValue<{ b: "tag1" | "tag2" | null }, "b", null, true> - expectTypeOf().toEqualTypeOf<{ b: "tag1" | "tag2" }>() - - type test4E = RefineFieldPathValue<{ b: ("tag1" | "tag2")[] }, `b.${number}`, "tag1", true> - expectTypeOf().toEqualTypeOf<{ b: "tag2"[] }>() - - type test5E = RefineFieldPathValue<{ b: "tag1"; v1: unknown } | { b: "tag2"; v2: unknown }, "b", "tag1", true> - expectTypeOf().toEqualTypeOf<{ b: "tag2"; v2: unknown }>() - - type test6E = RefineFieldPathValue< - { a: { b: "tag1"; v1: string } | { b: "tag2"; v2: number } } | { something: "else " }, - "a.b", - "tag2", - true - > - expectTypeOf().toEqualTypeOf<{ a: { b: "tag1"; v1: string } }>() - - type test10 = RefineFieldPathValue< - | { nested: { a: string; _tag: "a" } | { b: number; _tag: "b" }; _tag: "ab" } - | { nested: { b: number; _tag: "b" } | { c: boolean; _tag: "c" }; _tag: "bc" }, - "nested._tag", - "a" - > - - expectTypeOf().toEqualTypeOf<{ - nested: { - a: string - _tag: "a" - } - _tag: "ab" - }>() -} - -{ - type test1 = SetFieldPathValue<{ foo: { bar: string[] } }, `foo.bar`, boolean> - expectTypeOf().toEqualTypeOf<{ foo: { bar: boolean } }>() - - type test1a = SetFieldPathValue<{ foo: { bar: string[]; baz: 12 } }, `foo.bar`, boolean> - expectTypeOf().toEqualTypeOf<{ foo: { bar: boolean; baz: 12 } }>() - - type test2 = SetFieldPathValue<{ foo: { bar: string[] } }, `foo.bar.${number}`, boolean> - expectTypeOf().toEqualTypeOf<{ foo: { bar: boolean[] } }>() - - type test2a = SetFieldPathValue<{ foo: { bar: readonly string[]; baz: 3 }; ban: 123 }, `foo.bar.${number}`, boolean> - expectTypeOf().toEqualTypeOf<{ foo: { bar: readonly boolean[]; baz: 3 }; ban: 123 }>() - - type test2b = SetFieldPathValue< - { foo: { bar: readonly { a: 1; b: 2 }[]; baz: 3 }; ban: 123 }, - `foo.bar.${number}.b`, - "b" - > - expectTypeOf().toEqualTypeOf<{ foo: { bar: readonly { a: 1; b: "b" }[]; baz: 3 }; ban: 123 }>() - - type test3 = SetFieldPathValue<{ foo: ["a", "b"] }, `foo.0`, boolean> - expectTypeOf().toEqualTypeOf<{ foo: [boolean, "b"] }>() - - type test3a = SetFieldPathValue<{ foo: [{ a: 123 }, "b"] }, `foo.0.a`, boolean> - expectTypeOf().toEqualTypeOf<{ foo: [{ a: boolean }, "b"] }>() -} - -/** - * Type to evaluate the type which the given paths point to. - * @typeParam TFieldValues - field values which are indexed by the paths - * @typeParam TPath - paths into the deeply nested field values - * @example - * ``` - * FieldPathValues<{foo: {bar: string}}, ['foo', 'foo.bar']> - * = [{bar: string}, string] - * ``` - */ -export type FieldPathValues< - TFieldValues extends FieldValues, - TPath extends FieldPath[] | readonly FieldPath[] -> = - & {} - & { - [K in keyof TPath]: FieldPathValue< - TFieldValues, - TPath[K] & FieldPath - > - } - -/** - * Type which eagerly collects all paths through a fieldType that matches a give type - * @typeParam TFieldValues - field values which are indexed by the paths - * @typeParam TValue - the value you want to match into each type - * @example - * ```typescript - * FieldPathByValue<{foo: {bar: number}, baz: number, bar: string}, number> - * = 'foo.bar' | 'baz' - * ``` - */ -export type FieldPathByValue = { - [Key in FieldPath]: FieldPathValue< - TFieldValues, - Key - > extends TValue ? Key - : never -}[FieldPath] diff --git a/packages/effect-app/src/Model/filter/types/path/index.ts b/packages/effect-app/src/Model/filter/types/path/index.ts deleted file mode 100644 index 67d7d71b2..000000000 --- a/packages/effect-app/src/Model/filter/types/path/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** Re-export public API */ - -export type { PathString } from "./common.ts" -export type { FieldPath, FieldPathByValue, FieldPathValue, FieldPathValues, Path, PathValue } from "./eager.ts" diff --git a/packages/effect-app/src/Model/filter/types/utils.ts b/packages/effect-app/src/Model/filter/types/utils.ts deleted file mode 100644 index 95d8aa7db..000000000 --- a/packages/effect-app/src/Model/filter/types/utils.ts +++ /dev/null @@ -1,128 +0,0 @@ -declare const $NestedValue: unique symbol - -/** - * @deprecated to be removed in the next major version - */ -export type NestedValue = { - [$NestedValue]: never -} & TValue - -/** - * @deprecated to be removed in the next major version - */ -export type UnpackNestedValue = T extends NestedValue ? U - : T extends Date | FileList | File | Blob ? T - : T extends object ? { [K in keyof T]: UnpackNestedValue } - : T - -/* -Projects that React Hook Form installed don't include the DOM library need these interfaces to compile. -React Native applications is no DOM available. The JavaScript runtime is ES6/ES2015 only. -These definitions allow such projects to compile with only --lib ES6. - -Warning: all of these interfaces are empty. -If you want type definitions for various properties, you need to add `--lib DOM` (via command line or tsconfig.json). -*/ - -export type Noop = () => void - -interface File extends Blob { - readonly lastModified: number - readonly name: string -} - -interface FileList { - readonly length: number - item(index: number): File | null - [index: number]: File -} - -export type Primitive = - | null - | undefined - | string - | number - | boolean - | symbol - | bigint - -export type BrowserNativeObject = Date | FileList | File - -export type EmptyObject = { [K in string | number]: never } - -export type NonUndefined = T extends undefined ? never : T - -export type LiteralUnion = - | T - | (U & { _?: never }) - -export type DeepPartial = T extends BrowserNativeObject | NestedValue ? T - : { [K in keyof T]?: DeepPartial } - -export type DeepPartialSkipArrayKey = T extends - | BrowserNativeObject - | NestedValue ? T - : T extends ReadonlyArray ? { [K in keyof T]: DeepPartialSkipArrayKey } - : { [K in keyof T]?: DeepPartialSkipArrayKey } - -/** - * Checks whether the type is any - * See {@link https://stackoverflow.com/a/49928360/3406963} - * @typeParam T - type which may be any - * ``` - * IsAny = true - * IsAny = false - * ``` - */ -export type IsAny = 0 extends 1 & T ? true : false - -/** - * Checks whether the type is never - * @typeParam T - type which may be never - * ``` - * IsAny = true - * IsAny = false - * ``` - */ -export type IsNever = [T] extends [never] ? true : false - -export type DeepMap = IsAny extends true ? any - : T extends BrowserNativeObject | NestedValue ? TValue - : T extends object ? { [K in keyof T]: DeepMap, TValue> } - : TValue - -export type IsFlatObject = Extract< - Exclude, - any[] | object -> extends never ? true - : false - -export type Merge = { - [K in keyof A | keyof B]?: K extends keyof A & keyof B ? [A[K], B[K]] extends [object, object] ? Merge - : A[K] | B[K] - : K extends keyof A ? A[K] - : K extends keyof B ? B[K] - : never -} - -export type Resolve = - & { - [K in keyof T]: Resolve - } - & {} - -export type ResolveFirstLevel = - & { - [K in keyof T]: T[K] - } - & {} - -export type Cast = T extends U ? T : U - -export type IsLiteral = string extends T ? False : number extends T ? False : True - -export type Extends = T extends U ? True : False - -export type IsEqual = (<_>() => _ extends T ? 1 : 2) extends (<_>() => _ extends U ? 1 : 2) ? true : false - -export type Equals = IsEqual extends true ? True : False diff --git a/packages/effect-app/src/Model/filter/types/validator.ts b/packages/effect-app/src/Model/filter/types/validator.ts deleted file mode 100644 index 062346819..000000000 --- a/packages/effect-app/src/Model/filter/types/validator.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Message } from "./errors.ts" -import type { FieldValues, InternalFieldName } from "./fields.ts" -import type { FieldPath, FieldPathValue } from "./path/index.ts" - -export type ValidationValue = boolean | number | string | RegExp - -export type ValidationRule< - TValidationValue extends ValidationValue = ValidationValue -> = TValidationValue | ValidationValueMessage - -export type ValidationValueMessage< - TValidationValue extends ValidationValue = ValidationValue -> = { - value: TValidationValue - message: Message -} - -export type ValidateResult = Message | Message[] | boolean | undefined - -export type Validate = ( - value: TFieldValue -) => ValidateResult | Promise - -export type RegisterOptions< - TFieldValues extends FieldValues = FieldValues, - TFieldName extends FieldPath = FieldPath -> = Partial<{ - required: Message | ValidationRule - min: ValidationRule - max: ValidationRule - maxLength: ValidationRule - minLength: ValidationRule - pattern: ValidationRule - validate: - | Validate> - | Record>> - valueAsNumber: boolean - valueAsDate: boolean - value: FieldPathValue - setValueAs: (value: any) => any - shouldUnregister?: boolean - onChange?: (event: any) => void - onBlur?: (event: any) => void - disabled: boolean - deps: InternalFieldName | InternalFieldName[] -}> diff --git a/packages/effect-app/src/Model/query.ts b/packages/effect-app/src/Model/query.ts deleted file mode 100644 index 22d242aca..000000000 --- a/packages/effect-app/src/Model/query.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from "./query/dsl.ts" -export * from "./query/new-kid-interpreter.ts" - -export interface RawQuery { - cosmos: (vals: { name: string }) => { - query: string - parameters: { - name: string - value: any - }[] - } - memory: (t: readonly Encoded[]) => readonly Out[] -} diff --git a/packages/effect-app/src/Model/query/dsl.ts b/packages/effect-app/src/Model/query/dsl.ts deleted file mode 100644 index b91351471..000000000 --- a/packages/effect-app/src/Model/query/dsl.ts +++ /dev/null @@ -1,3092 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as Data from "effect/Data" -import { flow } from "effect/Function" -import * as Pipeable from "effect/Pipeable" -import type { Covariant } from "effect/Types" -import type * as Option from "../../Option.ts" -import type * as S from "../../Schema.ts" -import type { NonNegativeInt } from "../../Schema.ts" -import type { Ops } from "../filter/filterApi.ts" -import type { FieldValues } from "../filter/types.ts" -import type { FieldPath, FieldPathValue, RefineFieldPathValue } from "../filter/types/path/eager.ts" - -export type QAll< - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - A = TFieldValues, - R = never, - TType extends "one" | "many" = "many", - Exclusive extends boolean = false -> = - | Query - | QueryWhere - | QueryEnd - | QueryProjection - -export const QId = Symbol() -export type QId = typeof QId - -export interface QueryTogether< - out TFieldValues extends FieldValues, - out TFieldValuesRefined extends TFieldValues = TFieldValues, - out Exclusive extends boolean = false, - out T extends "initial" | "where" | "end" | "projection" = "initial", - out A = TFieldValues, - out R = never, - out TType extends "many" | "one" | "count" = "many" -> extends Pipeable.Pipeable { - readonly [QId]: { - readonly _TFieldValues: Covariant - readonly _T: Covariant - readonly _TFieldValuesRefined: Covariant - readonly _A: Covariant - readonly _R: Covariant - readonly _TT: Covariant - readonly _Exclusive: Covariant - } -} - -type ExtractTType = T extends QueryTogether ? TType : never -type ExtractExclusiveness = T extends QueryTogether - ? Exclusive - : never -type ExtractFieldValues = T extends QueryTogether ? TFieldValues - : never -type ExtractFieldValuesRefined = T extends QueryTogether - ? TFieldValuesRefined - : never -type KeysOfUnion = T extends T ? keyof T : never -/** Peel Schema `tag`/`Literal` brands down to the underlying literal value. */ -type LiteralValue = T extends { readonly literal: infer L } ? L - : T extends string | number | boolean | null | bigint ? T - // Schema.tag<"x"> / withConstructorDefault wrappers are object brands whose - // nominal shape is not a PropertyKey — fall back to never so callers treat - // them via UnwrapTag below. - : never -// Last-resort: if T is a single object brand wrapping a string literal in its -// structure, treat non-union object tags as single-literal for same-tag duals. -type TagKey = [LiteralValue] extends [never] ? ( - string extends T ? string - : [T] extends [PropertyKey] ? T - // object brand (e.g. tag<"packing">): treat as single opaque tag token - : T - ) - : LiteralValue -// One-directional: domain tag may be a single literal that is a member of a -// multi-tag projection (`"picking" extends "picking"|"picked"`). Bidirectional -// equality would reject every multi-tag flat projection. -type ExtractTagged = From extends { readonly _tag: infer FromTag } - ? [TagKey] extends [TagKey] ? From : never - : never -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never -/** - * True when `Tag` is a single tag token (string literal or opaque brand like - * Schema.tag<"packing">), not a union of tags and not bare `string`. - */ -type IsSingleLiteralTag = string extends Tag ? false - : [Tag] extends [UnionToIntersection] ? true - : false -/** - * Domain shape that may supply stored fields for projection member `I`. - * - * - True tagged unions (`A | B` with different fields per `_tag`) resolve to the - * matching member so state-owned keys are not treated as universal. - * - Flat models that only carry `_tag: "a" | "b"` on a shared shape (no - * per-tag members in the Encoded union) fall back to the full `From` so - * existing Class+Literals projections keep typechecking. - */ -type ProjectableSource = I extends { readonly _tag: infer Tag } ? ( - [ExtractTagged] extends [never] ? From : ExtractTagged - ) - : From - -/** - * One projection member is projectable when every key is either: - * - in `ExtraKeys` (computed by the query / not stored on the domain row), or - * - a key of the matching domain source member (key presence only; nested - * field types may be narrowed by the projection). - * - * Uses `keyof Source` (not `KeysOfUnion` of the whole domain union) so a field - * owned only by some tags cannot be required on every branch — except for a - * single literal tag that maps to several domain variants (same `_tag`, - * different payloads), where `KeysOfUnion` allows each variant's keys. - */ -/** - * Keys the domain may supply for projection member `I`. - * - Single-literal tagged `I` (`_tag: "packing"`): keys of *any* same-tag - * domain variant (`KeysOfUnion`) — dual packing/closed shapes stay projectable. - * - Multi-tag / string-tagged `I` (`_tag: "a"|"b"` or `string`): keys of the - * *intersection* of matched domain members (`keyof` of the source union) so - * a flat multi-tag DTO cannot claim state-only fields (e.g. `batchId` on - * `initial`). - * - Untagged `I` (plain project DTOs): keys present on *any* domain member - * (`KeysOfUnion`), matching historical `project()` behavior. - */ -type ProjectableDomainKeys = I extends { readonly _tag: infer Tag } ? ( - IsSingleLiteralTag extends true ? KeysOfUnion> - : keyof ProjectableSource - ) - : KeysOfUnion - -/** - * Keys on projection member `I` that are neither computed (`ExtraKeys`) nor - * present on the matching domain source. Key presence only — nested field - * types may be narrowed by the projection, and optional domain/projection - * keys must not fail via `{ k?: T } extends { k: T }` (the old `-?` mapped - * assignability check rejected legitimate optionalKey fields like closed.batchId). - */ -type UnprojectableKeys = { - [K in keyof I]-?: K extends ExtraKeys ? never - : K extends ProjectableDomainKeys ? never - : K -}[keyof I] - -/** - * Distribute over tagged-union projection Encoded types. A non-distributive - * check against a union only sees `keyof (A|B)` (key intersection) and misses - * branch-only fields like cancel-only omissions. - */ -type IsProjectableMember = [I] extends [FieldValues] - ? ([UnprojectableKeys] extends [never] ? true : false) - : false - -/** - * `unknown` when every member of projection Encoded `I` is projectable from - * domain Encoded `From` (plus optional ExtraKeys for computed fields); `never` - * otherwise — use as an intersection constraint on a schema argument. - */ -type ProjectableGuard = false extends ( - I extends any ? IsProjectableMember : never -) ? never - : unknown - -/** - * Compile-time proof that a projection Encoded shape only requires stored keys - * that exist on the matching domain tagged state (or non-tagged source), plus - * any `ExtraKeys` filled by `projectComputed` (counts, flags, collects, …). - * - * Catches the class of Overview.List SchemaError where a cancel/recovery state - * omits a workflow lock field (`activeRequest`) in the domain model but the - * projection still requires it on every branch. - * - * @example - * ```ts - * type _ok = ProjectableFromDomain< - * { readonly _tag: "cancelled"; readonly id: string }, - * DomainEnc - * > // unknown - * - * type _bad = ProjectableFromDomain< - * { readonly _tag: "cancelled"; readonly id: string; readonly activeRequest: null }, - * DomainEnc - * > // never — activeRequest is not on domain cancelled - * ``` - */ -export type ProjectableFromDomain< - ProjectionEncoded, - DomainEncoded, - ExtraKeys extends PropertyKey = never -> = ProjectableGuard - -export type RelationDirection = "some" | "every" -export type Relation = { relation: RelationDirection } -export type Query = QueryTogether -export type QueryWhere< - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - Exclusive extends boolean = false -> = - & QueryTogether< - TFieldValues, - TFieldValuesRefined, - Exclusive, - "where" - > - & Relation - -export type QueryEnd< - TFieldValues extends FieldValues, - TType extends "many" | "one" | "count" = "many", - Exclusive extends boolean = false -> = QueryTogether< - TFieldValues, - TFieldValues, - Exclusive, - "end", - TFieldValues, - never, - TType -> - -export type QueryProjection< - TFieldValues extends FieldValues, - A = TFieldValues, - R = never, - TType extends "many" | "one" | "count" = "many", - Exclusive extends boolean = false -> = QueryTogether< - TFieldValues, - TFieldValues, - Exclusive, - "projection", - A, - R, - TType -> - -export type ComputedProjectionOperation = (q: Query) => QueryWhere - -export type ComputedProjectionMathExpression = - | { - readonly _tag: "field" - readonly field: string - } - | { - readonly _tag: "mul" - readonly left: ComputedProjectionMathExpression - readonly right: ComputedProjectionMathExpression - } - -declare const ComputedProjectionExpressionTypeId: unique symbol - -type ComputedProjectionOutput = { - readonly [ComputedProjectionExpressionTypeId]?: Covariant -} - -export type ComputedProjectionExpression = - & ComputedProjectionOutput - & ( - | { - readonly _tag: "relation-count" - readonly path: string - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-any" - readonly path: string - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-every" - readonly path: string - readonly operation: ComputedProjectionOperation - } - | { - readonly _tag: "relation-distinct-count" - readonly path: string - readonly field: string - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-sum" - readonly path: string - readonly field: string - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-sum-expr" - readonly path: string - readonly expression: ComputedProjectionMathExpression - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-sum-expr-by" - readonly path: string - readonly expression: ComputedProjectionMathExpression - readonly unit: string - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-sum-expr-normalized" - readonly path: string - readonly expression: ComputedProjectionMathExpression - readonly unit: string - readonly toBase: string - readonly factors: Readonly> - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-collect" - readonly path: string - readonly field: string - readonly distinct: boolean - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-collect-fields" - readonly path: string - readonly fields: readonly string[] - readonly distinct: boolean - readonly operation?: ComputedProjectionOperation - } - | { - readonly _tag: "relation-length" - readonly path: string - } - ) - -type NoExtraComputedKeys = string extends keyof M ? unknown - : Exclude> extends never ? unknown - : never - -/** - * An expression that aggregates values across documents (for use with {@link aggregate}). - * `agg-field` references a document field to group by; other tags are aggregate functions. - */ -export type AggregateExpression = - | { - readonly _tag: "agg-field" - readonly path: string - } - | { - readonly _tag: "agg-count" - } - | { - readonly _tag: "agg-count-when" - readonly operation: ComputedProjectionOperation - } - | { - readonly _tag: "agg-sum" - readonly field: string - } - | { - readonly _tag: "agg-min" - readonly field: string - } - | { - readonly _tag: "agg-max" - readonly field: string - } - -export type AggregateMap = Readonly> - -export type ComputedProjectionMap = Readonly> -export type Q = - | Initial - | Where - | And - | Or - | Order - | Page - | Project - | One - | Count - -export class Initial extends Data.TaggedClass("value")<{ value: "initial" }> - implements Query -{ - readonly [QId]!: any - constructor() { - super({ value: "initial" as const }) - } - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class Where extends Data.TaggedClass("where")<{ - current: Query - operation: [string, Ops, any] | [string, any] | ((q: Query) => QueryWhere) - relation: RelationDirection - subPath?: string -}> implements QueryWhere { - readonly [QId]!: any - - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class And extends Data.TaggedClass("and")<{ - current: Query - operation: [string, Ops, any] | [string, any] | ((q: Query) => QueryWhere) - relation: RelationDirection -}> implements QueryWhere { - readonly [QId]!: any - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class Or extends Data.TaggedClass("or")<{ - current: Query - operation: [string, Ops, any] | [string, any] | ((q: Query) => QueryWhere) - relation: RelationDirection -}> implements QueryWhere { - readonly [QId]!: any - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class Page extends Data.TaggedClass("page")<{ - current: Query | QueryWhere | QueryEnd - take?: number | undefined - skip?: number | undefined -}> implements QueryEnd { - readonly [QId]!: any - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class One extends Data.TaggedClass("one")<{ - current: Query | QueryWhere | QueryEnd -}> implements QueryEnd { - readonly [QId]!: any - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class Count extends Data.TaggedClass("count")<{ - current: Query | QueryWhere | QueryEnd -}> implements QueryEnd { - readonly [QId]!: any - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class Order> - extends Data.TaggedClass("order")<{ - current: Query | QueryWhere | QueryEnd - field: TFieldName - direction: "ASC" | "DESC" - }> - implements QueryEnd -{ - readonly [QId]!: any - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export class Project - extends Data.TaggedClass("project")<{ - current: Query | QueryWhere | QueryEnd - schema: S.Codec - mode: "collect" | "project" | "transform" | "aggregate" - computed?: ComputedProjectionMap - aggregateMap?: AggregateMap - }> - implements QueryProjection -{ - readonly [QId]!: any - override pipe() { - // eslint-disable-next-line prefer-rest-params - return Pipeable.pipeArguments(this, arguments) - } -} - -export const make: () => Query = () => new Initial() - -export const where: FilterWhere = (...operation: any[]) => (current: any) => - new Where({ current, operation: typeof operation[0] === "function" ? flow(...operation as [any]) : operation } as any) - -export const and: FilterContinuationAnd = (...operation: any[]) => (current: any) => - new And({ current, operation: typeof operation[0] === "function" ? flow(...operation as [any]) : operation } as any) - -export const or: FilterContinuationOr = (...operation: any[]) => (current: any) => - new Or({ current, operation: typeof operation[0] === "function" ? flow(...operation as [any]) : operation } as any) - -export const whereEvery: WhereEveryOrSome = - ((subPath: string, ...[first, ...rest]: any[]) => (current: any) => - new Where( - { - current, - operation: typeof first === "function" - ? flow(...[first, ...rest] as [any]) - : [`${subPath}.-1.${first}`, ...rest], - relation: "every", - subPath - } as any - )) as unknown as WhereEveryOrSome -export const whereSome: WhereEveryOrSome = - ((subPath: string, ...[first, ...rest]: any[]) => (current: any) => - new Where( - { - current, - operation: typeof first === "function" - ? flow(...[first, ...rest] as [any]) - : [`${subPath}.-1.${first}`, ...rest], - relation: "some", - subPath - } as any - )) as unknown as WhereEveryOrSome - -export const order: { - < - Q extends Query | QueryWhere | QueryEnd - >( - field: FieldPath>, - direction?: "ASC" | "DESC" - ): ( - current: Q - ) => QueryEnd, "many", ExtractExclusiveness> -} = (field, direction = "ASC") => (current) => new Order({ current, field: field as any, direction }) - -export const page: { - ( - page: { skip?: number; take?: number } - ): | QueryWhere | QueryEnd>( - current: Q - ) => QueryEnd, "many", ExtractExclusiveness> -} = ({ skip, take }) => (current) => - new Page({ - current, - take, - skip - }) - -export const one: { - | QueryWhere | QueryEnd>( - current: Q - ): QueryEnd, "one", ExtractExclusiveness> -} = (current) => - new One({ - current - }) - -// it's better to implement a distinct count so that the implementation can be optimised per adapter -export const count: { - < - Q extends Query | QueryWhere | QueryEnd - >( - current: Q - ): QueryProjection, NonNegativeInt, never, "count", ExtractExclusiveness> -} = (current) => new Count({ current }) - -/** - * Attach a projection schema to a query. - * - * The `select` clause sent to the store is derived from the schema's encoded - * AST property names (top-level + per-array sub-keys), so a projection always - * narrows what is read from the store. The repository augments that select - * with `id` and `_etag` for change tracking. See {@link toFilter} and the - * dispatch in `Repository/internal/internal.ts`. - * - * Modes — pick based on shape of the decoded value and on whether the - * persistence-model (PM) reverse-mapping is needed: - * - * - `"transform"` (default when `mode` omitted): goes through the repo's - * `parseMany` pipeline. The raw row is reverse-mapped via the - * etag/PM cache (re-injecting `_etag` and any PM-shape state) before - * decoding. Decode failures `orDie` (error channel = `never`). Use when - * the schema operates on the full PM shape (e.g. full-entity reads that - * must preserve etag tracking). - * - * - `"project"`: decodes the raw encoded row directly with the supplied - * schema. No PM reverse-mapping, no etag cache merge. Decode failures - * surface as `S.SchemaError`. Use for slim DTOs / aggregations that do not - * need etag tracking and whose schema input is a plain subset of `Encoded`. - * - * - `"collect"`: like `"project"`, but the schema yields `Option` and - * `None` values are dropped post-decode (`Array.getSomes`). Use to filter - * rows during decode (e.g. discriminated-union narrowing where some rows - * should not appear in the result). - */ -export const project: { - < - Q extends Query | QueryWhere | QueryEnd, - I extends ExtractFieldValuesRefined, - A = ExtractFieldValuesRefined, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: S.Codec, I, R>, - mode: "collect" - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> - - < - Q extends Query | QueryWhere | QueryEnd, - I extends ExtractFieldValuesRefined, - A = ExtractFieldValuesRefined, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: S.Codec, - mode: "project" - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> - < - Q extends Query | QueryWhere | QueryEnd, - I extends ExtractFieldValuesRefined, - A = ExtractFieldValuesRefined, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: S.Codec - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> - - < - Q extends Query | QueryWhere | QueryEnd, - I extends FieldValues, - A, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: - & S.Codec, I, R> - & ProjectableGuard>, - mode: "collect" - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> - - < - Q extends Query | QueryWhere | QueryEnd, - I extends FieldValues, - A, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: - & S.Codec - & ProjectableGuard>, - mode: "project" - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> - < - Q extends Query | QueryWhere | QueryEnd, - I extends FieldValues, - A, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: - & S.Codec - & ProjectableGuard> - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> -} = (schema: any, mode = "transform") => (current: any) => new Project({ current, schema, mode } as any) - -/** - * Element type of an array-valued field path on `TFieldValues`. Falls back to - * `FieldValues` when the path does not resolve to an array of structs so that - * `FieldPath<...>` stays defined (it just degrades to `string`). - * - * Uses `Extract` so that when `P` defaults to the full `FieldPath` - * union, only the array-valued branches contribute to the element type. - */ -export type RelationElement< - TFieldValues extends FieldValues, - P extends FieldPath -> = Extract, ReadonlyArray> extends ReadonlyArray - ? (E extends FieldValues ? E : FieldValues) - : FieldValues - -export const relation = < - TFieldValues extends FieldValues, - const P extends FieldPath = FieldPath ->( - path: P -) => ({ - /** - * Typed math-expression builder bound to the relation's element scope: - * `relation("items").expr.field("weight")` constrains the field path to - * `FieldPath>`. - */ - expr: { - field: (field: FieldPath>): ComputedProjectionMathExpression => ({ - _tag: "field", - field - }), - mul: ( - left: ComputedProjectionMathExpression, - right: ComputedProjectionMathExpression - ): ComputedProjectionMathExpression => ({ _tag: "mul", left, right }) - }, - length: (): ComputedProjectionExpression => ({ - _tag: "relation-length", - path - }), - count: (operation?: ComputedProjectionOperation): ComputedProjectionExpression => - operation - ? { - _tag: "relation-count", - path, - operation - } - : { - _tag: "relation-count", - path - }, - any: (operation?: ComputedProjectionOperation): ComputedProjectionExpression => - operation - ? { - _tag: "relation-any", - path, - operation - } - : { - _tag: "relation-any", - path - }, - every: (operation: ComputedProjectionOperation): ComputedProjectionExpression => ({ - _tag: "relation-every", - path, - operation - }), - distinctCount: >>( - field: F, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression => - operation - ? { - _tag: "relation-distinct-count", - path, - field, - operation - } - : { - _tag: "relation-distinct-count", - path, - field - }, - sum: >>( - field: F, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression => - operation - ? { - _tag: "relation-sum", - path, - field, - operation - } - : { - _tag: "relation-sum", - path, - field - }, - sumExpr: ( - expression: ComputedProjectionMathExpression, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression => - operation - ? { - _tag: "relation-sum-expr", - path, - expression, - operation - } - : { - _tag: "relation-sum-expr", - path, - expression - }, - sumExprBy: >>( - expression: ComputedProjectionMathExpression, - options: { unit: F }, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression< - ReadonlyArray<{ readonly unit: FieldPathValue, F>; readonly total: number }> - > => - operation - ? { - _tag: "relation-sum-expr-by", - path, - expression, - unit: options.unit, - operation - } - : { - _tag: "relation-sum-expr-by", - path, - expression, - unit: options.unit - }, - sumExprNormalized: ( - expression: ComputedProjectionMathExpression, - options: { - unit: FieldPath> - toBase: string - factors: Readonly> - }, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression => - operation - ? { - _tag: "relation-sum-expr-normalized", - path, - expression, - unit: options.unit, - toBase: options.toBase, - factors: options.factors, - operation - } - : { - _tag: "relation-sum-expr-normalized", - path, - expression, - unit: options.unit, - toBase: options.toBase, - factors: options.factors - }, - collect: >>( - field: F, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression, F>>> => - operation - ? { - _tag: "relation-collect", - path, - field, - distinct: false, - operation - } - : { - _tag: "relation-collect", - path, - field, - distinct: false - }, - collectDistinct: >>( - field: F, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression, F>>> => - operation - ? { - _tag: "relation-collect", - path, - field, - distinct: true, - operation - } - : { - _tag: "relation-collect", - path, - field, - distinct: true - }, - collectFields: >[]>( - fields: F, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression< - ReadonlyArray, F[number]>> - > => - operation - ? { - _tag: "relation-collect-fields", - path, - fields, - distinct: false, - operation - } - : { - _tag: "relation-collect-fields", - path, - fields, - distinct: false - }, - collectDistinctFields: >[]>( - fields: F, - operation?: ComputedProjectionOperation - ): ComputedProjectionExpression< - ReadonlyArray, F[number]>> - > => - operation - ? { - _tag: "relation-collect-fields", - path, - fields, - distinct: true, - operation - } - : { - _tag: "relation-collect-fields", - path, - fields, - distinct: true - } -}) - -/** - * Untyped math-expression builder. Field paths are not statically validated — - * prefer the scope-bound `relation(path).expr` builder when the element type - * is known, since it constrains the field argument to `FieldPath`. - * - * The generic parameter is accepted for symmetry with the scoped builder so - * callers may opt into a tighter check via `expr.field("x")`. - */ -export const expr = { - field: ( - field: FieldPath - ): ComputedProjectionMathExpression => ({ _tag: "field", field }), - mul: ( - left: ComputedProjectionMathExpression, - right: ComputedProjectionMathExpression - ): ComputedProjectionMathExpression => ({ _tag: "mul", left, right }) -} as const - -export const computed = (value: T): T => value - -/** - * Helpers passed to the inline-builder form of {@link projectComputed}. The - * `relation` factory is bound to the source row shape so paths/fields inside - * the projection are validated against the document shape inferred from the - * pipe. - */ -export interface ComputedHelpers { - relation: >(path: P) => ReturnType> -} - -const makeComputedHelpers = (): ComputedHelpers => ({ - relation: (path) => relation(path) -}) - -/** - * Only treat computed-map keys as ExtraKeys when `M` is a concrete object type. - * `ComputedProjectionMap` is `Record`, so `keyof M` is `string` and - * would otherwise allow every projection field as "computed". - */ -type ConcreteComputedKeys = string extends keyof M ? never : Extract - -/** - * Proof that projection Encoded `I` is projectable from domain Encoded, allowing - * concrete computed keys. Intersected onto the computed-map argument so - * inference of `M` is complete before the check runs. - */ -type ProjectableComputedMap< - M extends ComputedProjectionMap, - I extends FieldValues, - Domain -> = - & M - & NoExtraComputedKeys - & ( - [ProjectableGuard>] extends [never] ? { - readonly __projectableFromDomain: - "projection fields must exist on the matching domain tagged state or be computed keys" - } - : unknown - ) - -export const projectComputed: { - < - Q extends Query | QueryWhere | QueryEnd, - Schema extends S.Codec, - M extends ComputedProjectionMap, - I extends FieldValues = S.Codec.Encoded, - E extends boolean = ExtractExclusiveness - >( - schema: Schema, - build: (helpers: ComputedHelpers>) => ProjectableComputedMap>, - mode: "collect" - ): ( - current: Q - ) => QueryProjection< - ExtractFieldValuesRefined, - S.Schema.Type extends Option.Option ? A : never, - never, - ExtractTType, - E - > - - < - Q extends Query | QueryWhere | QueryEnd, - Schema extends S.Codec, - M extends ComputedProjectionMap, - I extends FieldValues = S.Codec.Encoded, - E extends boolean = ExtractExclusiveness - >( - schema: Schema, - build: (helpers: ComputedHelpers>) => ProjectableComputedMap>, - mode?: "project" - ): ( - current: Q - ) => QueryProjection, S.Schema.Type, never, ExtractTType, E> - - < - Q extends Query | QueryWhere | QueryEnd, - Schema extends S.Codec, - M extends ComputedProjectionMap, - I extends FieldValues = S.Codec.Encoded, - E extends boolean = ExtractExclusiveness - >( - schema: Schema, - computedProjection: ProjectableComputedMap>, - mode: "collect" - ): ( - current: Q - ) => QueryProjection< - ExtractFieldValuesRefined, - S.Schema.Type extends Option.Option ? A : never, - never, - ExtractTType, - E - > - - < - Q extends Query | QueryWhere | QueryEnd, - Schema extends S.Codec, - M extends ComputedProjectionMap, - I extends FieldValues = S.Codec.Encoded, - E extends boolean = ExtractExclusiveness - >( - schema: Schema, - computedProjection: ProjectableComputedMap>, - mode?: "project" - ): ( - current: Q - ) => QueryProjection, S.Schema.Type, never, ExtractTType, E> -} = ( - schema: any, - mapOrBuild: - | ComputedProjectionMap - | ((helpers: ComputedHelpers) => ComputedProjectionMap), - mode = "project" -) => -(current: any) => { - const computedProjection = typeof mapOrBuild === "function" - ? mapOrBuild(makeComputedHelpers()) - : mapOrBuild - return new Project({ current, schema, mode, computed: computedProjection }) -} - -/** - * Builder shape returned by {@link agg}. Field-taking methods are constrained - * to `FieldPath` so paths are validated against the document - * shape. - */ -export interface AggBuilder { - field: (path: FieldPath) => AggregateExpression - count: () => AggregateExpression - countWhen: (operation: ComputedProjectionOperation) => AggregateExpression - sum: (field: FieldPath) => AggregateExpression - min: (field: FieldPath) => AggregateExpression - max: (field: FieldPath) => AggregateExpression -} - -const makeAggBuilder = (): AggBuilder => ({ - field: (path) => ({ _tag: "agg-field", path }), - count: () => ({ _tag: "agg-count" }), - countWhen: (operation) => ({ _tag: "agg-count-when", operation }), - sum: (field) => ({ _tag: "agg-sum", field }), - min: (field) => ({ _tag: "agg-min", field }), - max: (field) => ({ _tag: "agg-max", field }) -}) - -/** - * Scope-bound aggregate-expression builder factory. Invoke with the source - * document field-value shape to get a builder whose `field`/`sum`/`min`/`max` - * arguments are constrained to `FieldPath`. - * - * Prefer the inline callback form of {@link aggregate} (`aggregate(schema, - * ($) => ({...}))`) — there the source shape is inferred from the pipe so no - * explicit type argument is needed. This factory is the escape hatch when - * the builder is constructed outside the pipe. - */ -export const agg = (): AggBuilder => - makeAggBuilder() - -/** - * Attach an aggregate projection to a query, performing GROUP BY + aggregate functions at the - * database level instead of fetching all rows and grouping in memory. - * - * Pass a builder callback to get a typed `agg` bound to the source row shape - * (inferred from the pipe — no explicit generic needed): - * - * @example - * ```ts - * repo.query( - * where("status", "active"), - * aggregate( - * S.Struct({ city: S.String, count: S.Number }), - * ($) => ({ - * city: $.field("address.city"), - * count: $.countWhen((q) => q.pipe(where("active", true))) - * }) - * ) - * ) - * ``` - * - * A plain {@link AggregateMap} is also accepted for the rare case where the - * map is built outside the pipe (loses path inference). - * - * The output is decoded directly with `schema` (no PM reverse-mapping, no etag tracking). - * Decode failures surface as `S.SchemaError`. - */ -export const aggregate: { - < - Q extends Query | QueryWhere | QueryEnd, - I extends Record, - A = ExtractFieldValuesRefined, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: S.Codec, - build: (agg: AggBuilder>) => AggregateMap - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> - < - Q extends Query | QueryWhere | QueryEnd, - I extends Record, - A = ExtractFieldValuesRefined, - R = never, - E extends boolean = ExtractExclusiveness - >( - schema: S.Codec, - aggregateMap: AggregateMap - ): ( - current: Q - ) => QueryProjection, A, R, ExtractTType, E> -} = (schema: any, mapOrBuild: AggregateMap | ((agg: AggBuilder) => AggregateMap)) => (current: any) => { - const aggregateMap = typeof mapOrBuild === "function" - ? mapOrBuild(makeAggBuilder()) - : mapOrBuild - return new Project({ current, schema, mode: "aggregate", aggregateMap } as any) -} - -type GetArV = T extends ReadonlySet ? R - : T extends readonly (infer R)[] ? R - : never - -type GetMapK = T extends ReadonlyMap ? K : never -type GetMapV = T extends ReadonlyMap ? V : never -type GetMapEntry = T extends ReadonlyMap ? readonly [K, V] : never - -type InValues = readonly T[] | ReadonlySet - -export type FilterContinuations = { - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - value: V - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "gt" | "gte" | "lt" | "lte" | "neq", - value: V // only numbers? - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "startsWith" | "endsWith" | "contains" | "notContains" | "notStartsWith" | "notEndsWith", - value: FieldPathValue extends string ? string : never - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "in" | "notIn", - value: InValues> - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: - | "includes" - | "notIncludes", - value: GetArV - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: - | "includes-any" - | "notIncludes-any" - | "includes-all" - | "notIncludes-all", - value: InValues> - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "hasKey" | "notHasKey", - value: GetMapK - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "hasValue" | "notHasValue", - value: GetMapV - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "hasKeyValue" | "notHasKeyValue", - value: GetMapEntry - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "hasKey-any" | "notHasKey-any" | "hasKey-all" | "notHasKey-all", - value: InValues> - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "hasValue-any" | "notHasValue-any" | "hasValue-all" | "notHasValue-all", - value: InValues> - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "hasKeyValue-any" | "notHasKeyValue-any" | "hasKeyValue-all" | "notHasKeyValue-all", - value: InValues> - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >(f: { - path: TFieldName - op: "eq" - value: V - }): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => IsCurrentInitial extends true ? QueryWhere - : QueryWhere -} - -export type FilterContinuationsWithSubpath = { - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - value: V - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "gt" | "gte" | "lt" | "lte" | "neq", - value: V // only numbers? - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "startsWith" | "endsWith" | "contains" | "notContains" | "notStartsWith" | "notEndsWith", - value: FieldPathValue extends string ? string : never - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "in" | "notIn", - value: InValues - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: - | "includes" - | "notIncludes", - value: GetArV - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: - | "includes-any" - | "notIncludes-any" - | "includes-all" - | "notIncludes-all", - value: InValues> - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "hasKey" | "notHasKey", - value: GetMapK - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "hasValue" | "notHasValue", - value: GetMapV - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "hasKeyValue" | "notHasKeyValue", - value: GetMapEntry - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "hasKey-any" | "notHasKey-any" | "hasKey-all" | "notHasKey-all", - value: InValues> - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "hasValue-any" | "notHasValue-any" | "hasValue-all" | "notHasValue-all", - value: InValues> - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >( - subPath: TFieldName, - restPath: TFieldNameSub, - op: "hasKeyValue-any" | "notHasKeyValue-any" | "hasKeyValue-all" | "notHasKeyValue-all", - value: InValues> - ): ( - current: Query - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - TFieldValuesSub extends TFieldValues[TFieldName][number], - TFieldNameSub extends FieldPath, - V extends FieldPathValue - >(f: { - subPath: TFieldName - restPath: TFieldNameSub - op: "eq" - value: V - }): ( - current: Query - ) => QueryWhere -} - -/* dprint-ignore-start */ -export type RefineWithLiteral< - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - V, - Exclde extends boolean = false -> = - // refine only if the value is a primitive - [V] extends [string | number | boolean | null | bigint] - ? RefineFieldPathValue - : TFieldValues -/* dprint-ignore-end */ - -export type FilteringRefinements = { - < - TFieldValues extends FieldValues, - const TFieldName extends FieldPath, - const V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - value: V - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => QueryWhere< - TFieldValues, - // @ts-expect-error it's TS - RefineWithLiteral, - TFieldName extends "_tag" ? true : false // consider only _tag as an exclusive field and only in the positive case - > - < - TFieldValues extends FieldValues, - const TFieldName extends FieldPath, - const V extends FieldPathValue, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "neq", - value: V - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => QueryWhere< - TFieldValues, - // @ts-expect-error it's TS - RefineWithLiteral, - E - > - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath, - const V extends readonly FieldPathValue[], - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false - >( - path: TFieldName, - op: "in", - value: V - ): ( - current: IsCurrentInitial extends true ? Query - : QueryWhere - ) => QueryWhere< - TFieldValues, - // @ts-expect-error it's TS - RefineWithLiteral>, - E - > -} - -export type NestedQueriesFixedRefinement = { - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false - >( - fb: (current: Query) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere, - fh: (query: QueryWhere) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere, - fh: (query: QueryWhere) => QueryWhere, - fi: (query: QueryWhere) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere, - fh: (query: QueryWhere) => QueryWhere, - fi: (query: QueryWhere) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere, - fh: (query: QueryWhere) => QueryWhere, - fi: (query: QueryWhere) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere, - fh: (query: QueryWhere) => QueryWhere, - fi: (query: QueryWhere) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false, - E13 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere, - fh: (query: QueryWhere) => QueryWhere, - fi: (query: QueryWhere) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere, - fm: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false, - E13 extends boolean = false, - E14 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: (query: QueryWhere) => QueryWhere, - fd: (query: QueryWhere) => QueryWhere, - fe: (query: QueryWhere) => QueryWhere, - ff: (query: QueryWhere) => QueryWhere, - fg: (query: QueryWhere) => QueryWhere, - fh: (query: QueryWhere) => QueryWhere, - fi: (query: QueryWhere) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere, - fm: ( - query: QueryWhere - ) => QueryWhere, - fn: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: IsCurrentInitial extends true ? Query : QueryWhere - ) => QueryWhere -} - -export type NestedQueriesFreeIntersectionRefinement = { - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false - >( - fb: (current: Query) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - TFieldValuesRefined12 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - TFieldValuesRefined12 extends TFieldValues = TFieldValues, - TFieldValuesRefined13 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false, - E13 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere, - fm: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - TFieldValuesRefined12 extends TFieldValues = TFieldValues, - TFieldValuesRefined13 extends TFieldValues = TFieldValues, - TFieldValuesRefined14 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false, - E13 extends boolean = false, - E14 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere, - fm: ( - query: QueryWhere - ) => QueryWhere, - fn: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere -} - -// to be safe, or forces the output to be exclusive because you can always or _tag filters -export type NestedQueriesFreeDisjointRefinement = { - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false - >( - fb: (current: Query) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - TFieldValuesRefined12 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - TFieldValuesRefined12 extends TFieldValues = TFieldValues, - TFieldValuesRefined13 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false, - E13 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere, - fm: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere - - < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - TFieldValuesRefined2 extends TFieldValues = TFieldValues, - TFieldValuesRefined3 extends TFieldValues = TFieldValues, - TFieldValuesRefined4 extends TFieldValues = TFieldValues, - TFieldValuesRefined5 extends TFieldValues = TFieldValues, - TFieldValuesRefined6 extends TFieldValues = TFieldValues, - TFieldValuesRefined7 extends TFieldValues = TFieldValues, - TFieldValuesRefined8 extends TFieldValues = TFieldValues, - TFieldValuesRefined9 extends TFieldValues = TFieldValues, - TFieldValuesRefined10 extends TFieldValues = TFieldValues, - TFieldValuesRefined11 extends TFieldValues = TFieldValues, - TFieldValuesRefined12 extends TFieldValues = TFieldValues, - TFieldValuesRefined13 extends TFieldValues = TFieldValues, - TFieldValuesRefined14 extends TFieldValues = TFieldValues, - E extends boolean = false, - E2 extends boolean = false, - E3 extends boolean = false, - E4 extends boolean = false, - E5 extends boolean = false, - E6 extends boolean = false, - E7 extends boolean = false, - E8 extends boolean = false, - E9 extends boolean = false, - E10 extends boolean = false, - E11 extends boolean = false, - E12 extends boolean = false, - E13 extends boolean = false, - E14 extends boolean = false - >( - fb: (current: Query) => QueryWhere, - fc: ( - query: QueryWhere - ) => QueryWhere, - fd: ( - query: QueryWhere - ) => QueryWhere, - fe: ( - query: QueryWhere - ) => QueryWhere, - ff: ( - query: QueryWhere - ) => QueryWhere, - fg: ( - query: QueryWhere - ) => QueryWhere, - fh: ( - query: QueryWhere - ) => QueryWhere, - fi: ( - query: QueryWhere - ) => QueryWhere, - fj: ( - query: QueryWhere - ) => QueryWhere, - fk: ( - query: QueryWhere - ) => QueryWhere, - fl: ( - query: QueryWhere - ) => QueryWhere, - fm: ( - query: QueryWhere - ) => QueryWhere, - fn: ( - query: QueryWhere - ) => QueryWhere - ): ( - current: QueryWhere - ) => QueryWhere -} - -export type FilterWhere = - & NestedQueriesFixedRefinement - & FilteringRefinements - & FilterContinuations - -export type FilterContinuationAnd = - & NestedQueriesFreeIntersectionRefinement - & FilteringRefinements - & FilterContinuations - -export type FilterContinuationOr = - & NestedQueriesFreeDisjointRefinement - & FilterContinuations - -// it does not support refinements by choice (for now) -export type WhereEveryOrSome = - & { - < - TFieldValues extends FieldValues, - TFieldName extends FieldPath - >( - subPath: TFieldName, - dude: ( - current: Query - ) => QueryWhere, - ...dudes: (( - current: QueryWhere - ) => QueryWhere)[] - ): ( - current: Query - ) => QueryWhere - } - & FilterContinuationsWithSubpath diff --git a/packages/effect-app/src/Model/query/new-kid-interpreter.ts b/packages/effect-app/src/Model/query/new-kid-interpreter.ts deleted file mode 100644 index a0c38e24b..000000000 --- a/packages/effect-app/src/Model/query/new-kid-interpreter.ts +++ /dev/null @@ -1,531 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { identity } from "effect/Function" -import * as Array from "../../Array.ts" -import * as Option from "../../Option.ts" -import * as S from "../../Schema.ts" -import * as SchemaAST from "../../SchemaAST.ts" -import { dropUndefinedT } from "../../utils.ts" -import type { FilterResult } from "../filter/filterApi.ts" -import type { FieldValues } from "../filter/types.ts" -import type { FieldPath } from "../filter/types/path/eager.ts" -import { make, type Q, type QAll } from "../query/dsl.ts" - -export type AggregateIrExpression = - | { readonly _tag: "agg-count" } - | { readonly _tag: "agg-count-when"; readonly filter: readonly FilterResult[] } - | { readonly _tag: "agg-sum"; readonly field: string } - | { readonly _tag: "agg-min"; readonly field: string } - | { readonly _tag: "agg-max"; readonly field: string } - -export type AggregateIrItem = - | AggregateIrExpression - | { readonly _tag: "agg-field"; readonly path: string } - -export type ComputedProjectionMathIrExpression = - | { - readonly _tag: "field" - readonly field: string - } - | { - readonly _tag: "mul" - readonly left: ComputedProjectionMathIrExpression - readonly right: ComputedProjectionMathIrExpression - } - -export type ComputedProjectionIrExpression = - | { - readonly _tag: "relation-count" - readonly path: string - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-any" - readonly path: string - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-every" - readonly path: string - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-distinct-count" - readonly path: string - readonly field: string - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-sum" - readonly path: string - readonly field: string - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-sum-expr" - readonly path: string - readonly expression: ComputedProjectionMathIrExpression - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-sum-expr-by" - readonly path: string - readonly expression: ComputedProjectionMathIrExpression - readonly unit: string - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-sum-expr-normalized" - readonly path: string - readonly expression: ComputedProjectionMathIrExpression - readonly unit: string - readonly toBase: string - readonly factors: Readonly> - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-collect" - readonly path: string - readonly field: string - readonly distinct: boolean - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-collect-fields" - readonly path: string - readonly fields: readonly string[] - readonly distinct: boolean - readonly filter: readonly FilterResult[] - } - | { - readonly _tag: "relation-length" - readonly path: string - } - -type SelectItem = - | keyof TFieldValues - | { key: string; subKeys: string[] } - | { - key: string - computed: ComputedProjectionIrExpression - } - -const isArraySelect = ( - item: SelectItem -): item is { key: string; subKeys: string[] } => typeof item === "object" && item !== null && "subKeys" in item - -const mergeArraySelect = ( - select: SelectItem[], - arraySelect: { key: string; subKeys: string[] } -) => { - const subKeys = [...new Set(arraySelect.subKeys)] - const existing = select.find( - (item): item is { key: string; subKeys: string[] } => isArraySelect(item) && item.key === arraySelect.key - ) - - if (existing) { - const existingIndex = select.indexOf(existing) - select[existingIndex] = { - key: arraySelect.key, - subKeys: [...new Set([...existing.subKeys, ...subKeys])] - } - } else { - select.push({ key: arraySelect.key, subKeys }) - } - - const scalarIndex = select.findIndex((item) => item === arraySelect.key) - if (scalarIndex >= 0) { - select.splice(scalarIndex, 1) - } -} - -type Result = { - filter: FilterResult[] - schema: S.Codec | undefined - limit: number | undefined - skip: number | undefined - order: { key: FieldPath; direction: "ASC" | "DESC" }[] - ttype: "one" | "many" | "count" | undefined - mode: "collect" | "project" | "transform" | "aggregate" | undefined - computed: Record | undefined - aggregateMap: Record | undefined -} - -const interpret = < - TFieldValues extends FieldValues, - TFieldValuesRefined extends TFieldValues = TFieldValues, - A = TFieldValues, - R = never ->(_: QAll) => { - const a = _ as Q - - const data: Result = { - filter: [], - schema: undefined, - limit: undefined, - skip: undefined, - order: [], - ttype: undefined, - mode: undefined, - computed: undefined, - aggregateMap: undefined - } - - const upd = ( - v: Result - ) => { - data.filter.push(...v.filter) - data.order.push(...v.order) - if (v.limit !== undefined) data.limit = v.limit - if (v.skip !== undefined) data.skip = v.skip - if (v.ttype !== undefined) data.ttype = v.ttype - if (v.schema !== undefined) data.schema = v.schema - if (v.mode !== undefined) data.mode = v.mode - if (v.computed !== undefined) data.computed = v.computed - if (v.aggregateMap !== undefined) data.aggregateMap = v.aggregateMap - } - - const applyPath = (path: string) => (_: FilterResult): FilterResult => - _.t === "where" || _.t === "and" || _.t === "or" - ? { ..._, path: `${path}.-1.${_.path}` } - : { ..._, result: _.result.map(applyPath(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 }] - } - 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 -} - -const walkTransformation = (t: S.AST.AST): S.AST.AST => { - if (S.AST.isDeclaration(t) && t.typeParameters.length > 0) { - return walkTransformation(t.typeParameters[0]!) - } - return t -} - -const objectAsts = (ast: S.AST.AST): readonly S.AST.Objects[] => { - const t = walkTransformation(ast) - if (S.AST.isObjects(t)) { - return [t] - } - if (S.AST.isUnion(t)) { - return t.types.flatMap(objectAsts) - } - return [] -} - -const objectKeys = (ast: S.AST.AST): readonly string[] => [ - ...new Set(objectAsts(ast).flatMap((object) => object.propertySignatures.map((_) => _.name as string))) -] - -export const toFilter = < - TFieldValues extends FieldValues, - A, - R, - TFieldValuesRefined extends TFieldValues = TFieldValues ->( - q: QAll, - baseSchema?: S.Schema -) => { - // TODO: Native interpreter for each db adapter, instead of the intermediate "new-kid" format - const a = interpret(q) - - // Aggregate mode: build select entirely from aggregateMap (no schema-driven field list) - if (a.mode === "aggregate" && a.aggregateMap) { - const aggSelect = Object.entries(a.aggregateMap).map(([key, item]) => { - if (item._tag === "agg-field") { - return { key, path: item.path } - } - return { key, aggregate: item } - }) - return dropUndefinedT({ - t: null as unknown as TFieldValues, - limit: a.limit, - skip: a.skip, - select: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(aggSelect)) as any, - schema: a.schema, - computed: undefined, - order: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(a.order)), - ttype: a.ttype, - mode: "aggregate" as const, - filter: a.filter.length ? a.filter : undefined - }) - } - - const schema = a.schema - let select: SelectItem[] = [] - // TODO: support more complex (nested) schemas? - if (schema) { - const t = walkTransformation(SchemaAST.toEncoded(schema.ast)) - const objects = [...objectAsts(t)] - if (Array.isArrayNonEmpty(objects)) { - select = [...objectKeys(t)] - for (const prop of objects.flatMap((_) => _.propertySignatures)) { - if (S.AST.isArrays(prop.type)) { - // make sure we only select when there are actually type literals in the tuple... - // otherwise we might be dealing with strings etc. - // TODO; be more strict, can't support arrays with unions that have non TypeLiteral members etc.. - const arraySelect = { - key: prop.name as string, - subKeys: Array.flatMap( - prop.type.rest, - (x) => { - return objectKeys(x) - } - ) - } - if (arraySelect.subKeys.length > 0) { - mergeArraySelect(select, arraySelect) - } - } - } - } - } - const computed = a.computed - const getSelectKey = (_: (typeof select)[number]) => { - if (typeof _ === "string") { - return _ - } - if (typeof _ === "object" && _ !== null && "key" in _) { - return _.key - } - return String(_) - } - const schemaKeys = select.map(getSelectKey) - const nonEncodedSchemaKeys = (() => { - if (!baseSchema) { - return [] as string[] - } - const encoded = walkTransformation(SchemaAST.toEncoded(baseSchema.ast)) - const encodedKeys = objectKeys(encoded) - return schemaKeys.filter((key) => !encodedKeys.includes(key)) - })() - const missingComputedKeys = nonEncodedSchemaKeys.filter((key) => !(computed && key in computed)) - - if (Array.isArrayNonEmpty(missingComputedKeys)) { - throw new Error(`Missing computed projections for schema keys: ${missingComputedKeys.join(", ")}`) - } - - if (computed) { - const computedKeys = Object.keys(computed) - const extraComputedKeys = computedKeys.filter((key) => !schemaKeys.includes(key)) - if (Array.isArrayNonEmpty(extraComputedKeys)) { - throw new Error(`Computed projection keys must exist in projection schema: ${extraComputedKeys.join(", ")}`) - } - select = select.filter((_) => { - const key = getSelectKey(_) - return !(key in computed) - }) - select.push(...Object.entries(computed).map(([key, expression]) => ({ key, computed: expression }))) - } - return dropUndefinedT({ - t: null as unknown as TFieldValues, - limit: a.limit, - skip: a.skip, - select: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(select)), - schema, - computed, - order: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(a.order)), - ttype: a.ttype, - mode: a.mode ?? "transform", - filter: a.filter.length - ? a.filter - : undefined - }) -} diff --git a/packages/effect-app/src/NonEmptySet.ts b/packages/effect-app/src/NonEmptySet.ts deleted file mode 100644 index 1cd39e22a..000000000 --- a/packages/effect-app/src/NonEmptySet.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { NonEmptyReadonlyArray } from "effect/Array" -import type * as Equivalence from "effect/Equivalence" -import * as Option from "effect/Option" -import type * as Order from "effect/Order" -import { flow, pipe } from "./Function.ts" -import { filter_, filterMap, filterMap_, fromArray as fromArrayOriginal, insert as insertOriginal, insert_ as insert_Original, map, map_, reduce, reduce_, remove, remove_, type Set, toArray as toArrayOriginal } from "./Set.ts" - -export interface NonEmptyBrand { - readonly NonEmpty: unique symbol -} - -export type NonEmptySet = Set & NonEmptyBrand - -function make_(ord: Order.Order, eq_?: Equivalence.Equivalence) { - const eq = eq_ - ?? ((x, y) => ord(x, y) === 0) - - const fromArray_ = fromArrayOriginal(eq) - const fromArray = flow(fromArray_, fromSet) - const fromNonEmptyArray = (arr: NonEmptyReadonlyArray) => fromArray_(arr) as NonEmptySet - const concat_ = (set: NonEmptySet, it: Iterable) => fromArray([...set, ...it]) - const insert__ = insertOriginal(eq) - const insert: (a: A) => (set: NonEmptySet) => NonEmptySet = insert__ as any - const insert_: (set: NonEmptySet, a: A) => NonEmptySet = insert_Original as any - - function replace_(set: NonEmptySet, a: A) { - return (pipe(filter_(set, (x) => !eq(x, a)), insert__(a)) as NonEmptySet) - } - - const toArray__ = toArrayOriginal(ord) - - function toArray(s: NonEmptySet) { - return toArray__(s) as NonEmptyReadonlyArray - } - - const remove__ = remove(eq) - const filterMap__ = filterMap(eq) - - return { - insert, - insert_, - remove: (a: A) => flow(remove__(a), fromSet), - remove_: flow(remove_(eq), fromSet), - reduce: reduce(ord), - reduce_: reduce_(ord), - replace: (a: A) => (set: NonEmptySet) => replace_(set, a), - replace_, - toArray, - fromArray, - fromNonEmptyArray, - from: (it: Iterable) => fromArray([...it]), - of: (a: A) => new Set([a]) as unknown as NonEmptySet, - concat_, - concat: (it: Iterable) => (set: NonEmptySet) => concat_(set, it), - - // A and B the same, useful when editing elements. - map: map(eq) as unknown as ( - f: (x: A) => A - ) => (set: NonEmptySet) => NonEmptySet, - map_: map_(eq) as unknown as ( - set: NonEmptySet, - f: (x: A) => A - ) => NonEmptySet, - filterMap: (f: (a: A) => Option.Option) => flow(filterMap__((a) => f(a)), fromSet), - filterMap_: flow(filterMap_(eq), fromSet) - } - // TODO: extend -} - -class Wrapper { - wrapped(ord: Order.Order, eq?: Equivalence.Equivalence) { - return make_(ord, eq) - } -} - -export interface NonEmptySetSchemaExtensions extends ReturnType["wrapped"]> {} - -export const make: ( - ord: Order.Order, - eq?: Equivalence.Equivalence -) => NonEmptySetSchemaExtensions = make_ - -export function fromSet(set: Set) { - if (set.size > 0) { - return Option.some(set as NonEmptySet) - } else { - return Option.none() - } -} - -// TODO -export * from "./Set.ts" diff --git a/packages/effect-app/src/Option.ts b/packages/effect-app/src/Option.ts deleted file mode 100644 index 3daac9dfe..000000000 --- a/packages/effect-app/src/Option.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -// eslint-disable-next-line import/no-unassigned-import -import "./builtin.ts" -import { type Some } from "effect/Option" -import * as Option from "effect/Option" - -export * from "effect/Option" - -export function omitableToNullable(om: Option.Option | undefined) { - return om ?? Option.fromNullishOr(om) -} - -export const fromBool = (b: boolean) => (b ? Option.some(true) : Option.none()) - -/** - * Access property, unwrapping Options along the path - */ -export function p>>( - k: K -): (v: Option.Option) => Option.Option<_A> -export function p(k: K): (v: Option.Option) => Option.Option -export function p(k: any) { - return (v: Option.Option) => Option.flatMap(v, (a) => convert((a as any)[k])) -} -function convert(a: any) { - return Option.isSome(a) || Option.isNone(a) ? a : Option.fromNullishOr(a) -} -export type _A = A extends Some ? Y : never -type KeysMatching = { - [K in keyof T]: T[K] extends V ? K : never -}[keyof T] - -export const PartialExceptionTypeId = Symbol() -export type PartialExceptionTypeId = typeof PartialExceptionTypeId - -export class PartialException { - readonly _typeId: PartialExceptionTypeId = PartialExceptionTypeId -} - -function raisePartial(): X { - throw new PartialException() -} - -/** - * Simulates a partial function - */ -export function partial( - f: (miss: () => X) => (...args: ARGS) => A -): (...args: ARGS) => Option.Option { - return (...args) => { - try { - return Option.some(f(raisePartial)(...args)) - } catch (e) { - if (e instanceof PartialException) { - return Option.none() - } - throw e - } - } -} diff --git a/packages/effect-app/src/Pure.ts b/packages/effect-app/src/Pure.ts deleted file mode 100644 index 41315bc14..000000000 --- a/packages/effect-app/src/Pure.ts +++ /dev/null @@ -1,450 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as Chunk from "effect/Chunk" -import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" -import * as Result from "effect/Result" -import * as Tuple from "effect/Tuple" -import * as Context from "./Context.ts" - -const S1 = Symbol() -const S2 = Symbol() -const W = Symbol() - -export interface PureState { - readonly [S1]: (_: S) => void - readonly [S2]: () => S2 - - state: S2 -} - -export interface PureLog { - readonly [W]: () => W - log: Chunk.Chunk -} - -export interface PureEnv extends PureState, PureLog {} - -export interface PureEnvTest extends PureState, PureLog {} - -class PureEnvBase implements PureEnv { - readonly [W]!: () => W - readonly [S1]!: (_: S) => void - readonly [S2]!: () => S2 - readonly state: S2 - readonly log: Chunk.Chunk - - constructor(s: S2) { - this.state = s - this.log = Chunk.empty() - } -} - -export function makePureEnv(s: S2): PureEnv { - return new PureEnvBase(s) -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function unifyPureEnv>( - self: X -): PureEnv< - [X] extends [{ [W]: () => infer W }] ? W : never, - [X] extends [{ [S1]: (_: infer S) => void }] ? S : never, - [X] extends [{ [S2]: () => infer S2 }] ? S2 : never -> { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return self -} - -export type Pure = Effect.Effect | R> - -// type dsl = ProgramDSL - -export function GMUA_( - get: Pure, - modify: (i: GA) => Pure, - update: (i: GA) => Pure -): Pure { - return Effect.flatMap(get, modify).pipe(Effect.flatMap(([s, a]) => Effect.map(update(s), () => a))) -} - -export function GMUA(modify: (i: GA) => Pure) { - return ( - get: Pure, - update: (i: GA) => Pure - ) => GMUA_(get, modify, update) -} - -export function GMU_( - get: Pure, - modify: (i: GA) => Pure, - update: (i: GA) => Pure -): Pure { - return Effect.flatMap(get, modify).pipe(Effect.flatMap(update)) -} - -export function GMU(modify: (i: GA) => Pure) { - return ( - get: Pure, - update: (i: GA) => Pure - ) => GMU_(get, modify, update) -} - -const tagg = Context.Service<{ env: PureEnv }>("PureEnv") -function castTag() { - return tagg as any as Context.Service, PureEnvEnv> -} - -export const ServiceTag = Symbol() -export type ServiceTag = typeof ServiceTag - -export abstract class PhantomTypeParameter { - protected abstract readonly [ServiceTag]: { - readonly [NameP in Identifier]: (_: InstantiatedType) => InstantiatedType - } -} - -export type ServiceShape> = Omit< - T, - keyof Context.ServiceClass.Shape -> - -export abstract class ServiceTagged extends PhantomTypeParameter {} - -export function makeService>(_: Omit) { - return _ as T -} - -export const PureEnvEnv = Symbol() -export interface PureEnvEnv extends ServiceTagged { - env: PureEnv -} - -export function get(): Pure { - return (castTag()).useSync((_) => _.env.state) -} - -export function set(s: S): Pure { - return (castTag()).useSync((_) => { - _.env.state = s - }) -} - -export type PureLogT = Pure - -export function log(w: W): PureLogT { - return (castTag()).useSync((_) => { - _.env.log = Chunk.append(_.env.log, w) - }) -} - -export function logMany(w: Iterable): PureLogT { - return (castTag()).useSync((_) => { - _.env.log = Chunk.appendAll(_.env.log, Chunk.fromIterable(w)) - }) -} - -export function runAll( - self: Effect.Effect>, - s: S4 -): Effect.Effect< - readonly [Chunk.Chunk, Result.Result], - never, - Exclude }> -> { - const a = Effect - .flatMap(self, (x) => - (castTag()) - .useSync( - ({ env: _ }) => ({ log: _.log, state: _.state }) - ) - .pipe( - Effect.map( - ({ log, state }) => Tuple.make(log, Result.succeed(Tuple.make(state, x))) - ) - )) - .pipe(Effect.catch((err: any) => tagg.useSync((env) => Tuple.make(env.env.log, Result.fail(err))))) - return Effect.provide(a, Layer.succeed(tagg, { env: makePureEnv(s) as any }) as any) as any -} - -export function runResult( - self: Effect.Effect>, - s: S4 -) { - return Effect.map(runAll(self, s), ([log, r]) => Tuple.make(log, Result.map(r, ([s]) => s))) -} - -export function runTerm( - self: Effect.Effect>, - s: S4 -) { - return Effect.flatMap( - runAll(self, s), - ([evts, r]) => Effect.map(Effect.fromResult(r), ([s3, a]) => Tuple.make(s3, Chunk.toArray(evts), a)) - ) -} - -export function runTermDiscard( - self: Effect.Effect>, - s: S4 -) { - return Effect.map(runTerm(self, s), ([s3, w3]) => Tuple.make(s3, w3)) -} - -export function runA( - self: Effect.Effect>, - s: S4 -) { - return Effect.map(runAll(self, s), ([log, r]) => Tuple.make(log, Result.map(r, ([, a]) => a))) -} - -export function modify( - mod: (s: S2) => readonly [S3, A] -): Effect.Effect }> { - return (castTag()).useSync((_) => { - const [s, a] = mod(_.env.state) - _.env.state = s as any - return a - }) as any -} - -export function modifyM( - mod: (s: S2) => Effect.Effect> -): Effect.Effect> { - // return serviceWithEffect(_ => Ref.modifyM_(_.state, mod)) - return Effect.flatMap( - (castTag()).useSync((_) => _), - (_: any) => - Effect.map(mod(_.env.state), ([s, a]: any) => { - _.env.state = s - return a - }) - ) as any -} - -export function updateWith(upd: (s: S2) => S3) { - return modify((_: S2) => { - const r = upd(_) - return Tuple.make(r, r) - }) -} - -export function updateWithEffect( - upd: (s: S2, log: (evt: W) => PureLogT) => Effect.Effect> -): Effect.Effect> { - return modifyM((_: S2) => Effect.map(upd(_, log), (_) => Tuple.make(_, _))) -} - -export type FixEnv = - | Exclude> - | PureEnvEnv - -// export function getMA(self: (s: S) => A): Pure { -// return Effect.accessM((_: PureState) => Ref.get(_.state).map(self)) -// } - -// export function getM(self: (s: S) => Pure): Pure { -// return Effect.accessM((_: PureState) => Ref.get(_.state).flatMap(self)) -// } - -// export function getTM__(self: (s: S) => Pure) { -// return (tag: Tag>) => Effect.accessServiceM(tag)(_ => Ref.get(_.state).flatMap(self)) -// } - -// export function getTM_(tag: Tag>, self: (s: S) => Pure) { -// return Effect.accessServiceM(tag)(_ => Ref.get(_.state).flatMap(self)) -// } - -// export function getTM(tag: Tag>) { -// const access = Effect.accessServiceM(tag) -// return (self: (s: S) => Pure) => access(_ => Ref.get(_.state).flatMap(self)) -// } - -// export function makeProgramDSL(): ProgramDSL { -// return makeDSL_() -// } - -// function makeDSL_() { -// const tag = tagg as unknown as Tag> -// const get = Effect.serviceWithEffect(tag, _ => _.env.state.get) -// function getM(self: (s: S2) => Effect.Effect) { return get.flatMap(self) } -// function get_(self: (s: S2) => A) { return get.map(self)} -// function set(s: S2) { -// return Effect.serviceWithEffect(tag, _ => _.env.state.set(s)) -// } -// function log(w: W2): Effect.Effect<{ -// env: PureEnv; -// }, never, void> { -// return Effect.serviceWithEffect(tag, _ => _.env.log.update(l => l.append(w as any))) as any -// } - -// const baseDSL = { -// get, -// log, -// set -// } - -// function modify(mod: (s: S2) => readonly [S3, A]): Effect.Effect<{ -// env: PureEnv; -// }, never, A> { -// return Effect.serviceWithEffect(tag, _ => -// _.env.state.get.map(_ => mod(_)).flatMap(([s, a]) => _.env.state.set(s as any ).map(() => a)) -// ) as any -// } - -// function modifyM(mod: (s: S2) => Effect.Effect): Effect.Effect<{ -// env: PureEnv; -// } | R, E, A> { -// // return serviceWithEffect(_ => Ref.modifyM_(_.state, mod)) -// return Effect.serviceWithEffect(tag, _ => -// _.env.state.get.flatMap(_ => mod(_ as unknown as S2)).flatMap(([s, a]) => _.env.state.set(s as any).map(() => a)) -// ) as any -// } - -// function update(upd: (s: S2) => S3) { -// return modify(_ => Tuple.make(upd(_), void 0 as void)) -// } - -// function updateM(upd: (s: S2) => Effect.Effect) { -// return modifyM(_ => upd(_).map(_ => Tuple.make(_, void 0 as void))) -// } - -// const accessLog = Effect.serviceWithEffect(tag, _ => _.env.log.get) - -// function runAll( -// self: Effect.Effect, E, A>, -// s: S4 -// ): Effect.Effect}>, never, readonly [Chunk, Either.Either]> { -// return self.flatMap(x => -// Effect.serviceWithEffect(tag, ({ env: _ }) => -// Effect.all({ log: _.log.get, state: _.state.get }) -// // Ref.get(_.log).flatMap(log => Ref.get(_.state).map(state => ({ log, state }))) -// ).map( -// ( -// { log, state } -// ) => Tuple.make(log, Either.right(Tuple.make(state, x)) as Either.Either) -// ) -// ).catchAll( -// err => -// accessLog.map(log => -// Tuple.make(log, Either.left(err) as Either.Either) -// ) -// ).provideService(tag, { env: makePureEnv(s) as any }) as any -// } - -// function runResult( -// self: Effect.Effect, E, A>, -// s: S4 -// ) { -// return runAll(self, s).map(([log, r]) => Tuple.make(log, r.map(([s]) => s))) -// } - -// function runA( -// self: Effect.Effect, E, A>, -// s: S4 -// ) { -// return runAll(self, s).map(([log, r]) => Tuple.make(log, r.map(([, a]) => a))) -// } - -// return { -// ...baseDSL, - -// getM, -// get_, -// accessLog, -// runAll, -// runA, -// runResult, - -// modify, -// modifyM, -// update, -// updateM -// } -// } - -// type dsl_ = ReturnType> - -// export interface ProgramDSL extends dsl_ { } - -// export function dslmodifyM(dsl: ProgramDSL, mod: (s: S, dsl: ProgramDSL) => Effect.Effect) { -// return dsl.modifyM(_ => mod(_, dsl)) -// } - -// export function dslmodify(dsl: ProgramDSL, mod: (s: S, dsl: ProgramDSL) => readonly [S, A]) { -// return dsl.modify(_ => mod(_, dsl)) -// } - -// export function dslupdateM(dsl: ProgramDSL, upd: (s: S2, dsl: ProgramDSL) => Effect.Effect) { -// return dsl.updateM(_ => upd(_, dsl)) -// } - -// export function dslupdate(dsl: ProgramDSL, upd: (s: S, dsl: ProgramDSL) => S) { -// return dsl.update(_ => upd(_, dsl)) -// } - -// export interface ZPure {} - -// export class ZPureImpl implements ZPure { -// zipRight( -// f: ZPure -// ): ZPure { -// throw new Error("not implemented") -// } - -// flatMap( -// f: (a: A) => ZPure -// ): ZPure { -// throw new Error("not implemented") - -// } -// } - -// declare type Eff1 = Effect.Effect, never, void> - -// function flatMap(self: Effect.Effect, E, A>, (s: S) => Effect.Effect) { -// } - -// export interface PureEnv2 extends PureState2, PureLog {} - -// const S1 = Symbol() -// const S2 = Symbol() -// const W = Symbol() -// export interface PureState2 { -// readonly [S1]: () => S -// readonly [S2]: () => S2 - -// readonly state: Ref -// } - -// const abc = Do($ => { -// const s = $(Pure.get<{ a: 1 }>()) -// console.log(s) -// // const a = $(Pure.log("hello")) -// $(Pure.log("hallo" as const)) -// $(Pure.log("hello" as const)) -// $(Pure.log<"hallo" | "hello" | 5>(5)) -// }) - -// declare const a: Effect.Effect< -// | { -// env: PureEnv -// } -// | { env: PureEnv } -// | { env: PureEnv }, -// never, -// never -// > -// type R = _R -// type Env = T extends { env: infer rr } ? rr : never - -// type RRRR = Env - -// const abcc = unifyPureEnv( -// undefined as any as RRRR -// ) -// const test = Pure.runAll( -// abc, -// { a: 1 } -// ) diff --git a/packages/effect-app/src/QueueMaker.ts b/packages/effect-app/src/QueueMaker.ts deleted file mode 100644 index bfcf6213e..000000000 --- a/packages/effect-app/src/QueueMaker.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RequestContext } from "./RequestContext.ts" - -/** - * Per-message context envelope (tenant/store id, request metadata) carried - * alongside message-schema payloads. Alias of {@link RequestContext}. - * - * All that remains of the former queue machinery (SB/SQLite/mem `QueueMaker` - * implementations and the `QueueBase` interface) after the move to cluster - * entities — the message schemas that embed it still reference this envelope. - */ -export const QueueMeta = RequestContext diff --git a/packages/effect-app/src/RequestContext.ts b/packages/effect-app/src/RequestContext.ts deleted file mode 100644 index b8c6a7b2b..000000000 --- a/packages/effect-app/src/RequestContext.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as Context from "./Context.ts" -import { UserProfileId } from "./ids.ts" -import * as S from "./Schema.ts" -import { NonEmptyString255 } from "./Schema.ts" - -export const Locale = S.Literals(["en", "de"]) -export type Locale = typeof Locale.Type - -export class LocaleRef extends Context.Reference("Locale", { defaultValue: (): Locale => "en" }) {} - -export class RequestContext extends S.Opaque()(S.Struct({ - span: S.Struct({ - traceId: S.String, - spanId: S.String, - sampled: S.Boolean - }), - name: NonEmptyString255, - locale: Locale, - sourceId: S.optional(NonEmptyString255), // TODO? - namespace: NonEmptyString255, - /** @deprecated */ - userProfile: S.optional(S.Struct({ sub: UserProfileId })) // -})) { - static toMonitoring(this: void, self: RequestContext) { - return { - operationName: self.name, - locale: self.locale - } - } -} - -export const spanAttributes = (ctx: Pick & Partial) => ({ - "code.function.name": ctx.name, - "app.locale": ctx.locale, - "app.tenant.id": ctx.namespace, - ...ctx.sourceId ? { "client.id": ctx.sourceId } : {}, - ...(ctx.userProfile?.sub - ? { - "user.id": ctx - .userProfile - .sub, - "user.roles": "roles" in ctx - .userProfile - ? ctx.userProfile.roles - : undefined - } - : {}) -}) diff --git a/packages/effect-app/src/RequestScopedDependencies.ts b/packages/effect-app/src/RequestScopedDependencies.ts deleted file mode 100644 index e045f7a56..000000000 --- a/packages/effect-app/src/RequestScopedDependencies.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as Data from "effect/Data" -import type * as Scope from "effect/Scope" -import * as Context from "./Context.ts" -import * as Effect from "./Effect.ts" -import * as Layer from "./Layer.ts" - -export type Root = "root" - -export class RequestScopedDependencyNotStartedError extends Data.TaggedError( - "RequestScopedDependencyNotStartedError" -)<{ readonly key: string }> {} - -export interface RequestScopedDependency extends Context.Reference { - readonly current: Effect.Effect - readonly layer: Layer.Layer -} - -export const access = ( - dependency: Context.Reference, - key: string -): Effect.Effect => - dependency.pipe( - Effect.filterOrFail((_): _ is Service => _ !== "root", () => new RequestScopedDependencyNotStartedError({ key })) - ) - -export const make = ( - key: string, - service: Effect.Effect -): RequestScopedDependency> => { - const dependency = Context.Reference(key, { defaultValue: () => "root" }) - return Object.assign(dependency, { - current: access(dependency, key), - layer: Layer.effect(dependency, service) - }) -} - -export const layer = < - Deps extends readonly [RequestScopedDependency, ...Array>] ->( - ...dependencies: Deps -): Layer.Layer< - never, - { [K in keyof Deps]: Deps[K] extends RequestScopedDependency ? E : never }[number], - { [K in keyof Deps]: Deps[K] extends RequestScopedDependency ? R : never }[number] -> => dependencies.map((_) => _.layer).reduce((acc, l) => Layer.merge(acc, l)) as any diff --git a/packages/effect-app/src/Schema.ts b/packages/effect-app/src/Schema.ts deleted file mode 100644 index 01eecece8..000000000 --- a/packages/effect-app/src/Schema.ts +++ /dev/null @@ -1,523 +0,0 @@ -import * as S from "effect/Schema" -import * as EffectSchemaParser from "effect/SchemaParser" -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 { 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" -import { copy, defineOwn, extendM, type StructuralCopyOrigin } from "./utils.ts" - -// --------------------------------------------------------------------------- -// Default helpers — re-exported from effect/Schema -// -// The five helpers below are surfaced explicitly so the (important) policy -// around them lives next to the export. See also the file-level note in -// `./Schema/ext.ts` and the documented wrappers in -// `./Schema/ext.ts`, `./Schema/numbers.ts`, `./Schema/moreStrings.ts`, -// and `./ids.ts`. -// -// **Construction-only**: `withConstructorDefault` fills the field when it -// is omitted from `.make(...)` input. It is NOT applied during decode, so -// it CANNOT be used to just-in-time migrate database fields. A stored -// record missing the field will still fail to decode. -// -// **`withDecodingDefault*` is discouraged**: a missing field in persisted -// data is just as likely to be data corruption as it is an old-shape -// document; silently substituting a default hides the problem and can -// poison downstream aggregates. Prefer an explicit, preferably versioned -// migration of database data over shoving missing fields under the rug. -// --------------------------------------------------------------------------- - -/** - * Attach a default value used **only** when constructing a value (e.g. via - * `.make(...)` or struct constructors) and the field is omitted from input. - * - * **Not applied during decode.** Decoding a payload that is missing the - * field will still raise a parse error. Do **not** rely on this to migrate - * database fields just-in-time — see the section header above. - * - * @see {@link withDecodingDefault} / {@link withDecodingDefaultType} — - * decode-time variants (discouraged for persisted data; use explicit, - * versioned migrations instead). - */ -export { withConstructorDefault } from "effect/Schema" - -/** - * Attach a default value used during decode when the field's `Encoded` value - * is missing **or** `undefined`. The default is specified as an `Encoded` - * value and threaded through the schema's decode step. - * - * **Discouraged for persisted data.** A missing field in a stored record is - * just as likely to be data corruption as it is an old-shape document; - * silently substituting a default hides the problem. Prefer an explicit, - * preferably versioned migration of database data — see the section header - * above. - * - * @see {@link withDecodingDefaultKey} — key-absent-only variant - * @see {@link withDecodingDefaultType} — `Type`-side variant - * @see {@link withConstructorDefault} — for `.make(...)`-time defaults - */ -export { withDecodingDefault } from "effect/Schema" - -/** - * Attach a default value used during decode when the field **key is absent** - * (note: not when present and `undefined`). The default is an `Encoded` - * value. - * - * **Discouraged for persisted data** — same reasoning as - * {@link withDecodingDefault}. Use explicit, preferably versioned migrations - * over decode-time fallbacks. - * - * @see {@link withDecodingDefault} — value-absent-or-undefined variant - * @see {@link withDecodingDefaultTypeKey} — `Type`-side variant - * @see {@link withConstructorDefault} — for `.make(...)`-time defaults - */ -export { withDecodingDefaultKey } from "effect/Schema" - -/** - * Attach a default value used during decode when the field is missing **or** - * `undefined`. The default is specified as a `Type` value (i.e. on the - * decoded side). - * - * **Discouraged for persisted data** — same reasoning as - * {@link withDecodingDefault}. Use explicit, preferably versioned migrations - * over decode-time fallbacks. - * - * @see {@link withDecodingDefault} — `Encoded`-side variant - * @see {@link withDecodingDefaultTypeKey} — key-absent-only variant - * @see {@link withConstructorDefault} — for `.make(...)`-time defaults - */ -export { withDecodingDefaultType } from "effect/Schema" - -/** - * Attach a default value used during decode when the field **key is absent** - * (note: not when present and `undefined`). The default is a `Type` value. - * - * **Discouraged for persisted data** — same reasoning as - * {@link withDecodingDefault}. Use explicit, preferably versioned migrations - * over decode-time fallbacks. - * - * @see {@link withDecodingDefaultKey} — `Encoded`-side variant - * @see {@link withDecodingDefaultType} — value-absent-or-undefined variant - * @see {@link withConstructorDefault} — for `.make(...)`-time defaults - */ -export { withDecodingDefaultTypeKey } from "effect/Schema" - -export * from "effect/Schema" - -export * from "./Schema/Class.ts" -export { Class, ErrorClass, Opaque, OpaqueErrorFacadeClass, OpaqueFacade, OpaqueShape, OpaqueType, TaggedClass, TaggedErrorClass } from "./Schema/Class.ts" - -export { fromBrand, nominal } from "./Schema/brand.ts" -export { Array, Boolean, Date, DateFromString, Finite, Literals, NullOr, Number, ReadonlyMap, ReadonlySet } from "./Schema/ext.ts" -export { Int, NonNegativeInt } from "./Schema/numbers.ts" - -export * from "./Schema/email.ts" -export * from "./Schema/ext.ts" -export * from "./Schema/moreStrings.ts" -export * from "./Schema/numbers.ts" -export * from "./Schema/phoneNumber.ts" -export * from "./Schema/schema.ts" -export * from "./Schema/SpecialJsonSchema.ts" -export * from "./Schema/SpecialOpenApi.ts" -export * from "./Schema/strings.ts" -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 -) -export * as SchemaParser from "./Schema/SchemaParser.ts" - -/** - * Schema for a TypeScript `void` return value — the canonical `Void` used - * across the app, including RPC success schemas. Accepts **any present value** - * at runtime and discards it to `undefined`, while keeping the decoded/encoded - * type as `void`. Use `S.Undefined` when the input must be exactly `undefined`. - */ -export interface Void extends S.Void {} -export const Void: Void = S.Void - -// --------------------------------------------------------------------------- -// Struct / NonEmptyArray / Record -// --------------------------------------------------------------------------- - -export function Struct( - fields: Fields -): Struct { - const result = S.Struct(fields).annotate(concurrencyUnbounded) - const allowVoidMake = (schema: any): any => { - // Normalize omitted input to an empty object so optional/default-only structs can be constructed with make(). - // Do not read schema.make first: Effect 4 caches it as a non-writable own property. - const origMake = EffectSchemaParser.make(schema) - const origMakeOption = EffectSchemaParser.makeOption(schema) - const origMakeEffect = EffectSchemaParser.makeEffect(schema) - const wrap = (orig: (input: any, options?: any) => any) => - function(this: any, input: any, options?: any) { - return orig.call(this, input === undefined ? {} : input, options) - } - defineOwn(schema, "make", wrap(origMake)) - defineOwn(schema, "makeOption", wrap(origMakeOption)) - defineOwn(schema, "makeEffect", wrap(origMakeEffect)) - return schema - } - // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-assignment - const origMapFields: any = result.mapFields - // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-assignment - const origAnnotate: any = result.annotate - // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-assignment - const origAnnotateKey: any = result.annotateKey - - const preserveCopyAndMethods = (schema: any): any => { - schema.copy = copy - schema.mapFields = function(this: any, f: any, options?: any) { - return (result as any).mapFields.call(this, f, options) - } - schema.annotate = function(this: any, annotations?: any) { - return (result as any).annotate.call(this, annotations) - } - schema.annotateKey = function(this: any, annotations?: any) { - return (result as any).annotateKey.call(this, annotations) - } - return allowVoidMake(schema) - } - ;(result as any).mapFields = function(this: any, f: any, options?: any) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const mapped = origMapFields.call(this, f, options).annotate(concurrencyUnbounded) - return preserveCopyAndMethods(mapped) - } - ;(result as any).annotate = function(this: any, annotations?: any) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const annotated = origAnnotate.call(this, annotations) - return preserveCopyAndMethods(annotated) - } - ;(result as any).annotateKey = function(this: any, annotations?: any) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const annotated = origAnnotateKey.call(this, annotations) - return preserveCopyAndMethods(annotated) - } - ;(result as any).copy = copy - allowVoidMake(result) - return result as Struct -} - -export interface Struct extends - S.Bottom< - Struct.Type, - Struct.Encoded, - Struct.DecodingServices, - Struct.EncodingServices, - AST.Objects, - // Rebuild is what's returned from annotate etc - Struct, - Struct.MakeIn, - Struct.Iso - > -{ - /** - * The field definitions of this struct. Spread them into a new struct to - * reuse fields across schemas. - * - * **Example** (Reusing fields across structs) - * - * ```ts - * import * as Schema from "effect/Schema" - * - * const Timestamped = Schema.Struct({ - * createdAt: Schema.Date, - * updatedAt: Schema.Date - * }) - * - * const User = Schema.Struct({ - * ...Timestamped.fields, - * name: Schema.String, - * email: Schema.String - * }) - * ``` - */ - readonly fields: Fields - /** - * Returns a new struct with the fields modified by the provided function. - * - * **Options** - * - * - `unsafePreserveChecks` - if `true`, keep any `.check(...)` constraints - * that were attached to the original union. Defaults to `false`. - * - * **Warning**: This is an unsafe operation. Since `mapFields` - * transformations change the schema type, the original refinement functions - * may no longer be valid or safe to apply to the transformed schema. Only - * use this option if you have verified that your refinements remain correct - * after the transformation. - */ - mapFields( - f: (fields: Fields) => To, - options?: { - readonly unsafePreserveChecks?: boolean | undefined - } | undefined - ): Struct>> - - // added copy - readonly copy: StructuralCopyOrigin> -} - -export declare namespace Struct { - export type Fields = S.Struct.Fields - export type Type = S.Struct.Type - export type Encoded = S.Struct.Encoded - export type DecodingServices = S.Struct.DecodingServices - export type EncodingServices = S.Struct.EncodingServices - // changed; all optional allows void - export type MakeIn = RequiredKeys> extends never - ? void | S.Struct.MakeIn - : S.Struct.MakeIn - export type Iso = S.Struct.Iso -} - -export type StructNestedEncodedError = { - readonly _tag: "StructNestedEncodedError" - readonly message: "Expected a Struct schema or a schema with from.Encoded" - readonly schema: T -} - -export type StructNestedEncoded = T extends { fields: infer Fields extends S.Struct.Fields } ? Struct.Encoded - : T extends { readonly from: { readonly Encoded: infer Encoded } } ? Encoded - : StructNestedEncodedError - -export function NonEmptyArray(value: Value): S.NonEmptyArray { - return S.NonEmptyArray(value).annotate(concurrencyUnbounded) -} - -export function TaggedStruct( - value: Tag, - fields: Fields -): TaggedStruct { - return Struct({ _tag: S.tag(value), ...fields }) as any -} -export interface TaggedStruct - extends Struct<{ readonly _tag: S.tag } & Fields> -{} -export declare namespace TaggedStruct { - export type Fields = S.Struct.Fields - export type Type = S.Struct.Type< - { readonly _tag: S.tag } & F - > - export type Encoded = S.Struct.Encoded< - { readonly _tag: S.tag } & F - > - export type DecodingServices = - S.Struct.DecodingServices< - { readonly _tag: S.tag } & F - > - export type EncodingServices = - S.Struct.EncodingServices< - { readonly _tag: S.tag } & F - > - export type MakeIn = S.Struct.MakeIn< - { readonly _tag: S.tag } & F - > - export type Iso = S.Struct.Iso< - { readonly _tag: S.tag } & F - > -} - -export function Record( - key: Key, - value: Value -): S.$Record { - return S.Record(key, value).annotate(concurrencyUnbounded) -} -export declare namespace Record { - export type Key = S.Record.Key - export type Type = S.Record.Type - export type Encoded = S.Record.Encoded -} - -export const SpanId = Symbol() -export type SpanId = typeof SpanId - -export interface WithOptionalSpan { - [SpanId]?: Tracer.Span -} - -export const Email = EmailT.pipe(withDefaultMake) - -export type Email = EmailType - -export const PhoneNumber = PhoneNumberT.pipe(withDefaultMake) - -export type PhoneNumber = PhoneNumberType - -// Copied from SchemaAST.collectSentinels (marked @internal in effect). -// Returns all { key, literal } pairs that can discriminate a union member. -const getTagFromAST = (schema: S.Top): string => { - const sentinels = collectSentinelsFromAST(schema.ast) - const sentinel = sentinels.find((s) => s.key === "_tag") - if (sentinel !== undefined && typeof sentinel.literal === "string") return sentinel.literal - throw new Error("No _tag literal found on schema member") -} - -function collectSentinelsFromAST( - ast: SchemaAST.AST -): Array<{ key: PropertyKey; literal: SchemaAST.LiteralValue | symbol }> { - switch (ast._tag) { - case "Declaration": { - const s = ast.annotations?.["~sentinels"] - return Array.isArray(s) ? s : [] - } - case "Objects": - return ast.propertySignatures.flatMap( - (ps): Array<{ key: PropertyKey; literal: SchemaAST.LiteralValue | symbol }> => { - const type = ps.type - if (!SchemaAST.isOptional(type)) { - if (SchemaAST.isLiteral(type)) return [{ key: ps.name, literal: type.literal }] - if (SchemaAST.isUniqueSymbol(type)) return [{ key: ps.name, literal: type.symbol }] - } - return [] - } - ) - case "Suspend": - return collectSentinelsFromAST(ast.thunk()) - default: - return [] - } -} - -export const tags = < - Members extends NonEmptyReadonlyArray<(S.Top & { readonly Type: { readonly _tag: string } })> ->( - self: Members -) => - S.Literals( - self.map(getTagFromAST) as { - [Index in keyof Members]: Members[Index]["Type"]["_tag"] - } - ) as S.Literals< - { - [Index in keyof Members]: Members[Index]["Type"]["_tag"] - } - > - -type TaggedUnionMembers = NonEmptyReadonlyArray< - S.Top & { readonly Type: { readonly _tag: string } } -> - -type TaggedUnionTags = S.Literals< - { - [Index in keyof Members]: Members[Index]["Type"]["_tag"] - } -> - -type TaggedPropertyKeys = { - [K in keyof A & string]: A[K] extends Members[number]["Type"] ? K : never -}[keyof A & string] - -type PropertyGuardsFor< - Members extends TaggedUnionMembers, - K extends string, - A -> = - & { - readonly [M in Members[number] as `is${M["Type"]["_tag"]}`]: ( - target: A - ) => target is A & { readonly [P in K]: M["Type"] } - } - & { - readonly isAnyOf: >( - tags: Tags - ) => ( - target: A - ) => target is A & { readonly [P in K]: Extract } - } - -type PropertyGuards< - Members extends TaggedUnionMembers, - K extends string -> = - & { - readonly [M in Members[number] as `is${M["Type"]["_tag"]}`]: < - T extends { readonly [P in K]: Members[number]["Type"] } - >(target: T) => target is T & { readonly [P in K]: M["Type"] } - } - & { - readonly isAnyOf: >( - tags: Tags - ) => ( - target: T - ) => target is T & { readonly [P in K]: Extract } - } - -type TaggedUnionWithTags = S.toTaggedUnion<"_tag", Members> & { - readonly tags: TaggedUnionTags - readonly generateGuards: (property: K) => PropertyGuards - readonly generateGuardsFor: () => >( - property: K - ) => PropertyGuardsFor -} - -// Mirrors effect `Schema.toTaggedUnion`: duplicate `_tag` values, including `1` and `"1"`, are rejected. -type TaggedFor = { readonly Type: { readonly [K in Tag]: PropertyKey } } -type FlattenTaggedMembers = Schemas extends readonly [infer Head, ...infer Tail] - ? Head extends S.Union ? [...FlattenTaggedMembers, ...FlattenTaggedMembers] - : [Head, ...FlattenTaggedMembers] - : [] -type DiscriminantKey = T extends number ? `${T}` : T -type EnsureUniqueDiscriminants, Seen = never> = Tags extends - readonly [infer Head extends PropertyKey, ...infer Tail extends ReadonlyArray] - ? [DiscriminantKey & Seen] extends [never] ? EnsureUniqueDiscriminants> - : never - : unknown -type TaggedDiscriminants< - Members extends ReadonlyArray>, - Flattened extends ReadonlyArray> = FlattenTaggedMembers -> = { readonly [I in keyof Flattened]: Flattened[I]["Type"]["_tag"] } -type UniqueTaggedUnion = - & S.Union - & NoInfer>> - -const extendTaggedUnionWithTags = ( - schema: UniqueTaggedUnion -): TaggedUnionWithTags => - extendM(schema.pipe(S.toTaggedUnion("_tag")), (tagged) => { - const makeGuards = (property: string) => { - const result: any = {} - const guards: Record boolean> = tagged.guards - for (const tag of Object.keys(guards)) { - const guard = guards[tag]! - result[`is${tag}`] = (target: any) => guard(target[property]) - } - result.isAnyOf = (memberTags: Array) => { - const check = tagged.isAnyOf(memberTags) - return (target: any) => check(target[property]) - } - return result - } - return { - tags: tags(schema.members), - generateGuards: makeGuards, - generateGuardsFor: () => makeGuards - } - }) - -export const ExtendTaggedUnion = ( - schema: UniqueTaggedUnion -): TaggedUnionWithTags => extendTaggedUnionWithTags(schema) - -export const TaggedUnion = < - Members extends TaggedUnionMembers ->( - members: Members & NoInfer>> -): TaggedUnionWithTags => - // `Schema.Union` does not carry the uniqueness proof checked on `members`. - extendTaggedUnionWithTags(S.Union(members) as UniqueTaggedUnion) diff --git a/packages/effect-app/src/Schema/Class.ts b/packages/effect-app/src/Schema/Class.ts deleted file mode 100644 index a13163e22..000000000 --- a/packages/effect-app/src/Schema/Class.ts +++ /dev/null @@ -1,683 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type * as Cause from "effect/Cause" -import * as Effect from "effect/Effect" -import * as S from "effect/Schema" -import * as SchemaGetter from "effect/SchemaGetter" -import * as SchemaIssue from "effect/SchemaIssue" -import * as SchemaTransformation from "effect/SchemaTransformation" -import type { Struct as EffectAppStruct } from "../Schema.ts" -import * as SchemaAST from "../SchemaAST.ts" -import { copyOrigin } from "../utils.ts" -import { concurrencyUnbounded } from "./ext.ts" -import * as SchemaParser from "./SchemaParser.ts" - -type ClassAnnotations = S.Annotations.Declaration - -export interface EnhancedClass< - Self, - SchemaS extends S.Constraint & { readonly fields: S.Struct.Fields }, - Inherited -> extends S.Class { - /** - * See `copyOrigin` docs in `utils.ts` for return-type design details. - */ - readonly copy: ReturnType Self>> -} -type MissingSelfGeneric = - `Missing \`Self\` generic - use \`class Self extends ${Usage}()(${Params}{ ... })\`` - -type HasFields = { - readonly fields: Fields -} | { - readonly from: HasFields -} - -type ClassOptions = { - readonly strict?: boolean -} - -export declare const ExtendedSchemaNoEncoded: unique symbol - -export type ExtendedSchemaNoEncoded = typeof ExtendedSchemaNoEncoded - -/** - * Override schema `Encoded` while keeping `Constraint` (intersection, not `Omit`). - * Free type-params for `Encoded` keep the conditional unresolved — callers that - * need Encoded overrides use the intersection form of the class factory return type. - */ -type WithEncoded = SchemaS & { readonly Encoded: Encoded } - -type ExtendedSchema = [ - Encoded -] extends [ExtendedSchemaNoEncoded] ? SchemaS - : WithEncoded - -/** Struct fields schema, optionally with a caller-supplied Encoded override. */ -type ClassFieldsSchema = ExtendedSchema, Encoded> - -export type Class = EnhancedClass< - Self, - S, - Inherited -> - -/** - * Build a modified Declaration that accepts struct-matching values during - * encoding, given the original Declaration and the class's fields. - */ -function makeRelaxedDeclaration( - ast: SchemaAST.Declaration, - fields: S.Struct.Fields, - cls: any -): SchemaAST.Declaration { - const parseOptions = ast.annotations?.["parseOptions"] as SchemaAST.ParseOptions | undefined - const structSchema = S.Struct(fields) - const annotatedStruct = parseOptions ? S.toType(structSchema).annotate({ parseOptions }) : S.toType(structSchema) - const decodeStruct = SchemaParser.decodeUnknownEffect(annotatedStruct) - - return new SchemaAST.Declaration( - ast.typeParameters, - () => (input: unknown, self: SchemaAST.Declaration, options: SchemaAST.ParseOptions) => { - if (input instanceof cls) { - return Effect.succeed(input) - } - if (input !== null && typeof input === "object") { - return decodeStruct(input, options) - } - return Effect.fail(new SchemaIssue.InvalidType(self, input)) - }, - ast.annotations, - ast.checks, - ast.encoding, - ast.context - ) -} - -// --------------------------------------------------------------------------- -// Class — like Schema.Class but with relaxed encoding -// --------------------------------------------------------------------------- - -/** - * Like `Schema.Class`, but the resulting class accepts plain objects matching - * the struct schema during encoding — not only `instanceof` or type-id - * checks. - * - * @example - * ```ts - * import * as Schema from "effect/Schema" - * import { Class } from "./Class.ts" - * - * class A extends Class("A")({ a: Schema.String }) {} - * - * // Construction works as normal: - * new A({ a: "hello" }) - * - * // Encoding accepts plain objects: - * Schema.encodeUnknownSync(A)({ a: "hello" }) // { a: "hello" } - * ``` - */ -export const Class: ( - identifier: string -) => ( - fieldsOr: Fields | HasFields, - annotations?: ClassAnnotations, - options?: ClassOptions -) => [Self] extends [never] ? MissingSelfGeneric<"Class"> - : EnhancedClass< - Self, - ClassFieldsSchema, - Brand - > = (identifier) => (fields, annotations, options) => { - const relaxed = options?.strict === false - // Build the original Schema.Class - const Base = (S.Class as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations }) - // Get the original ast getter from the base class - const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! - - // Cache per-class to avoid recomputing - const astCache = new WeakMap() - const copyCache = new WeakMap>() - - return class extends Base { - static get copy() { - let cached = copyCache.get(this) - if (cached === undefined) { - cached = copyOrigin(this) - copyCache.set(this, cached) - } - return cached - } - static get ast(): SchemaAST.Declaration { - let cached = astCache.get(this) - if (cached !== undefined) return cached - // Call the original getter with `this` bound to the actual user class, - // so getClassSchema(this) creates a schema that uses `new this(...)`. - const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration - cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst - astCache.set(this, cached) - return cached - } - static mapFields(f: any, options?: any) { - return Base.mapFields(f, options).annotate(concurrencyUnbounded) - } - } as any - } - -// --------------------------------------------------------------------------- -// TaggedClass — like Schema.TaggedClass but with relaxed encoding -// --------------------------------------------------------------------------- - -/** - * Like `Schema.TaggedClass`, but the resulting class accepts plain objects - * matching the struct schema during encoding. - * - * @example - * ```ts - * import * as Schema from "effect/Schema" - * import { TaggedClass } from "./Class.ts" - * - * class Circle extends TaggedClass()("Circle", { - * radius: Schema.Number - * }) {} - * - * Schema.encodeUnknownSync(Circle)({ _tag: "Circle", radius: 5 }) - * ``` - */ -export const TaggedClass: ( - identifier?: string -) => ( - tag: Tag, - fieldsOr: Fields | HasFields, - annotations?: ClassAnnotations, - options?: ClassOptions -) => [Self] extends [never] ? MissingSelfGeneric<"TaggedClass"> - : EnhancedClass< - Self, - ClassFieldsSchema<{ readonly _tag: S.tag } & Fields, Encoded>, - Brand - > = (identifier) => (tag, fields, annotations, options) => { - const relaxed = options?.strict === false - const Base = (S.TaggedClass as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations }) - const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! - const astCache = new WeakMap() - const copyCache = new WeakMap>() - - return class extends Base { - static get copy() { - let cached = copyCache.get(this) - if (cached === undefined) { - cached = copyOrigin(this) - copyCache.set(this, cached) - } - return cached - } - static get ast(): SchemaAST.Declaration { - let cached = astCache.get(this) - if (cached !== undefined) return cached - const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration - cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst - astCache.set(this, cached) - return cached - } - static mapFields(f: any, options?: any) { - return Base.mapFields(f, options).annotate(concurrencyUnbounded) - } - } as any - } - -// --------------------------------------------------------------------------- -// ErrorClass — like Schema.ErrorClass but with relaxed encoding -// --------------------------------------------------------------------------- - -export const ErrorClass: ( - identifier: string -) => ( - fieldsOr: Fields | HasFields, - annotations?: ClassAnnotations, - options?: ClassOptions -) => [Self] extends [never] ? MissingSelfGeneric<"ErrorClass"> - : EnhancedClass< - Self, - ClassFieldsSchema, - Cause.YieldableError & Brand - > = (identifier) => (fields, annotations, options) => { - const relaxed = options?.strict === false - const Base = (S.Error as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations }) - const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! - const astCache = new WeakMap() - const copyCache = new WeakMap>() - - return class extends Base { - static get copy() { - let cached = copyCache.get(this) - if (cached === undefined) { - cached = copyOrigin(this) - copyCache.set(this, cached) - } - return cached - } - static get ast(): SchemaAST.Declaration { - let cached = astCache.get(this) - if (cached !== undefined) return cached - const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration - cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst - astCache.set(this, cached) - return cached - } - static mapFields(f: any, options?: any) { - return Base.mapFields(f, options).annotate(concurrencyUnbounded) - } - } as any - } - -// --------------------------------------------------------------------------- -// TaggedErrorClass — like Schema.TaggedErrorClass but with relaxed encoding -// --------------------------------------------------------------------------- - -export const TaggedErrorClass: ( - identifier?: string -) => ( - tag: Tag, - fieldsOr: Fields | HasFields, - annotations?: ClassAnnotations, - options?: ClassOptions -) => [Self] extends [never] ? MissingSelfGeneric<"TaggedErrorClass"> - : EnhancedClass< - Self, - ClassFieldsSchema<{ readonly _tag: S.tag } & Fields, Encoded>, - Cause.YieldableError & Brand - > = (identifier) => (tag, fields, annotations, options) => { - const relaxed = options?.strict === false - const Base = (S.TaggedError as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations }) - const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! - const astCache = new WeakMap() - const copyCache = new WeakMap>() - - return class extends Base { - static get copy() { - let cached = copyCache.get(this) - if (cached === undefined) { - cached = copyOrigin(this) - copyCache.set(this, cached) - } - return cached - } - static get ast(): SchemaAST.Declaration { - let cached = astCache.get(this) - if (cached !== undefined) return cached - const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration - cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst - astCache.set(this, cached) - return cached - } - static mapFields(f: any, options?: any) { - return Base.mapFields(f, options).annotate(concurrencyUnbounded) - } - } as any - } - -export interface Opaque extends S.Opaque { - readonly "Encoded": Encoded extends ExtendedSchemaNoEncoded ? SchemaS["Encoded"] : Encoded -} - -export const Opaque: () => ( - schema: S -) => Opaque & Omit = S.Opaque as any - -/** - * Like {@link Opaque}, but the class **instance type is exactly `Self`** (the supplied - * decoded `Type`) instead of the structurally-computed `struct["Type"] & Brand`. - * - * Stock `Opaque` types the instance as `struct["Type"] & Brand` and only overrides the - * schema's `Type`/`Encoded` *members* with `Self`/`Encoded`. So `make`/`copy`/consumers - * still resolve the struct's mapped `Type`. With a codegen-supplied pre-expanded literal - * `Type` interface, `OpaqueType` lets all of those resolve a single **named** interface - * (resolved once per checker) instead of re-deriving the mapped `Type` — cutting - * instantiation on `Type`-touching consumers. - * - * Use with `class X extends OpaqueType()(struct) {}` where `X.Type` - * and `X.Encoded` are generated literal interfaces (see `@effect-app/eslint-codegen-model`, - * `static` + `type` mode). - * - * KNOWN GAP — **no branding**: the instance type is a plain structural `Self`, so opaque - * types of identical shape are mutually assignable. (Stock `Opaque` is also structural by - * default — `Brand` defaults to `{}` — so this only differs if you passed a non-default - * `Brand`.) Re-introducing a nominal brand on top of a supplied `Self` (e.g. branding the - * generated `Type` interface) is not yet implemented. - * - * NOTE: only `Type` (via `Self`) and `Encoded` are supplied statically here; `make`'s input - * (`~type.make.in`) and other derived members are still computed from the struct. See - * a future `OpaqueShape`-style helper if those also need to be supplied. - */ -export interface OpaqueType extends S.Opaque { - readonly "Encoded": Encoded extends ExtendedSchemaNoEncoded ? SchemaS["Encoded"] : Encoded - new(_: never): Self -} - -export const OpaqueType: () => ( - schema: S -) => OpaqueType & Omit = S.Opaque as any - -type OpaqueFacadeConstructorArgs = {} extends MakeIn ? [props?: MakeIn, options?: S.MakeOptions] - : [props: MakeIn, options?: S.MakeOptions] - -// Only the codec channels are required. `copy`/`fields`/`mapFields` are NOT -// required here: transformed schemas (`.pipe(S.encodeKeys/annotate/filter/...)`) -// don't expose them at the static level, and requiring them would reject those -// models from facading. When present they still flow through via -// `OpaqueFacadeStatics`; when absent the facade simply doesn't offer them. -type OpaqueFacadeInput = S.Top & { - readonly DecodingServices: DecodingServices - readonly EncodingServices: EncodingServices -} - -type OpaqueClassFacadeInput = - & OpaqueFacadeInput< - DecodingServices, - EncodingServices - > - & (abstract new(...args: Array) => unknown) - -type PrototypeFunction = Function & { prototype: object } - -// Class schemas expose a constructor descriptor under this key (was previously -// identified via a `~effect/Schema/Class` annotation id). -const ConstructorAnnotationKey = "~constructor" - -const getOwnOrInheritedPropertyDescriptor = (value: object, property: PropertyKey): PropertyDescriptor | undefined => { - let current: object | null = value - while (current !== null) { - const descriptor = Object.getOwnPropertyDescriptor(current, property) - if (descriptor !== undefined) return descriptor - current = Object.getPrototypeOf(current) - } -} - -const isClassSchemaConstructor = (value: unknown): value is PrototypeFunction => { - if (typeof value !== "function") return false - // Transformed schemas (e.g. `Class.pipe(S.encodeKeys(...))`) are also functions with a - // Declaration AST that still carries `~constructor`, but they are not constructable classes. - // Real Schema.Class constructors expose a static `identifier`. - if (typeof (value as { readonly identifier?: unknown }).identifier !== "string") return false - const ast = (value as { readonly ast?: unknown }).ast - return SchemaAST.isAST(ast) - && SchemaAST.isDeclaration(ast) - && ast.annotations?.[ConstructorAnnotationKey] !== undefined -} - -const getFacadeClassSchema = (schema: OpaqueFacadeInput): PrototypeFunction | undefined => { - if (isClassSchemaConstructor(schema)) return schema - const target = "to" in schema ? (schema as { readonly to?: unknown }).to : undefined - return isClassSchemaConstructor(target) ? target : undefined -} - -const makePublicClassTransformation = (Public: PrototypeFunction) => - new SchemaTransformation.Transformation( - SchemaGetter.transform((input) => Reflect.construct(Public, [input], Public)), - SchemaGetter.passthrough() - ) - -const makeFacadeClassAst = ( - schema: OpaqueFacadeInput, - originalAst: SchemaAST.AST, - Public: PrototypeFunction -): SchemaAST.AST => { - if (SchemaAST.isDeclaration(schema.ast)) { - const schemaEncoding = schema.ast.encoding ?? [] - if (schemaEncoding.length > 0) { - return new SchemaAST.Declaration( - schema.ast.typeParameters, - schema.ast.run, - schema.ast.annotations, - schema.ast.checks, - [new SchemaAST.Link(schemaEncoding[0]!.to, makePublicClassTransformation(Public)), ...schemaEncoding.slice(1)], - schema.ast.context, - schema.ast.encodingChecks - ) - } - } - - return schema.rebuild(originalAst).ast -} - -// Carry the schema's own statics, dropping the generic `S.Top` machinery and -// `prototype`. EXCEPT `to`: models compose each other via `X.to.fields` / -// `X.to.copy` at definition time, so it must survive on the facade. -type OpaqueFacadeStatics = Omit | "prototype"> - -/** - * Like {@link OpaqueType}, but ALSO supplies a static **make-input** shape, so - * `make`/`copy` resolve a named `MakeIn` interface instead of re-deriving the struct's - * mapped `~type.make.in`. Use with codegen-supplied `X.Type` / `X.Encoded` / `X.Make`: - * - * ```ts - * class X extends OpaqueShape()(struct) {} - * ``` - * - * `decode`/`encode` already use the supplied `Type`/`Encoded` (they read the schema's - * `Type`/`Encoded` members, which are `Self`/`Encoded` here) — no separate override needed. - * - * NOTE — measured gain is modest (~5%): the struct passed to the wrapper is still - * constructed in full (definition cost dominates); the static shapes only cheapen - * consumer-side reads of `Type`/`Encoded`/`MakeIn`. Same `no-branding` gap as - * {@link OpaqueType}. - */ -export interface OpaqueShape - extends S.Opaque -{ - readonly "Encoded": Encoded - readonly "~type.make.in": MakeIn - new(_: never): Self -} - -export const OpaqueShape: () => ( - schema: S -) => OpaqueShape & Omit = S.Opaque as any - -/** - * Shallow public view for generated model facades. - * - * The runtime value can still be the full private schema class, but emitted - * declarations expose only named `Type` / `Encoded` / `Make` interfaces and a - * small static surface. This keeps downstream project references from pulling - * the private struct field map back through `typeof Model`. - */ -export interface OpaqueFacade< - Self, - Encoded, - MakeIn, - DecodingServices = never, - EncodingServices = DecodingServices, - Brand = {} -> extends - S.Bottom< - Self, - Encoded, - DecodingServices, - EncodingServices, - SchemaAST.AST, - S.Codec, - MakeIn, - Self, - readonly [], - MakeIn - > -{ - new(_: never): Brand - readonly copy: ReturnType Self>> - // NOTE: no `identifier` here — `S.Opaque` (and requests) build on `S.Bottom`, not `S.Class`, - // so they have no `identifier`. It lives only on the class facades below. - // NOTE: `fields` / `mapFields` are intentionally NOT redeclared here. They are - // carried (precise) from the underlying schema via `OpaqueFacadeStatics`. A wide - // `mapFields(f: (fields: S.Struct.Fields) => To)` override would win overload - // resolution and erase field precision in `Q.project(X.mapFields(...))`. -} - -export interface OpaqueClassFacade< - Self, - Encoded, - MakeIn, - DecodingServices = never, - EncodingServices = DecodingServices, - Brand = {} -> extends - S.Bottom< - Self, - Encoded, - DecodingServices, - EncodingServices, - SchemaAST.AST, - S.Codec, - MakeIn, - Self, - readonly [], - MakeIn - > -{ - new(...args: OpaqueFacadeConstructorArgs): Brand - readonly copy: ReturnType Self>> - readonly identifier: string -} - -export function OpaqueFacade< - Self, - Encoded, - MakeIn, - DecodingServices = never, - EncodingServices = DecodingServices, - Brand = {} ->() { - function facade>( - schema: SchemaS - ): - & OpaqueClassFacade - & OpaqueFacadeStatics - function facade>( - schema: SchemaS - ): - & OpaqueFacade - & OpaqueFacadeStatics - function facade(schema: OpaqueFacadeInput) { - const Base = getFacadeClassSchema(schema) - if (Base !== undefined) { - const astCache = new WeakMap() - const originalAstDescriptor = getOwnOrInheritedPropertyDescriptor(Base, "ast") - - return class extends (Base as any) { - static get ast(): SchemaAST.AST { - let cached = astCache.get(this) - if (cached !== undefined) return cached - - const originalAst = originalAstDescriptor?.get?.call(this) as SchemaAST.AST - cached = makeFacadeClassAst(schema, originalAst, this) - astCache.set(this, cached) - return cached - } - } - } - - class Facade {} - return Object.setPrototypeOf(Facade, schema) - } - - return facade -} - -/** - * Like class-schema {@link OpaqueFacade}, but for error models (`TaggedErrorClass` / - * `ErrorClass`). The decoded instance type carries `Cause.YieldableError`, so - * `yield* new MyError(...)`, `Effect.fail(myError)`, and `instanceof` all keep - * working through the facade — the runtime `_X` is the real error class (the - * facade `X extends ...(_X)` inherits its prototype), and the type reflects it. - * Nothing is lost vs the underlying error class. - */ -export interface OpaqueErrorFacadeClass< - Self, - Encoded, - MakeIn, - DecodingServices = never, - EncodingServices = DecodingServices, - Brand = {} -> extends - S.Bottom< - Self, - Encoded, - DecodingServices, - EncodingServices, - SchemaAST.AST, - S.Codec, - MakeIn, - Self, - readonly [], - MakeIn - > -{ - // YieldableError (not Self) on the constructed instance — like class-schema OpaqueFacade's - // `new(): Brand`, the data type comes from the declaration-merged `interface X`, - // so `Self` must NOT appear here (would recurse). Merging `X & YieldableError` - // makes `yield* new X()` / `Effect.fail` / `instanceof` work without losing data. - new(...args: OpaqueFacadeConstructorArgs): Cause.YieldableError & Brand - readonly copy: ReturnType Self>> - readonly identifier: string -} - -export function OpaqueErrorFacadeClass< - Self, - Encoded, - MakeIn, - DecodingServices = never, - EncodingServices = DecodingServices, - Brand = {} ->() { - return >( - schema: SchemaS - ): - & OpaqueErrorFacadeClass - & OpaqueFacadeStatics => - { - type FacadeSchema = - & OpaqueErrorFacadeClass - & OpaqueFacadeStatics - - if (isClassSchemaConstructor(schema)) { - return schema as SchemaS & FacadeSchema - } - - throw new TypeError("OpaqueErrorFacadeClass requires a class schema") - } -} - -/** - * Workflow-compatible struct facade. - * - * A top-level `const X = S.Struct(...)` / `S.TaggedStruct(...)` schema model whose `.d.ts` - * is produced by the schema-aware declaration emitter is retyped from the giant inline - * `S.Struct<{ ...fields... }>` to this facade. - * - * Unlike {@link OpaqueFacade}, `StructFacade` **extends effect-app's `Struct`**, so the - * value is still a real (effect-app) struct schema: it satisfies `Workflow.AnyStructSchema` and - * the `Struct` reconstruction the workflow machinery performs, and `Union` / - * `.fields.x` keep working. It MUST be effect-app's `Struct` (not effect core's) — scanner - * models are effect-app `S.Struct`, so a facade built on `effect/Schema`'s `Struct` is not - * assignable where an effect-app struct is expected. At the same time the type-level members - * (`Type` / `Encoded` / make-input / services) are pinned to the named interfaces the emitter - * generates in `namespace X`, so consumers reference those by name instead of re-deriving them - * from `Fields` on every use. `Fields` is the model's own generated `X.Fields` interface — a - * single materialization of the (expensive) field shape that every reference site points at. - */ -export type StructFacade< - Self, - Encoded, - MakeIn, - DecodingServices, - EncodingServices, - Fields extends S.Struct.Fields -> = - & Omit, "Type" | "Encoded" | "~type.make.in" | "DecodingServices" | "EncodingServices"> - & { - readonly "Type": Self - readonly "Encoded": Encoded - readonly "~type.make.in": MakeIn - readonly "DecodingServices": DecodingServices - readonly "EncodingServices": EncodingServices - } diff --git a/packages/effect-app/src/Schema/FastCheck.ts b/packages/effect-app/src/Schema/FastCheck.ts deleted file mode 100644 index 0c01c89aa..000000000 --- a/packages/effect-app/src/Schema/FastCheck.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type * as fc from "fast-check" - -export type { Arbitrary } from "fast-check" -type FC = typeof fc - -export type { FC } - -export const ALPHABET = "abcdefghijklmnopqrstuvwxyz" -export const ALPHABET_UC = ALPHABET.toUpperCase() - -// uppercase A-Z -const UC_char = (fc: Pick) => fc.integer({ min: 0x41, max: 0x5a }).map(String.fromCharCode) -export const UC_alphabet = (min: number, max: number) => (fc: FC) => - fc.array(UC_char(fc), { minLength: min, maxLength: max }).map((arr) => arr.join("")) -// lowercase A-Z -const LC_char = (fc: Pick) => fc.integer({ min: 0x61, max: 0x7a }).map(String.fromCharCode) -export const LC_alphabet = (min: number, max: number) => (fc: Pick) => - fc.array(LC_char(fc), { minLength: min, maxLength: max }).map((arr) => arr.join("")) - -// numbers 0-9 -const N_char = (fc: Pick) => fc.integer({ min: 0x30, max: 0x39 }).map(String.fromCharCode) -export const Numbers = (min: number, max: number) => (fc: Pick) => - fc.array(N_char(fc), { minLength: min, maxLength: max }).map((arr) => arr.join("")) diff --git a/packages/effect-app/src/Schema/Methods.ts b/packages/effect-app/src/Schema/Methods.ts deleted file mode 100644 index 2da6a7a93..000000000 --- a/packages/effect-app/src/Schema/Methods.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type ReadMethods = "GET" -export type WriteMethods = "POST" | "PUT" | "PATCH" | "DELETE" - -export type Rest = ReadMethods | WriteMethods - -export type System = "OPTIONS" | "HEAD" | "TRACE" - -export type All = Rest | System diff --git a/packages/effect-app/src/Schema/SchemaParser.ts b/packages/effect-app/src/Schema/SchemaParser.ts deleted file mode 100644 index 5b0fe3265..000000000 --- a/packages/effect-app/src/Schema/SchemaParser.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as SchemaParser from "effect/SchemaParser" -import { withDefaultParseOptions } from "./ext.ts" - -export * from "effect/SchemaParser" - -export const decodeEffectConcurrently: typeof SchemaParser.decodeEffect = withDefaultParseOptions( - SchemaParser.decodeEffect -) - -export const decodeUnknownEffectConcurrently: typeof SchemaParser.decodeUnknownEffect = withDefaultParseOptions( - SchemaParser.decodeUnknownEffect -) diff --git a/packages/effect-app/src/Schema/SpecialJsonSchema.ts b/packages/effect-app/src/Schema/SpecialJsonSchema.ts deleted file mode 100644 index e99d1cd88..000000000 --- a/packages/effect-app/src/Schema/SpecialJsonSchema.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * SpecialJsonSchema — A variant of Schema.toJsonSchemaDocument that - * post-processes the output (e.g. flattens simple allOf). - */ -import type * as JsonSchema from "effect/JsonSchema" -import * as Schema from "effect/Schema" - -/** - * Converts a schema to a JSON Schema Document (draft-2020-12), with - * post-processing that flattens simple allOf entries. - */ -export function specialJsonSchemaDocument( - schema: Schema.Top, - options?: Schema.ToJsonSchemaOptions -): JsonSchema.Document<"draft-2020-12"> { - const jd = Schema.toJsonSchemaDocument(schema, options) - const processedDefs: JsonSchema.Definitions = {} - for (const [key, def] of Object.entries(jd.definitions)) { - processedDefs[key] = postProcessJsonSchema(def) - } - return { - dialect: "draft-2020-12", - schema: postProcessJsonSchema(jd.schema), - definitions: processedDefs - } -} - -/** - * Flattens `allOf` entries into the parent when the parent already has a - * `type` and every `allOf` entry is a plain constraint object (no `$ref`, - * no `type`). Merged properties from `allOf` entries win on conflict. - */ -export function flattenSimpleAllOf(obj: unknown): unknown { - if (obj === null || typeof obj !== "object") return obj - - if (globalThis.Array.isArray(obj)) { - return obj.map(flattenSimpleAllOf) - } - - const record = obj as Record - const result: Record = {} - for (const [key, value] of Object.entries(record)) { - result[key] = flattenSimpleAllOf(value) - } - - if (result["type"] && globalThis.Array.isArray(result["allOf"])) { - const allOf = result["allOf"] as Array> - const canFlatten = allOf.every((entry) => - typeof entry === "object" && entry !== null && !("$ref" in entry) && !("type" in entry) - ) - if (canFlatten) { - const { allOf: _, ...rest } = result - const merged: Record = { ...rest } - for (const entry of allOf) { - Object.assign(merged, entry) - } - return merged - } - } - - return result -} - -/** - * Recursively removes `additionalProperties: false` from JSON Schema objects. - * Only removes when the value is exactly `false` -- other values are left intact. - */ -export function removeAdditionalPropertiesFalse(obj: unknown): unknown { - if (obj === null || typeof obj !== "object") return obj - - if (globalThis.Array.isArray(obj)) { - return obj.map(removeAdditionalPropertiesFalse) - } - - const record = obj as Record - const result: Record = {} - for (const [key, value] of Object.entries(record)) { - if (key === "additionalProperties" && value === false) continue - result[key] = removeAdditionalPropertiesFalse(value) - } - - return result -} - -/** - * Flattens nested `anyOf` entries: if an anyOf entry is itself just `{ anyOf: [...] }` - * with no other keys, its children are inlined. If only one item remains, the anyOf - * wrapper is removed entirely. - */ -export function flattenNestedAnyOf(obj: unknown): unknown { - if (obj === null || typeof obj !== "object") return obj - if (globalThis.Array.isArray(obj)) return obj.map(flattenNestedAnyOf) - - const record = obj as Record - const result: Record = {} - for (const [key, value] of Object.entries(record)) { - result[key] = flattenNestedAnyOf(value) - } - - if (globalThis.Array.isArray(result["anyOf"])) { - const anyOf = result["anyOf"] as Array - const flattened: Array = [] - for (const entry of anyOf) { - if ( - typeof entry === "object" - && entry !== null - && !globalThis.Array.isArray(entry) - && "anyOf" in entry - && Object.keys(entry).length === 1 - && globalThis.Array.isArray((entry as Record)["anyOf"]) - ) { - flattened.push(...(entry as Record)["anyOf"] as Array) - } else { - flattened.push(entry) - } - } - if (flattened.length === 1) { - const { anyOf: _, ...rest } = result - const single = flattened[0] - if (typeof single === "object" && single !== null && !globalThis.Array.isArray(single)) { - return { ...rest, ...single } - } - return single - } - result["anyOf"] = flattened - } - - return result -} - -/** - * Applies JSON Schema post-processing: flattens simple allOf, - * flattens nested anyOf, then strips additionalProperties: false. - */ -export function postProcessJsonSchema(obj: JsonSchema.JsonSchema): JsonSchema.JsonSchema { - return removeAdditionalPropertiesFalse(flattenNestedAnyOf(flattenSimpleAllOf(obj))) as JsonSchema.JsonSchema -} diff --git a/packages/effect-app/src/Schema/SpecialOpenApi.ts b/packages/effect-app/src/Schema/SpecialOpenApi.ts deleted file mode 100644 index 44c88e372..000000000 --- a/packages/effect-app/src/Schema/SpecialOpenApi.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * SpecialOpenApi — Deduplicates `components/schemas` entries in an OpenAPI spec - * and applies JSON Schema post-processing (null removal, allOf flattening). - * - * When `OpenApi.fromApi` generates the spec, different AST nodes sharing the - * same identifier can produce duplicate entries (e.g. "X" and "X1") in - * `components.schemas`. This module provides a transform function that - * collapses those duplicates, rewrites all `$ref` pointers accordingly, - * and post-processes schemas for better codegen compatibility. - * - * Usage with the OpenApi `Transform` annotation: - * - * ```ts - * import { OpenApi } from "effect/http-api" - * import { deduplicateOpenApiSchemas } from "./SpecialOpenApi.ts" - * - * const api = HttpApi.make("myApi") - * .pipe(HttpApi.annotateContext(OpenApi.annotations({ transform: deduplicateOpenApiSchemas }))) - * ``` - */ - -import { postProcessJsonSchema } from "./SpecialJsonSchema.ts" - -/** - * Deduplicates `components.schemas` entries in an OpenAPI spec. - * - * Entries sharing the same base identifier (e.g. "X" and "X1") whose JSON - * representations are identical are collapsed into a single canonical entry, - * and all `$ref` pointers throughout the spec are rewritten to point to - * the canonical key. - * - * Designed to be used as the `transform` option in `OpenApi.annotations`. - */ -export function deduplicateOpenApiSchemas( - spec: Record -): Record { - const components = spec["components"] as Record | undefined - if (!components) return spec - const schemas = components["schemas"] as Record | undefined - if (!schemas) return spec - - const keys = Object.keys(schemas) - if (keys.length === 0) return spec - - // Group keys by base identifier (strip trailing digits) - const groups = new Map>() - for (const key of keys) { - const base = getBaseIdentifier(key) - const fingerprint = JSON.stringify(schemas[key]) - const group = groups.get(base) - if (group === undefined) { - groups.set(base, [{ key, fingerprint }]) - } else { - group.push({ key, fingerprint }) - } - } - - // Build remapping from duplicate keys to canonical keys - const remapping = new Map() - for (const [, group] of groups) { - if (group.length <= 1) continue - const seen = new Map() // fingerprint -> canonical key - for (const entry of group) { - const canonical = seen.get(entry.fingerprint) - if (canonical !== undefined) { - remapping.set(entry.key, canonical) - } else { - seen.set(entry.fingerprint, entry.key) - } - } - } - - if (remapping.size === 0) return postProcessJsonSchema(spec) - - // Build new schemas object without duplicates - const newSchemas: Record = {} - for (const key of keys) { - if (!remapping.has(key)) { - newSchemas[key] = schemas[key] - } - } - - // Deep clone the spec, replace schemas, and rewrite all $ref pointers - const newSpec = structuredClone(spec) - newSpec["components"]["schemas"] = newSchemas - rewriteRefs(newSpec, remapping) - - return postProcessJsonSchema(newSpec) -} - -/** - * Extracts the base identifier from a schema key by stripping trailing - * digits appended by the gen() function. - * E.g. "X1" -> "X", "X" -> "X", "MyType2" -> "MyType" - */ -function getBaseIdentifier(key: string): string { - const match = key.match(/^(.+?)(\d+)$/) - return match ? match[1]! : key -} - -/** - * Recursively rewrites `$ref` values in a JSON object tree. - * Mutates the object in-place (caller should pass a deep clone). - */ -function rewriteRefs(obj: any, remapping: Map): void { - if (obj === null || typeof obj !== "object") return - - if (Array.isArray(obj)) { - for (const item of obj) { - rewriteRefs(item, remapping) - } - return - } - - if (typeof obj.$ref === "string") { - // OpenAPI refs look like "#/components/schemas/X1" - const prefix = "#/components/schemas/" - if (obj.$ref.startsWith(prefix)) { - const refKey = obj.$ref.slice(prefix.length) - const canonical = remapping.get(refKey) - if (canonical !== undefined) { - obj.$ref = prefix + canonical - } - } - } - - for (const value of Object.values(obj)) { - rewriteRefs(value, remapping) - } -} diff --git a/packages/effect-app/src/Schema/brand.ts b/packages/effect-app/src/Schema/brand.ts deleted file mode 100644 index bd4275112..000000000 --- a/packages/effect-app/src/Schema/brand.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -import * as B from "effect/Brand" -import type * as Option from "effect/Option" -import type * as Result from "effect/Result" -import * as S from "effect/Schema" - -export interface Constructor> { - /** - * Constructs a branded type from a value of type `A`, throwing an error if - * the provided `A` is not valid. - */ - (args: Unbranded): A - /** - * Constructs a branded type from a value of type `A`, returning `Some` - * if the provided `A` is valid, `None` otherwise. - */ - option(args: Unbranded): Option.Option - /** - * Constructs a branded type from a value of type `A`, returning `Result.succeed` - * if the provided `A` is valid, `Result.fail` otherwise. - */ - result(args: Unbranded): Result.Result - /** - * Attempts to refine the provided value of type `A`, returning `true` if - * the provided `A` is valid, `false` otherwise. - */ - is(a: Unbranded): a is Unbranded & A -} - -type BrandAnnotations = S.Annotations.Filter - -export interface BrandedSchema> extends - S.Bottom< - C, - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], - S["ast"], - BrandedSchema, - S["~type.make.in"], - S["Iso"], - S["~type.parameters"], - C, - S["~type.mutability"], - S["~type.optionality"], - S["~type.constructor.default"], - S["~encoded.mutability"], - S["~encoded.optionality"] - > -{} - -export const fromBrand = >( - _constructor: Constructor, - options?: BrandAnnotations -) => -(self: Self): BrandedSchema => { - const branded = self.pipe(S.brand("Brand")) - return (options ? branded.pipe(S.annotate(options)) : branded) as BrandedSchema -} - -export type Brands

= P extends B.Brand ? B.Brand.Brands

- : never - -export type Unbranded

= P extends B.Brand ? B.Brand.Unbranded

> = { -// [K in keyof P[B.BrandTypeId]]: K extends string | symbol ? { -// readonly [k in K]: k -// } -// : never -// }[keyof P[B.BrandTypeId]] -// export type UnionToIntersection3 = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I -// : never - -/** Union of core `Schema.Date` (Date objects) and string-encoded `DateFromString`, with default helpers. */ -export const inputDate = extendM( - S.Union([S.Date, DateFromString]), - (s) => ({ - /** - * Construction-only default `new Date()`. Applied only when the field is - * omitted from `.make(...)` input. NOT applied during decode — cannot be - * used to JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new globalThis.Date()))), - /** - * Decode-time default `new Date()`. **Discouraged for persisted data:** a - * missing field may be data corruption, not an old-shape document; - * silently substituting `new Date()` hides the problem. Prefer an - * explicit, preferably versioned migration over a decode-time fallback. - * See file-level note. - */ - withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new globalThis.Date()))) - }) -) - -export interface UnionBrand {} - -// TODO: v4 migration — makeOpt used internal PropertySignature types that are removed in v4 -// Simplified to use v4's S.optional / S.optionalKey directly -export function makeOptional( - t: NER -): { - [K in keyof NER]: NER[K] extends S.Top ? ReturnType> : any -} { - return Struct.keys(t).reduce((prev, cur) => { - prev[cur] = S.optional(t[cur] as any) - return prev - }, {} as any) -} - -// `Omit` erases the polymorphic `this` of "this-returning" methods (`annotate`, `annotateKey`, -// `check`, `rebuild`): extracting them through a mapped type binds their `this["Rebuild"]` back to -// the original `T`, not to this intersection. So overriding just `Rebuild` above is not enough, those -// four methods have to be redeclared here with a concrete return type instead of `this["Rebuild"]`. -export type DropConstructorDefault = - & Omit< - T, - "~type.constructor.default" | "Rebuild" | "annotate" | "annotateKey" | "check" | "rebuild" - > - & { - readonly "~type.constructor.default": "no-default" - readonly "Rebuild": DropConstructorDefault - annotate(annotations: S.Annotations.Bottom): DropConstructorDefault - annotateKey(annotations: S.Annotations.Key): DropConstructorDefault - check( - ...checks: readonly [SchemaAST.Check, ...Array>] - ): DropConstructorDefault - rebuild(ast: T["ast"]): DropConstructorDefault - } - -// `replaceContext` is `@internal` in `effect/SchemaAST` (stripped from its public types, though the -// export itself still ships in the compiled JS). Effect's own `withConstructorDefault` goes through the -// same function internally, so this mirrors an existing code path rather than reaching for something -// novel, but it's still an internal API that could move or disappear without a signal. -const SchemaASTInternal = SchemaAST as unknown as { - readonly replaceContext: (ast: A, context: SchemaAST.Context | undefined) => A -} - -export interface DropConstructorDefaultLambda extends Struct.Lambda { - (self: T): DropConstructorDefault - readonly "~lambda.out": DropConstructorDefault -} - -/** - * A `Struct.Lambda` that removes a field's `withConstructorDefault`, if any. - * Compose with `Struct.pick`/`Struct.omit` via `Struct.map` to derive a - * partial-update schema from a "create" schema without inheriting defaults - * that would resurrect omitted fields in `.make(...)` output: - * - * ```ts - * import { flow } from "effect/Function" - * import * as Struct from "effect/Struct" - * import * as S from "effect-app/Schema" - * - * const Person = S.Struct({ - * id: S.String, - * name: S.String.pipe(S.optionalKey, S.withConstructorDefault(() => "")) - * }) - * - * const UpdatePerson = Person.mapFields( - * flow(Struct.omit(["id"]), Struct.map(S.dropConstructorDefault)) - * ) - * ``` - */ -export const dropConstructorDefault: DropConstructorDefaultLambda = Struct.lambda( - (schema) => { - const context = schema.ast.context - if (!context?.constructorDefault) return schema - return schema.rebuild( - SchemaASTInternal.replaceContext( - schema.ast, - new SchemaAST.Context(context.isOptional, context.isMutable, undefined, context.annotations) - ) - ) - } -) - -export function makeExactOptional( - t: NER -): { - [K in keyof NER]: NER[K] extends S.Top ? DropConstructorDefault>> - : any -} { - return Struct.keys(t).reduce((prev, cur) => { - prev[cur] = dropConstructorDefault(S.optionalKey(t[cur] as any)) - return prev - }, {} as any) -} - -/** A version of transform which is only a one way mapping of From->To */ -export const transformTo = ( - from: From, - to: To, - decode: ( - fromA: From["Type"], - options: SchemaAST.ParseOptions - ) => To["Encoded"] -) => - from.pipe( - S.decodeTo( - to, - SchemaTransformation.transformEffect({ - decode: (input: any, options: any) => Effect.sync(() => decode(input, options)), - encode: (i: any) => - Effect.fail( - new SchemaIssue.Forbidden( - { message: "One way schema transformation, encoding is not allowed" }, - i - ) - ) - }) - ) - ) - -/** A version of transformEffect which is only a one way mapping of From->To */ -export const transformToOrFail = ( - from: From, - to: To, - decode: ( - fromA: From["Type"], - options: SchemaAST.ParseOptions - ) => Effect.Effect -) => - from.pipe( - S.decodeTo( - to, - SchemaTransformation.transformEffect({ - decode, - encode: (i: any) => - Effect.fail( - new SchemaIssue.Forbidden( - { message: "One way schema transformation, encoding is not allowed" }, - i - ) - ) - }) - ) - ) - -export const provide: { - (context: Context.Context): (self: Self) => ProvidedCodec - (self: Self, context: Context.Context): ProvidedCodec -} = Function.dual(2, (self: Self, context: Context.Context): ProvidedCodec => { - const prov = Effect.provide(context) - return self.pipe( - S.middlewareDecoding((effect) => prov(effect)), - S.middlewareEncoding((effect) => prov(effect)) - ) -}) -export const contextFromServices = Effect.fnUntraced(function*< - Self extends S.Top, - Tags extends ReadonlyArray> ->(self: Self, ...services: Tags) { - const context: Context.Context> = Context.pick(...services)( - yield* Effect.context>() - ) - return provide(self, context) -}) as < - Self extends S.Top, - Tags extends ReadonlyArray> ->( - self: Self, - ...services: Tags -) => Effect.Effect< - ProvidedCodec>, - never, - Context.Service.Identifier -> diff --git a/packages/effect-app/src/Schema/moreStrings.ts b/packages/effect-app/src/Schema/moreStrings.ts deleted file mode 100644 index ece539d1e..000000000 --- a/packages/effect-app/src/Schema/moreStrings.ts +++ /dev/null @@ -1,390 +0,0 @@ -/** - * Branded string ID schemas with `.withConstructorDefault` extensions. - * - * Each `.withConstructorDefault` here is **only** applied when the field is - * omitted during construction (`.make(...)`). It is **not** applied during - * decode and therefore cannot be used to JIT-migrate database fields. - * - * For persisted data, prefer an explicit, preferably versioned migration - * over decode-time fallbacks. See `./ext.ts` for the full policy note. - */ -import type { Refinement } from "effect-app/Function" -import { extendM } from "effect-app/utils" -import type * as B from "effect/Brand" -import * as Effect from "effect/Effect" -import { pipe } from "effect/Function" -import * as S from "effect/Schema" -import * as Getter from "effect/SchemaGetter" -import * as SchemaIssue from "effect/SchemaIssue" -import type { Simplify } from "effect/Types" -import { customRandom, nanoid, urlAlphabet } from "nanoid" -import validator from "validator" -import type * as SchemaAST from "../SchemaAST.ts" -import { type BrandedSchema, fromBrand, nominal } from "./brand.ts" -import { withDefaultMake } from "./ext.ts" -import { type B as SchemaB } from "./schema.ts" -import type { NonEmptyString255Brand, NonEmptyStringBrand } from "./strings.ts" - -const nonEmptyString = S.NonEmptyString - -/** - * A string that is at least 1 character long and a maximum of 50. - */ -export interface NonEmptyString50Brand extends Simplify & NonEmptyString64Brand> {} - -/** - * A string that is at least 1 character long and a maximum of 50. - */ -export type NonEmptyString50 = string & NonEmptyString50Brand - -/** - * A string that is at least 1 character long and a maximum of 50. - */ -export interface NonEmptyString50Schema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString50 -} -export const NonEmptyString50: NonEmptyString50Schema = nonEmptyString.pipe( - S.check(S.isMaxLength(50)), - fromBrand(nominal(), { - identifier: "NonEmptyString50", - jsonSchema: {} - }), - withDefaultMake -) - -/** - * A string that is at least 1 character long and a maximum of 64. - */ -export interface NonEmptyString64Brand extends Simplify & NonEmptyString80Brand> {} - -/** - * A string that is at least 1 character long and a maximum of 64. - */ -export type NonEmptyString64 = string & NonEmptyString64Brand - -/** - * A string that is at least 1 character long and a maximum of 64. - */ -export interface NonEmptyString64Schema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString64 -} -export const NonEmptyString64: NonEmptyString64Schema = nonEmptyString.pipe( - S.check(S.isMaxLength(64)), - fromBrand(nominal(), { - identifier: "NonEmptyString64", - jsonSchema: {} - }), - withDefaultMake -) - -/** - * A string that is at least 1 character long and a maximum of 80. - */ -export interface NonEmptyString80Brand extends Simplify & NonEmptyString100Brand> {} - -/** - * A string that is at least 1 character long and a maximum of 80. - */ -export type NonEmptyString80 = string & NonEmptyString80Brand - -/** - * A string that is at least 1 character long and a maximum of 80. - */ - -export interface NonEmptyString80Schema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString80 -} -export const NonEmptyString80: NonEmptyString80Schema = nonEmptyString.pipe( - S.check(S.isMaxLength(80)), - fromBrand(nominal(), { - identifier: "NonEmptyString80", - jsonSchema: {} - }), - withDefaultMake -) - -/** - * A string that is at least 1 character long and a maximum of 100. - */ -export interface NonEmptyString100Brand extends Simplify & NonEmptyString255Brand> {} - -/** - * A string that is at least 1 character long and a maximum of 100. - */ -export type NonEmptyString100 = string & NonEmptyString100Brand - -/** - * A string that is at least 1 character long and a maximum of 100. - */ -export interface NonEmptyString100Schema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString100 -} -export const NonEmptyString100: NonEmptyString100Schema = nonEmptyString.pipe( - S.check(S.isMaxLength(100)), - fromBrand(nominal(), { - identifier: "NonEmptyString100", - jsonSchema: {} - }), - withDefaultMake -) - -/** - * A string that is at least 3 character long and a maximum of 255. - */ -export interface Min3String255Brand extends Simplify & NonEmptyString255Brand> {} - -/** - * A string that is at least 3 character long and a maximum of 255. - */ -export type Min3String255 = string & Min3String255Brand - -/** - * A string that is at least 3 character long and a maximum of 255. - */ -export interface Min3String255Schema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): Min3String255 -} -export const Min3String255: Min3String255Schema = pipe( - S.String, - S.check(S.isMinLength(3), S.isMaxLength(255)), - fromBrand(nominal(), { - identifier: "Min3String255", - jsonSchema: {} - }), - withDefaultMake -) - -/** - * A string that is at least 6 characters long and a maximum of 50. - */ -export interface StringIdBrand extends Simplify & NonEmptyString50Brand> {} - -/** - * A string that is at least 6 characters long and a maximum of 50. - */ -export type StringId = string & StringIdBrand - -const minLength = 6 -const maxLength = 50 -const nanoidSize = 21 -const nanoidByteLength = 10 * nanoidSize - -const StringIdString = pipe( - S.String, - S.check(S.isMinLength(minLength), S.isMaxLength(maxLength)), - fromBrand(nominal(), { - identifier: "StringId", - jsonSchema: {} - }) -) - -const nanoidFromBytes = (bytes: Uint8Array) => - customRandom(urlAlphabet, nanoidSize, (size) => bytes.subarray(0, size))() as StringId - -// JSON stays a branded string (toCodec). Generation uses toCodecArbitrary — the native -// replacement for fast-check `toArbitrary` — same as the old StringIdArb: -// uint8Array(210).map(bytes => customRandom(urlAlphabet, 21, …)). -const StringIdSchemaBase = S.declareConstructor()( - [], - () => (input, ast, options) => - S.is(StringIdString)(input) - ? Effect.succeed(input) - : Effect.fail(new SchemaIssue.InvalidType(ast, input, options)), - { - identifier: "StringId", - expected: "StringId", - toCodec: () => - S.link()(StringIdString, { - decode: Getter.passthrough(), - encode: Getter.passthrough() - }), - toCodecArbitrary: () => - S.link()( - S.Uint8Array.check(S.isMinLength(nanoidByteLength), S.isMaxLength(nanoidByteLength)), - { - decode: Getter.transform(nanoidFromBytes), - encode: Getter.forbiddenEncoding - } - ) - } -) -const makeStringId = (s?: string): StringId => - s !== undefined ? S.decodeSync(StringIdString)(s) : nanoid() as unknown as StringId -/** - * A string that is at least 6 characters long and a maximum of 50. - * - * `.withConstructorDefault` => fresh `nanoid()` (construction-only; not - * applied during decode — see file-level note). - */ -export interface StringIdSchema extends S.declareConstructor { - (i: string, options?: SchemaAST.ParseOptions): StringId - /** Generate fresh `nanoid()`-shaped `StringId`. */ - make(): StringId - /** Construct a `StringId` from a known string (validated via decodeSync). */ - make(input: string, options?: S.MakeOptions): StringId - readonly withConstructorDefault: S.withConstructorDefault< - S.declareConstructor - > -} -export const StringId: StringIdSchema = extendM( - StringIdSchemaBase, - (s) => ({ - make: makeStringId, - /** - * Construction-only default: fresh `nanoid()`-shaped `StringId`. Applied - * only when the field is omitted from `.make(...)` input. NOT applied - * during decode — cannot be used to JIT-migrate database fields. See - * file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(makeStringId))) - }) -) - .pipe(withDefaultMake) - -/** - * Build a `StringId` schema whose values are required to start with a fixed - * `prefix` (joined with `separator`, default `-`). - * - * The returned schema exposes `.withConstructorDefault` that mints a fresh - * prefixed id. Construction-only — not applied during decode; see file-level - * note. - */ -export function prefixedStringId() { - return ( - prefix: Prefix, - name: string, - separator?: Separator - ) => { - type FullPrefix = `${Prefix}${Separator}` - const pref = `${prefix}${separator ?? "-"}` as FullPrefix - const PrefixedString = StringIdString.pipe( - S.refine((x): x is Type => x.startsWith(pref), { identifier: name }) - ) - // Pre-rc.114: StringIdArb().map(x => pref + x.substring(0, 50 - pref.length)) - const s = S.declareConstructor()( - [], - () => (input, ast, options) => - S.is(PrefixedString)(input) - ? Effect.succeed(input) - : Effect.fail(new SchemaIssue.InvalidType(ast, input, options)), - { - identifier: name, - expected: name, - toCodec: () => - S.link()(PrefixedString, { - decode: Getter.passthrough(), - encode: Getter.passthrough() - }), - toCodecArbitrary: () => - S.link()( - S.Uint8Array.check(S.isMinLength(nanoidByteLength), S.isMaxLength(nanoidByteLength)), - { - decode: Getter.transform((bytes) => - (pref + nanoidFromBytes(bytes).substring(0, maxLength - pref.length)) as Type - ), - encode: Getter.forbiddenEncoding - } - ) - } - ) - const schema = s.pipe(withDefaultMake) - const make = () => (pref + StringId.make().substring(0, 50 - pref.length)) as Type - - return extendM( - schema, - (ex): PrefixedStringUtils => ({ - make, - /** - * Automatically adds the prefix. - */ - unsafeFrom: (str: string) => ex(pref + str), - /** - * Must provide a literal string starting with prefix. - */ - prefixSafe: (str: `${Prefix}${Separator}${REST}`) => ex(str), - prefix, - /** - * Construction-only default: fresh prefixed id. Applied only when - * the field is omitted from `.make(...)` input. NOT applied during - * decode — cannot be used to JIT-migrate database fields. See - * file-level note. - */ - withConstructorDefault: schema.pipe( - S.withConstructorDefault & S.WithoutConstructorDefault>( - Effect.sync(make) - ) - ) - }) - ) - } -} - -/** - * Build a branded `StringId` schema for the given branded `Id` type. - * - * Exposes `.withConstructorDefault` that mints a fresh `nanoid()`-shaped id. - * Construction-only — not applied during decode; see file-level note. - */ -export const brandedStringId = >(): BrandedStringIdSchema => - withDefaultMake( - Object.assign(Object.create(StringId), StringId) as BrandedStringIdSchema - ) - -export interface PrefixedStringUtils< - Type extends StringId, - Prefix extends string, - Separator extends string -> { - readonly make: () => Type - readonly unsafeFrom: (str: string) => Type - prefixSafe: (str: `${Prefix}${Separator}${REST}`) => Type - readonly prefix: Prefix - /** - * Construction-only default: fresh prefixed id. Applied only when the - * field is omitted from `.make(...)` input. NOT applied during decode — - * cannot be used to JIT-migrate database fields. See file-level note. - */ - readonly withConstructorDefault: S.withConstructorDefault & S.WithoutConstructorDefault> -} - -export interface BrandedStringIdSchema> extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): Id - /** Generate fresh `nanoid()`-shaped id (inherited from `StringId`). */ - make(): Id - /** Construct an `Id` from a known string (validated via decodeSync). */ - make(input: string, options?: S.MakeOptions): Id - readonly withConstructorDefault: S.withConstructorDefault> -} - -export interface UrlBrand extends Simplify & NonEmptyStringBrand> {} - -export type Url = string & UrlBrand - -const isUrl: Refinement = (s: string): s is Url => { - return validator.default.isURL(s, { require_tld: false }) -} - -export interface UrlSchema extends S.refine { - (i: string, options?: SchemaAST.ParseOptions): Url -} -export const Url: UrlSchema = S - .String - .pipe( - S.annotate({ - title: "Url", - format: "uri" - }), - S.refine(isUrl, { - identifier: "Url", - jsonSchema: { format: "uri" }, - // Native Arbitrary otherwise emits strings that `isURL({ require_tld: false })` - // accepts (`"R"`, `"prototype"`). Restore the old fast-check `fc.webUrl()` shape. - arbitraryConstraint: { - patterns: [{ - source: "^https://[a-z]{3,12}\\.(?:com|net|org|de)(?:/[a-z0-9-]{0,16})?$", - flags: "" - }] - } - }), - withDefaultMake - ) diff --git a/packages/effect-app/src/Schema/numbers.ts b/packages/effect-app/src/Schema/numbers.ts deleted file mode 100644 index 020be91dd..000000000 --- a/packages/effect-app/src/Schema/numbers.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Numeric brand schemas with `.withConstructorDefault` extensions. - * - * Each `.withConstructorDefault` here is **only** applied when the field is - * omitted during construction (`.make(...)`). It is **not** applied during - * decode and therefore cannot be used to JIT-migrate database fields. - * - * For persisted data, prefer an explicit, preferably versioned migration - * over decode-time fallbacks. See `./ext.ts` for the full policy note. - */ -import { extendM } from "effect-app/utils" -import * as Effect from "effect/Effect" -import * as Order from "effect/Order" -import * as S from "effect/Schema" -import type { Simplify } from "effect/Types" -import type * as SchemaAST from "../SchemaAST.ts" -import { type BrandedSchema, fromBrand, nominal } from "./brand.ts" -import { withDefaultMake } from "./ext.ts" -import { type B } from "./schema.ts" - -export interface PositiveIntBrand - extends Simplify & NonNegativeIntBrand & PositiveNumberBrand> -{} -export type PositiveInt = number & PositiveIntBrand -/** Positive integer. `.withConstructorDefault` => `1` (construction-only). */ -export interface PositiveIntSchema extends BrandedSchema { - (i: number, options?: SchemaAST.ParseOptions): PositiveInt - readonly withConstructorDefault: S.withConstructorDefault> -} -export const PositiveInt: PositiveIntSchema = extendM( - S.Int.pipe( - S.check(S.isGreaterThan(0)), - fromBrand(nominal(), { identifier: "PositiveInt", jsonSchema: {} }), - withDefaultMake - ), - (s) => ({ - /** - * Construction-only default `1`. Applied only when the field is omitted - * from `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1)))) - }) -) - -export interface NonNegativeIntBrand extends Simplify & IntBrand & NonNegativeNumberBrand> {} -export type NonNegativeInt = number & NonNegativeIntBrand -/** Non-negative integer. `.withConstructorDefault` => `0` (construction-only). */ -export interface NonNegativeIntSchema extends BrandedSchema { - (i: number, options?: SchemaAST.ParseOptions): NonNegativeInt - readonly withConstructorDefault: S.withConstructorDefault> -} -export const NonNegativeInt: NonNegativeIntSchema = extendM( - S.Int.pipe( - S.check(S.isGreaterThanOrEqualTo(0)), - fromBrand(nominal(), { - identifier: "NonNegativeInt", - jsonSchema: {} - }), - withDefaultMake - ), - (s) => ({ - /** - * Construction-only default `0`. Applied only when the field is omitted - * from `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) - }) -) - -export interface IntBrand extends Simplify> {} -export type Int = number & IntBrand -/** Integer. `.withConstructorDefault` => `0` (construction-only). */ -export interface IntSchema extends BrandedSchema { - (i: number, options?: SchemaAST.ParseOptions): Int - readonly withConstructorDefault: S.withConstructorDefault> -} -export const Int: IntSchema = extendM( - S.Int.pipe(fromBrand(nominal(), { identifier: "Int", jsonSchema: {} }), withDefaultMake), - (s) => ({ - /** - * Construction-only default `0`. Applied only when the field is omitted - * from `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) - }) -) - -export interface PositiveNumberBrand extends Simplify & NonNegativeNumberBrand> {} -export type PositiveNumber = number & PositiveNumberBrand -/** Positive finite number. `.withConstructorDefault` => `1` (construction-only). */ -// Generation-only: native Arbitrary otherwise samples up to Number.MAX_VALUE, so derived totals -// (amount x weight, sums) overflow to Infinity. Validation and JSON Schema are unaffected. -const realisticMaximum = 1_000_000 - -export interface PositiveNumberSchema extends BrandedSchema { - (i: number, options?: SchemaAST.ParseOptions): PositiveNumber - readonly withConstructorDefault: S.withConstructorDefault> -} -export const PositiveNumber: PositiveNumberSchema = extendM( - S.Finite.pipe( - S.check(S.isGreaterThan(0, { - // the full constraint: annotations replace the check default, which carries `order` + `minimum` - arbitraryConstraint: { order: Order.Number, minimum: 0, exclusiveMinimum: true, maximum: realisticMaximum } - })), - fromBrand(nominal(), { - identifier: "PositiveNumber", - jsonSchema: {} - }), - withDefaultMake - ), - (s) => ({ - /** - * Construction-only default `1`. Applied only when the field is omitted - * from `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1)))) - }) -) - -export interface NonNegativeNumberBrand extends Simplify> {} -export type NonNegativeNumber = number & NonNegativeNumberBrand -/** Non-negative finite number. `.withConstructorDefault` => `0` (construction-only). */ -export interface NonNegativeNumberSchema extends BrandedSchema { - (i: number, options?: SchemaAST.ParseOptions): NonNegativeNumber - readonly withConstructorDefault: S.withConstructorDefault> -} -export const NonNegativeNumber: NonNegativeNumberSchema = extendM( - S - .Finite - .pipe( - S.check(S.isGreaterThanOrEqualTo(0, { - arbitraryConstraint: { order: Order.Number, minimum: 0, maximum: realisticMaximum } - })), - fromBrand(nominal(), { - identifier: "NonNegativeNumber", - jsonSchema: {} - }), - withDefaultMake - ), - (s) => ({ - /** - * Construction-only default `0`. Applied only when the field is omitted - * from `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) - }) -) - -/** @deprecated Not an actual decimal */ -export const NonNegativeDecimal = NonNegativeNumber -/** @deprecated Not an actual decimal */ -export type NonNegativeDecimal = NonNegativeNumber - -/** @deprecated Not an actual decimal */ -export const PositiveDecimal = PositiveNumber -/** @deprecated Not an actual decimal */ -export type PositiveDecimal = PositiveNumber diff --git a/packages/effect-app/src/Schema/phoneNumber.ts b/packages/effect-app/src/Schema/phoneNumber.ts deleted file mode 100644 index 93533734f..000000000 --- a/packages/effect-app/src/Schema/phoneNumber.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Refinement } from "effect-app/Function" -import { isValidPhone } from "effect-app/validation" -import * as S from "effect/Schema" -import type { Simplify } from "effect/Types" -import { withDefaultMake } from "./ext.ts" -import type { B } from "./schema.ts" -import type { NonEmptyStringBrand } from "./strings.ts" - -export interface PhoneNumberBrand extends Simplify & NonEmptyStringBrand> {} -export type PhoneNumber = string & PhoneNumberBrand - -export const PhoneNumber = S - .String - .pipe( - S.annotate({ - title: "PhoneNumber", - description: "a phone number with at least 7 digits", - format: "phone" - }), - S.refine(isValidPhone as Refinement, { - identifier: "PhoneNumber", - description: "a phone number with at least 7 digits", - jsonSchema: { format: "phone" }, - // Native Arbitrary cannot invert `isValidPhone`; steer sampling to digit-only numbers (>= 7 digits). - arbitraryConstraint: { - patterns: [{ source: "^\\+?[1-9][0-9]{6,11}$", flags: "" }] - } - }), - withDefaultMake - ) diff --git a/packages/effect-app/src/Schema/schema.ts b/packages/effect-app/src/Schema/schema.ts deleted file mode 100644 index 5b405c8be..000000000 --- a/packages/effect-app/src/Schema/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as B from "effect/Brand" -import * as P from "effect/SchemaParser" -import * as AST from "../SchemaAST.ts" - -export { AST, B, P } diff --git a/packages/effect-app/src/Schema/strings.ts b/packages/effect-app/src/Schema/strings.ts deleted file mode 100644 index 71d857dd9..000000000 --- a/packages/effect-app/src/Schema/strings.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type * as B from "effect/Brand" -import * as S from "effect/Schema" -import type { Simplify } from "effect/Types" -import type * as SchemaAST from "../SchemaAST.ts" -import { type BrandedSchema, fromBrand, nominal } from "./brand.ts" -import { withDefaultMake } from "./ext.ts" - -export type NonEmptyStringBrand = B.Brand<"NonEmptyString"> -export type NonEmptyString = string & NonEmptyStringBrand -export interface NonEmptyStringSchema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString -} -export const NonEmptyString: NonEmptyStringSchema = S - .NonEmptyString - .pipe( - fromBrand(nominal(), { - identifier: "NonEmptyString", - jsonSchema: {} - }), - withDefaultMake - ) - -export interface NonEmptyString64kBrand extends Simplify & NonEmptyStringBrand> {} -export type NonEmptyString64k = string & NonEmptyString64kBrand -export interface NonEmptyString64kSchema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString64k -} -export const NonEmptyString64k: NonEmptyString64kSchema = S - .NonEmptyString - .pipe( - S.check(S.isMaxLength(64 * 1024)), - fromBrand(nominal(), { - identifier: "NonEmptyString64k", - jsonSchema: {} - }), - withDefaultMake - ) - -export interface NonEmptyString2kBrand extends Simplify & NonEmptyString64kBrand> {} -export type NonEmptyString2k = string & NonEmptyString2kBrand -export interface NonEmptyString2kSchema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString2k -} -export const NonEmptyString2k: NonEmptyString2kSchema = S - .NonEmptyString - .pipe( - S.check(S.isMaxLength(2 * 1024)), - fromBrand(nominal(), { - identifier: "NonEmptyString2k", - jsonSchema: {} - }), - withDefaultMake - ) - -export interface NonEmptyString255Brand extends Simplify & NonEmptyString2kBrand> {} -export type NonEmptyString255 = string & NonEmptyString255Brand -export interface NonEmptyString255Schema extends BrandedSchema { - (i: string, options?: SchemaAST.ParseOptions): NonEmptyString255 -} -export const NonEmptyString255: NonEmptyString255Schema = S - .NonEmptyString - .pipe( - S.check(S.isMaxLength(255)), - fromBrand(nominal(), { - identifier: "NonEmptyString255", - jsonSchema: {} - }), - withDefaultMake - ) diff --git a/packages/effect-app/src/SchemaAST.ts b/packages/effect-app/src/SchemaAST.ts deleted file mode 100644 index 968094487..000000000 --- a/packages/effect-app/src/SchemaAST.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "effect/SchemaAST" diff --git a/packages/effect-app/src/Set.ts b/packages/effect-app/src/Set.ts deleted file mode 100644 index 23af735d2..000000000 --- a/packages/effect-app/src/Set.ts +++ /dev/null @@ -1,666 +0,0 @@ -// ets_tracing: off - -import * as Array from "effect/Array" -import type * as Equivalence from "effect/Equivalence" -import * as Option from "effect/Option" -import type * as Order from "effect/Order" -import { not } from "effect/Predicate" -import type * as Result from "effect/Result" -import * as Tuple from "effect/Tuple" -import { identity, pipe, type Predicate, type Refinement } from "./Function.ts" - -export function find_( - as: ReadonlySet, - refinement: Refinement -): B | undefined -export function find_(set: ReadonlySet, predicate: Predicate): A | undefined -export function find_(set: ReadonlySet, predicate: Predicate) { - return [...set].find(predicate) -} - -export function findFirst_( - set: ReadonlySet, - refinement: Refinement -): Option.Option -export function findFirst_(set: ReadonlySet, predicate: Predicate): Option.Option -export function findFirst_(set: ReadonlySet, predicate: Predicate): Option.Option { - return Option.fromNullishOr([...set].find(predicate)) -} - -export function findFirstMap_( - set: ReadonlySet, - f: (a: A) => Option.Option -): Option.Option { - return Array.findFirst([...set], f) -} - -export type MutableSet = globalThis.Set -export type Set = ReadonlySet - -export const empty: Set = new Set() - -/** - * The set of elements which are in both the first and second set - */ -export function intersection_(E: Equivalence.Equivalence): (l: Set, r: Set) => Set { - const elemE = elem_(E) - return (x, y) => { - if (x === empty || y === empty) { - return empty - } - const r = new Set() - x.forEach((e) => { - if (elemE(y, e)) { - r.add(e) - } - }) - return r - } -} - -/** - * The set of elements which are in both the first and second set - */ -export function intersection(E: Equivalence.Equivalence): (r: Set) => (l: Set) => Set { - const i = intersection_(E) - return (x) => (y) => i(x, y) -} - -/** - * Convert a mutable set to a readonly one - */ -export function fromMutable(s: MutableSet): Set { - return new Set(s) -} - -/** - * Convert a set to a mutable one - */ -export function toMutable(s: Set): MutableSet { - return new Set(s) -} - -// /** -// * get Show for set given Show for values -// */ -// export function getShow(S: Show): Show> { -// return { -// show: (s) => { -// let elements = "" -// s.forEach((a) => { -// elements += S.show(a) + ", " -// }) -// if (elements !== "") { -// elements = elements.substring(0, elements.length - 2) -// } -// return `new Set([${elements}])` -// } -// } -// } - -/** - * Convert a set to an Array - */ -export function toArray(O: Order.Order): (set: Set) => ReadonlyArray { - return (x) => { - const r: Array = [] - x.forEach((e) => r.push(e)) - return r.sort(O) - } -} - -/** - * Convert a set to an Array - */ -export function toArray_(x: Set, O: Order.Order): ReadonlyArray { - return toArray(O)(x) -} - -// /** -// * Get Equivalence for Setgiven Equivalence for element -// */ -// export function getEquivalence.Equivalence(E: Equivalence.Equivalence): Equivalence.Equivalence> { -// const subsetE = isSubset_(E) -// return makeEquivalence((x, y) => subsetE(x, y) && subsetE(y, x)) -// } - -interface Next { - readonly done?: boolean - readonly value: A -} - -/** - * true if one or more elements match predicate - */ -export function some(predicate: Predicate): (set: Set) => boolean { - return (set) => { - const values = set.values() - let e: Next - let found = false - while (!found && !(e = values.next() as any).done) { - found = predicate(e.value) - } - return found - } -} - -/** - * true if one or more elements match predicate - */ -export function some_(set: Set, predicate: Predicate): boolean { - return some(predicate)(set) -} - -/** - * Projects a Set through a function - */ -export function map(E: Equivalence.Equivalence): (f: (x: A) => B) => (set: Set) => Set { - const m = map_(E) - return (f) => (set) => m(set, f) -} - -/** - * Projects a Set through a function - */ -export function map_(E: Equivalence.Equivalence): (set: Set, f: (x: A) => B) => Set { - const elemE = elem_(E) - return (set, f) => { - const r = new Set() - set.forEach((e) => { - const v = f(e) - if (!elemE(r, v)) { - r.add(v) - } - }) - return r - } -} - -/** - * true if all elements match predicate - */ -export function every(predicate: Predicate): (set: Set) => boolean { - return (set) => every_(set, predicate) -} - -/** - * true if all elements match predicate - */ -export function every_(set: Set, predicate: Predicate): boolean { - return not(some(not(predicate)))(set) -} - -/** - * Map + Flatten - */ -export function chain( - E: Equivalence.Equivalence -): (f: (x: A) => Set) => (set: Set) => Set { - const c = chain_(E) - return (f) => (set) => c(set, f) -} - -/** - * Map + Flatten - */ -export function chain_( - E: Equivalence.Equivalence -): (set: Set, f: (x: A) => Set) => Set { - const elemE = elem_(E) - return (set, f) => { - const r = new Set() - set.forEach((e) => { - f(e).forEach((e) => { - if (!elemE(r, e)) { - r.add(e) - } - }) - }) - return r - } -} - -/** - * `true` if and only if every element in the first set is an element of the second set - */ -export function isSubset(E: Equivalence.Equivalence): (x: Set, y: Set) => boolean { - const i = isSubset_(E) - return (x, y) => i(y, x) -} - -/** - * `true` if and only if every element in the first set is an element of the second set - */ -export function isSubset_(E: Equivalence.Equivalence): (x: Set, y: Set) => boolean { - const elemE = elem_(E) - return (x, y) => every((a: A) => elemE(y, a))(x) -} - -/** - * Filter set values using predicate - */ -export function filter( - refinement: Refinement -): (set: Set) => Set -export function filter(predicate: Predicate): (set: Set) => Set -export function filter(predicate: Predicate): (set: Set) => Set { - return (set) => filter_(set, predicate) -} - -/** - * Filter set values using predicate - */ -export function filter_( - set: Set, - refinement: Refinement -): Set -export function filter_(set: Set, predicate: Predicate): Set -export function filter_(set: Set, predicate: Predicate): Set { - const values = set.values() - let e: Next - const r = new Set() - while (!(e = values.next() as any).done) { - const value = e.value - if (predicate(value)) { - r.add(value) - } - } - return r -} - -/** - * Partition set values using predicate - */ -export function partition( - refinement: Refinement -): (set: Set) => readonly [Set, Set] -export function partition( - predicate: Predicate -): (set: Set) => readonly [Set, Set] -export function partition( - predicate: Predicate -): (set: Set) => readonly [Set, Set] { - return (set) => partition_(set, predicate) -} - -/** - * Partition set values using predicate - */ -export function partition_( - set: Set, - refinement: Refinement -): readonly [Set, Set] -export function partition_( - set: Set, - predicate: Predicate -): readonly [Set, Set] -export function partition_( - set: Set, - predicate: Predicate -): readonly [Set, Set] { - const values = set.values() - let e: Next - const right = new Set() - const left = new Set() - while (!(e = values.next() as any).done) { - const value = e.value - if (predicate(value)) { - right.add(value) - } else { - left.add(value) - } - } - return Tuple.make(left, right) -} - -/** - * Test if a value is a member of a set - */ -export function elem_(E: Equivalence.Equivalence): (set: Set, a: A) => boolean { - return (set, a) => { - const values = set.values() - let e: Next - let found = false - while (!found && !(e = values.next() as any).done) { - found = E(a, e.value) - } - return found - } -} - -/** - * Test if a value is a member of a set - */ -export function elem(E: Equivalence.Equivalence): (a: A) => (set: Set) => boolean { - const e = elem_(E) - return (a) => (set) => e(set, a) -} - -/** - * Partition elements according to f - */ -export function partitionMap( - EB: Equivalence.Equivalence, - EC: Equivalence.Equivalence -): (f: (a: A) => Result.Result) => (set: Set) => readonly [Set, Set] { - const pm = partitionMap_(EB, EC) - return (f: (a: A) => Result.Result) => (set: Set) => pm(set, f) -} - -/** - * Partition elements according to f - */ -export function partitionMap_( - EB: Equivalence.Equivalence, - EC: Equivalence.Equivalence -): (set: Set, f: (a: A) => Result.Result) => readonly [Set, Set] { - return (set: Set, f: (a: A) => Result.Result) => { - const values = set.values() - let e: Next - const left = new Set() - const right = new Set() - const hasB = elem_(EB) - const hasC = elem_(EC) - while (!(e = values.next() as any).done) { - const v = f(e.value) - switch (v._tag) { - case "Failure": - if (!hasB(left, v.failure)) { - left.add(v.failure) - } - break - case "Success": - if (!hasC(right, v.success)) { - right.add(v.success) - } - break - } - } - return Tuple.make(left, right) - } -} - -/** - * Form the set difference (`x` - `y`) - */ -export function difference_(E: Equivalence.Equivalence): (l: Set, r: Set) => Set { - const elemE = elem_(E) - return (x, y) => filter((a: A) => !elemE(y, a))(x) -} - -/** - * Form the set difference (`x` - `y`) - */ -export function difference(E: Equivalence.Equivalence): (x: Set, y: Set) => Set { - const diff = difference_(E) - return (x, y) => diff(x, y) -} - -/** - * Reduce over the set values - */ -export function reduce( - O: Order.Order -): (b: B, f: (b: B, a: A) => B) => (fa: Set) => B { - const red = reduce_(O) - return (b, f) => (fa) => red(fa, b, f) -} - -/** - * Reduce over the set values - */ -export function reduce_( - O: Order.Order -): (fa: Set, b: B, f: (b: B, a: A) => B) => B { - const toArrayO = toArray(O) - return (fa, b, f) => toArrayO(fa).reduce(f, b) -} - -// /** -// * Fold + Map -// */ -// export function foldMap( -// O: Order.Order, -// M: Identity -// ): (f: (a: A) => M) => (fa: Set) => M { -// const fm = foldMap_(O, M) -// return (f) => (fa) => fm(fa, f) -// } - -// /** -// * Fold + Map -// */ -// export function foldMap_( -// O: Order.Order, -// M: Identity -// ): (fa: Set, f: (a: A) => M) => M { -// const toArrayO = toArray(O) -// return (fa, f) => toArrayO(fa).reduce((b, a) => M.combine(b, f(a)), M.identity) -// } - -/** - * Create a set with one element - */ -export function singleton(a: A): Set { - return new Set([a]) -} - -/** - * Insert a value into a set - */ -export function insert(E: Equivalence.Equivalence): (a: A) => (set: Set) => Set { - const i = insert_(E) - return (a) => (set) => i(set, a) -} - -/** - * Insert a value into a set - */ -export function insert_(E: Equivalence.Equivalence): (set: Set, a: A) => Set { - const elemE = elem_(E) - return (set, a) => { - if (!elemE(set, a)) { - const r = new Set(set) - r.add(a) - return r - } else { - return set - } - } -} - -/** - * Delete a value from a set - */ -export function remove(E: Equivalence.Equivalence): (a: A) => (set: Set) => Set { - const rem = remove_(E) - return (a) => (set) => rem(set, a) -} - -/** - * Delete a value from a set - */ -export function remove_(E: Equivalence.Equivalence): (set: Set, a: A) => Set { - return (set, a) => filter((ax: A) => !E(a, ax))(set) -} - -/** - * If element is present remove it, if not add it - */ -export function toggle(E: Equivalence.Equivalence): (a: A) => (set: Set) => Set { - const t = toggle_(E) - return (a) => (set) => t(set, a) -} - -/** - * If element is present remove it, if not add it - */ -export function toggle_(E: Equivalence.Equivalence): (set: Set, a: A) => Set { - const elemE = elem_(E) - const removeE = remove(E) - const insertE = insert(E) - return (set, a) => (elemE(set, a) ? removeE : insertE)(a)(set) -} - -/** - * Create a set from an array - */ -export function fromArray(E: Equivalence.Equivalence): (as: ReadonlyArray) => Set { - return (as) => { - const len = as.length - const r = new Set() - const has = elem_(E) - for (let i = 0; i < len; i++) { - const a = as[i]! - if (!has(r, a)) { - r.add(a) - } - } - return r - } -} - -/** - * Set compaction, remove none - */ -export function compact(E: Equivalence.Equivalence): (fa: Set>) => Set { - return filterMap(E)(identity) -} - -/** - * Separate elements - */ -export function separate( - EE: Equivalence.Equivalence, - EA: Equivalence.Equivalence -): (fa: Set>) => readonly [Set, Set] { - return (fa) => { - const elemEE = elem_(EE) - const elemEA = elem_(EA) - const left: MutableSet = new Set() - const right: MutableSet = new Set() - fa.forEach((e) => { - switch (e._tag) { - case "Failure": - if (!elemEE(left, e.failure)) { - left.add(e.failure) - } - break - case "Success": - if (!elemEA(right, e.success)) { - right.add(e.success) - } - break - } - }) - return Tuple.make(left, right) - } -} - -/** - * Filter + Map - */ -export function filterMap( - E: Equivalence.Equivalence -): (f: (a: A) => Option.Option) => (fa: Set) => Set { - const fm = filterMap_(E) - return (f) => (fa) => fm(fa, f) -} - -/** - * Filter + Map - */ -export function filterMap_( - E: Equivalence.Equivalence -): (fa: Set, f: (a: A) => Option.Option) => Set { - const elemE = elem_(E) - return (fa, f) => { - const r: MutableSet = new Set() - fa.forEach((a) => { - const ob = f(a) - if (ob._tag === "Some" && !elemE(r, ob.value)) { - r.add(ob.value) - } - }) - return r - } -} - -/** - * Form the union of two sets - */ -export function union_(E: Equivalence.Equivalence): (set: Set, y: Set) => Set { - const elemE = elem_(E) - return (x, y) => { - if (x === empty) { - return y - } - if (y === empty) { - return x - } - const r = new Set(x) - y.forEach((e) => { - if (!elemE(r, e)) { - r.add(e) - } - }) - return r - } -} - -/** - * Form the union of two sets - */ -export function union(E: Equivalence.Equivalence): (set: Set, y: Set) => Set { - const u = union_(E) - return (x, y) => u(x, y) -} - -function make_(ord: Order.Order, eq_?: Equivalence.Equivalence) { - const eq = eq_ ?? ((x, y) => ord(x, y) === 0) - - const fromArray_ = fromArray(eq) - const concat_ = (set: Set, it: Iterable) => fromArray_([...set, ...it]) - const insert__ = insert(eq) - - function replace_(set: Set, a: A) { - return pipe(filter_(set, (x) => !eq(x, a)), insert__(a)) - } - - return { - insert: insert__, - insert_: insert_(eq), - remove: remove(eq), - remove_: remove_(eq), - reduce: reduce(ord), - reduce_: reduce_(ord), - replace: (a: A) => (set: Set) => replace_(set, a), - replace_, - toArray: toArray(ord), - fromArray, - from: (it: Iterable) => fromArray_([...it]), - empty: () => new Set(), - concat_, - concat: (it: Iterable) => (set: Set) => concat_(set, it), - - // A and B the same, useful when editing elements. - map: map(eq), - map_: map_(eq), - filterMap: filterMap(eq), - filterMap_: filterMap_(eq) - } - // TODO: extend -} - -class Wrapper { - wrapped(ord: Order.Order, eq: Equivalence.Equivalence) { - return make_(ord, eq) - } -} - -export interface SetSchemaExtensions extends ReturnType["wrapped"]> {} - -export const make: ( - ord: Order.Order, - eq?: Equivalence.Equivalence -) => SetSchemaExtensions = make_ diff --git a/packages/effect-app/src/Store.ts b/packages/effect-app/src/Store.ts deleted file mode 100644 index f2030df9d..000000000 --- a/packages/effect-app/src/Store.ts +++ /dev/null @@ -1,325 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type * as Redacted from "effect/Redacted" -import * as Semaphore from "effect/Semaphore" -import type { NonEmptyReadonlyArray } from "./Array.ts" -import type { DatabaseError, OptimisticConcurrencyException } from "./client/errors.ts" -import * as Context from "./Context.ts" -import * as Effect from "./Effect.ts" -import type { FilterResult } from "./Model/filter/filterApi.ts" -import type { FieldValues } from "./Model/filter/types.ts" -import type { FieldPath } from "./Model/filter/types/path/index.ts" -import type { AggregateIrExpression, ComputedProjectionIrExpression, RawQuery } from "./Model/query.ts" -import type * as Option from "./Option.ts" -import * as RequestScopedDependencies from "./RequestScopedDependencies.ts" -import { type Json as SchemaJson, NonEmptyString255, type SchemaError, type Top as SchemaTop } from "./Schema.ts" - -/** - * Adapter-neutral unique-key definition for stores that support unique indexes, - * such as the Cosmos adapter. This shape is intentionally kept structurally - * compatible with adapter-specific `UniqueKey` types. Each path identifies a - * field participating in the unique key, and adapters forward these paths - * directly to the underlying storage engine. - */ -export interface UniqueKey { - readonly paths: string[] -} - -/** - * A raw stored JSON document: a readonly record of JSON values. - * - * This is the shape a document has *in the database*: dates are ISO strings, - * `ReadonlyMap`/`ReadonlySet` are arrays, and absent or legacy keys are - * whatever was written. It is deliberately not the `Encoded` shape, which - * holds native `Date`/`Map`/`Set` values. - */ -export type JsonRecord = { readonly [key: string]: SchemaJson } - -export interface StoreConfig { - partitionValue: (e?: E) => string - /** - * Primarily used for testing, creating namespaces in the database to separate data e.g to run multiple tests in isolation within the same database. - * Memory/Disk use separate store instances per namespace. CosmosDB uses namespace-prefixed partition keys. SQL uses a `_namespace` column. - */ - allowNamespace?: (namespace: string) => boolean - /** - * just in time migrations, supported by the database driver, supporting queries, for simple default values - */ - defaultValues?: Partial - - /** - * just in time Migration: for complex migrations that aren't just simple - * default values - use {@link defaultValues} for those. - * - * Runs at the store boundary on the **raw JSON document**: after - * {@link defaultValues} have been merged in (they still only fill *absent* - * keys) and *before* the JSON→Encoded decode - * (`Schema.toCodecJson(Schema.toEncoded(schema))`). - * - * It therefore sees exactly what is stored - a `Date` field as an ISO - * string, a `ReadonlyMap` as an array of pairs, an explicit `null` as - * `null` - and it must return JSON again, never native Encoded values - * (no `Date`/`Map`/`Set` instances). That is what lets it repair legacy - * shapes, including explicit `null`s, before any schema decode happens. - * - * `_etag` is infrastructure metadata and is not part of the document passed - * to `jitM`. Not applied on the write/encode path. - */ - jitM?: (json: JsonRecord) => JsonRecord - - /** - * How many items can be processed in one batch at a time. - * Defaults to 100 for CosmosDB. - */ - maxBulkSize?: number - - /** - * Unique indexes, mainly for CosmosDB - */ - uniqueKeys?: UniqueKey[] - /** - * Domain schema whose Encoded shape is stored. JSON adapters use - * `Schema.toCodecJson(Schema.toEncoded(schema))` for documents, query - * values, and defaults. Date/Map/Set (and app types such as DateOnly) - * stay native on Encoded and lower only on this JSON boundary. - */ - schema?: SchemaTop -} - -export type SupportedValues = string | boolean | number | null -export type SupportedValues2 = string | boolean | number - -// default is eq -export type Where = - | { key: string; t?: "eq" | "not-eq"; value: SupportedValues } - | { key: string; t: "gt" | "lt" | "gte" | "lte"; value: SupportedValues2 } - | { - key: string - t: "contains" | "starts-with" | "ends-with" | "not-contains" | "not-starts-with" | "not-ends-with" - value: string - } - | { key: string; t: "includes" | "not-includes"; value: string } - | { - key: string - t: "in" | "not-in" - value: readonly (SupportedValues)[] - } - -export type Filter = readonly FilterResult[] - -export interface O { - key: FieldPath - direction: "ASC" | "DESC" -} - -export interface FilterArgs { - t: Encoded - filter?: Filter | undefined - select?: - | NonEmptyReadonlyArray< - U | { key: string; subKeys: readonly string[] } | { - key: string - computed: ComputedProjectionIrExpression - } | { - key: string - path: string - } | { - key: string - aggregate: AggregateIrExpression - } - > - | undefined - order?: NonEmptyReadonlyArray>> - limit?: number | undefined - skip?: number | undefined -} - -/** decodes stored documents unless `select` projects fields, hence `SchemaError` */ -export type FilterFunc = ( - args: FilterArgs -) => Effect.Effect<(U extends undefined ? Encoded : Pick)[], DatabaseError | SchemaError> - -export interface Store< - IdKey extends keyof Encoded, - Encoded extends FieldValues, - PM extends PersistenceModelType = PersistenceModelType -> { - /** decodes every stored document, so a document `jitM` does not repair fails with `SchemaError` */ - all: Effect.Effect - filter: FilterFunc - /** decodes the stored document, so a document `jitM` does not repair fails with `SchemaError` */ - find: (id: Encoded[IdKey]) => Effect.Effect, DatabaseError | SchemaError> - /** adapters that decode the written document on the way back can fail with `SchemaError` */ - set: (e: PM) => Effect.Effect - /** adapters that decode the written documents on the way back can fail with `SchemaError` */ - batchSet: ( - items: NonEmptyReadonlyArray - ) => Effect.Effect, OptimisticConcurrencyException | DatabaseError | SchemaError> - /** adapters that decode the written documents on the way back can fail with `SchemaError` */ - bulkSet: ( - items: NonEmptyReadonlyArray - ) => Effect.Effect, OptimisticConcurrencyException | DatabaseError | SchemaError> - batchRemove: (ids: NonEmptyReadonlyArray, partitionKey?: string) => Effect.Effect - /** - * Runs the raw query over stored JSON documents, not Encoded. - * `Repository.queryRaw` applies `toCodecJson` to the projector output. - */ - queryRaw: (query: RawQuery) => Effect.Effect - /** - * Explicitly seed a namespace. Primary is seeded eagerly on initialization. - * Non-primary namespaces must be seeded explicitly before use. - */ - seedNamespace: (namespace: string) => Effect.Effect -} - -export class StoreMaker extends Context.Opaque( - name: string, - idKey: IdKey, - seed?: Effect.Effect, E, R>, - config?: StoreConfig - ) => Effect.Effect, E, R> -}>()("effect-app/StoreMaker") { -} - -export const makeContextMap = () => { - const etags = new Map() - const getEtag = (id: string) => etags.get(id) - const setEtag = (id: string, eTag: string | undefined) => { - if (eTag === undefined) { - etags.delete(id) - } else { - etags.set(id, eTag) - } - } - - // const parsedCache = new Map< - // Parser, - // Map> - // >() - - // const parserCache = new Map< - // Parser, - // (i: any) => These.These - // >() - - // const setAndReturn = ( - // p: Parser, - // np: (i: I) => These.These - // ) => { - // parserCache.set(p, np) - // return np - // } - - // const parserEnv: ParserEnv = { - // // TODO: lax: true would turn off refinement checks, may help on large payloads - // // but of course removes confirming of validation rules (which may be okay for a database owned by the app, as we write safely) - // lax: false, - // cache: { - // getOrSetParser: (p) => parserCache.get(p) ?? setAndReturn(p, (i) => parserEnv.cache!.getOrSet(i, p)), - // getOrSetParsers: (parsers) => { - // return Object.entries(parsers).reduce((prev, [k, v]) => { - // prev[k] = parserEnv.cache!.getOrSetParser(v) - // return prev - // }, {} as any) - // }, - // getOrSet: (i, parse): any => { - // const c = parsedCache.get(parse) - // if (c) { - // const f = c.get(i) - // if (f) { - // // console.log("$$$ cache hit", i) - // return f - // } else { - // const nf = parse(i, parserEnv) - // c.set(i, nf) - // return nf - // } - // } else { - // const nf = parse(i, parserEnv) - // parsedCache.set(parse, new Map([[i, nf]])) - // return nf - // } - // } - // } - // } - - const store = new Map() - const sem = Semaphore.makeUnsafe(1) - - return { - createdAt: new Date(), - get: getEtag, - set: setEtag, - getOrCreateStore: (key: symbol, make: () => T): T => { - let value = store.get(key) as T | undefined - if (value === undefined) { - value = make() - store.set(key, value) - } - return value - }, - getOrCreateStoreEffect: (key: symbol, make: Effect.Effect): Effect.Effect => - sem.withPermits(1)(Effect.uninterruptible(Effect.gen(function*() { - const value = store.get(key) as T | undefined - if (value !== undefined) return value - const v = yield* make - store.set(key, v) - return v - }))), - clear: () => { - etags.clear() - store.clear() - } - } -} - -const makeMap = Effect.acquireRelease( - Effect.sync(() => makeContextMap()), - (m) => Effect.sync(() => m.clear()) -) - -export class ContextMap extends Context.Opaque()("effect-app/ContextMap", { make: makeMap }) { -} - -export const ContextMapContainer = RequestScopedDependencies.make( - "ContextMapContainer", - ContextMap.make.pipe(Effect.map(ContextMap.of)) -) - -export type ContextMapNotStartedError = RequestScopedDependencies.RequestScopedDependencyNotStartedError - -export const getContextMap = ContextMapContainer.current - -/** - * Runs `make` at most once per ContextMap (i.e. per request) and caches the - * resulting value in the ContextMap under a fresh symbol. Subsequent calls of - * the returned Effect within the same ContextMap return the cached value. - * - * Uses the ContextMap's shared semaphore for safe single initialization. - */ -export const cachedPerRequest = ( - make: Effect.Effect -): Effect.Effect => { - const cacheKey = Symbol() - return getContextMap.pipe( - Effect.flatMap((ctxMap) => ctxMap.getOrCreateStoreEffect(cacheKey, make)) - ) -} - -const defaultNs: NonEmptyString255 = NonEmptyString255("primary") -export class storeId extends Context.Reference("StoreId", { defaultValue: (): NonEmptyString255 => defaultNs }) {} - -export type PersistenceModelType = Encoded & { - _etag?: string | undefined -} - -export interface StorageConfig { - url: Redacted.Redacted - prefix: string - dbName: string - /** - * Cosmos only: autoscale max RU/s for containers the store creates, in databases - * without shared throughput. Unset keeps the Cosmos default (manual 400 RU/s). - */ - autoscaleMaxThroughput?: number | undefined -} diff --git a/packages/effect-app/src/TypeTest.ts b/packages/effect-app/src/TypeTest.ts deleted file mode 100644 index d7b7018db..000000000 --- a/packages/effect-app/src/TypeTest.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Nice way to underline types that are only there for type testing, not for production use -// sadly with unique symbols we get weird issues in app projects. -// api/src/X/PackList.Controllers.ts:21:1 - error TS4082: Default export of the module has or is using private name 'TypeTestId' -// export const TypeTestId: unique symbol = Symbol.for("@effect/infra/type-test") -// export type TypeTestId = typeof TypeTestId -export const TypeTestId = "effect-app/type-test" as const -export type TypeTestId = typeof TypeTestId diff --git a/packages/effect-app/src/Types.ts b/packages/effect-app/src/Types.ts deleted file mode 100644 index 99bc8370d..000000000 --- a/packages/effect-app/src/Types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * from "effect/Types" - -export type DeepMutable = T extends ReadonlyMap ? Map, DeepMutable> - : T extends ReadonlySet ? Set> - : [keyof T] extends [never] ? T - // keep brands alive - : T extends string ? T - : T extends number ? T - : { -readonly [K in keyof T]: DeepMutable } - -export type Mutable = { - -readonly [P in keyof T]: T[P] -} - -export type DistributiveOmit = T extends any ? Omit - : never diff --git a/packages/effect-app/src/Widen.type.ts b/packages/effect-app/src/Widen.type.ts deleted file mode 100644 index 6ad23d875..000000000 --- a/packages/effect-app/src/Widen.type.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { BuiltInObject } from "effect-app/utils" - -type AllKeys = T extends any ? keyof T : never - -type OptionalKeys = T extends any ? { [K in keyof T]-?: {} extends Pick ? K : never }[keyof T] - : never - -type Idx = T extends any ? K extends keyof T ? T[K] - : D - : never - -type PartialKeys = - & Omit - & Partial> extends infer O ? { [P in keyof O]: O[P] } - : never - -export type Primitive = boolean | string | number | bigint | symbol | undefined | null -export type Widen = [T] extends [BuiltInObject] | [Primitive] ? T - : [T] extends [Array] ? { [K in keyof T]: Widen } - : [T] extends [ReadonlyArray] ? { [K in keyof T]: Widen } - : [T] extends [object] ? PartialKeys< - { [K in AllKeys]: Widen> }, - Exclude, keyof T> | OptionalKeys - > - : T diff --git a/packages/effect-app/src/_ext/Array.ts b/packages/effect-app/src/_ext/Array.ts deleted file mode 100644 index 27f2ad0fa..000000000 --- a/packages/effect-app/src/_ext/Array.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as Chunk from "effect-app/Chunk" -import * as Effect from "effect-app/Effect" -import * as Equal from "effect/Equal" -import { NotFoundError } from "../client/errors.ts" - -function getFirstBy( - a: Iterable, - idKey: keyof A, - id: A[typeof idKey], - type: Type -) { - return Effect - .fromOption( - Chunk.fromIterable(a).pipe(Chunk.findFirst((_) => Equal.equals(_[idKey], id))) - ) - .pipe(Effect.mapError(() => new NotFoundError({ type, id }))) -} - -export function makeGetFirstBy() { - return (idKey: Id, type: Type) => - ( - a: Iterable, - id: A[Id] - ) => getFirstBy(a, idKey, id, type) -} - -export const makeGetFirstById = () => (type: Type) => - makeGetFirstBy()("id", type) - -export function getFirstById( - a: Iterable, - id: A["id"], - type: Type -) { - return getFirstBy(a, "id", id, type) -} diff --git a/packages/effect-app/src/_ext/ord.ext.ts b/packages/effect-app/src/_ext/ord.ext.ts deleted file mode 100644 index b89c2c949..000000000 --- a/packages/effect-app/src/_ext/ord.ext.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as Chunk from "effect/Chunk" -import type * as Equivalence from "effect/Equivalence" - -export function toArray( - gen: Generator -) { - return Array.from(gen) -} - -/** - * Remove duplicates from an array, keeping the first occurrence of an element. - */ -export function uniq(E: Equivalence.Equivalence) { - return (self: Chunk.Chunk): Chunk.Chunk => { - let out = Chunk.fromIterable([]) - for (let i = 0; i < self.length; i++) { - const a = Chunk.getUnsafe(self, i) - if (!elem(E, a)(out)) { - out = Chunk.append(out, a) - } - } - return self.length === out.length ? self : out - } -} - -/** - * Test if a value is a member of an array. Takes a `Equivalence.Equivalence` as a single - * argument which returns the function to use to search for a value of type `A` - * in an array of type `Chunk.Chunk`. - */ -export function elem(E: Equivalence.Equivalence, value: A) { - return (self: Chunk.Chunk): boolean => { - for (let i = 0; i < self.length; i++) { - if (E(Chunk.getUnsafe(self, i), value)) { - return true - } - } - return false - } -} diff --git a/packages/effect-app/src/builtin-json.d.ts b/packages/effect-app/src/builtin-json.d.ts deleted file mode 100644 index 1e643f362..000000000 --- a/packages/effect-app/src/builtin-json.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// JSON / Body return-type overrides (`any` -> `unknown`), isolated from -// `builtin.ts` so consumers that source-link this package (e.g. an embedded -// Effect-source setup) can toggle them off. As a `declare global`, the override -// is program-wide: in linked mode it leaks into source-linked Effect and breaks -// Effect's own `JSON.parse(...)` / `response.json()` call sites (TS2322 -// `unknown` vs `Json`). `builtin.ts` triple-slash-references this file, so -// normal builds keep the override. -declare global { - interface JSON { - /** - * Converts a JavaScript Object Notation (JSON) string into an object. - * @param text A valid JSON string. - * @param reviver A function that transforms the results. This function is called for each member of the object. - * If a member contains nested objects, the nested objects are transformed before the parent object is. - */ - parse(text: string, reviver?: (this: any, key: string, value: any) => any): unknown - } - - interface Body { - json(): Promise - } -} - -export {} diff --git a/packages/effect-app/src/builtin.ts b/packages/effect-app/src/builtin.ts deleted file mode 100644 index 084a6ce79..000000000 --- a/packages/effect-app/src/builtin.ts +++ /dev/null @@ -1,53 +0,0 @@ -// oxlint-disable-next-line typescript/triple-slash-reference -/// -import type { NonEmptyArray, NonEmptyReadonlyArray } from "effect/Array" - -declare global { - interface String { - /** - * Split a string into substrings using the specified separator and return them as an array. - * @param splitter An object that can split a string. - * @param limit A value used to limit the number of elements returned in the array. - */ - split(splitter: { [Symbol.split](string: string, limit?: number): string[] }, limit?: number): [string, ...string[]] - - /** - * Split a string into substrings using the specified separator and return them as an array. - * @param separator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned. - * @param limit A value used to limit the number of elements returned in the array. - */ - split(separator: string | RegExp, limit?: number): [string, ...string[]] - } - - // JSON.parse / Body.json overrides live in ./builtin-json.d.ts (referenced - // above) so a source-linked consumer can disable them in linked mode. - - interface Array { - // Conditional return instead of a `this`-typed overload: a `this` overload - // is selected-then-rejected on union-of-array receivers (TS2684), poisoning - // foreign code like `(readonly A[] | readonly B[]).map(...)`. A single - // conditional-return signature refines NonEmpty without a `this` param. - map( - callbackfn: (value: T, index: number, array: T[]) => U, - thisArg?: any - ): this extends NonEmptyArray ? NonEmptyArray : U[] - } - interface ReadonlyArray { - // Subsequent property declarations must have the same type. Property 'length' must be of type 'number', but here has type 'NonNegativeInt'. - // readonly length: NonNegativeInt - - map( - callbackfn: (value: T, index: number, array: readonly T[]) => U, - thisArg?: any - ): this extends NonEmptyReadonlyArray ? NonEmptyReadonlyArray : U[] - } -} - -declare module "effect/Option" { - export interface None { - get value(): A | undefined - } -} - -// TODO: v4 migration — Either module augmentation removed (Either → Result) -// Previously added .right to Left and .left to Right for convenience access diff --git a/packages/effect-app/src/client.ts b/packages/effect-app/src/client.ts deleted file mode 100644 index 578b82dd7..000000000 --- a/packages/effect-app/src/client.ts +++ /dev/null @@ -1,8 +0,0 @@ -// codegen:start {preset: barrel, include: ./client/*.ts} -export * from "./client/apiClientFactory.ts" -export * from "./client/clientFor.ts" -export * from "./client/errors.ts" -export * from "./client/InvalidationKeys.ts" -export * from "./client/makeClient.ts" -// codegen:end -export * as DataDependencies from "./DataDependencies.ts" diff --git a/packages/effect-app/src/client/InvalidationKeys.ts b/packages/effect-app/src/client/InvalidationKeys.ts deleted file mode 100644 index 6e0086335..000000000 --- a/packages/effect-app/src/client/InvalidationKeys.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as Equal from "effect/Equal" -import * as Ref from "effect/Ref" -import * as Context from "../Context.ts" -import * as Effect from "../Effect.ts" -import type { InvalidationKey } from "../rpc/Invalidation.ts" - -export type { InvalidationKey } -/** Shape of the per-mutation service that accumulates server-provided invalidation keys. */ -export interface InvalidationKeysService { - readonly add: (key: InvalidationKey) => Effect.Effect - readonly get: Effect.Effect> -} - -/** - * Context.Reference that accumulates invalidation keys received from the server via the - * `x-invalidate` HTTP response header. - * - * The default is a no-op: when not explicitly provided (e.g. outside a mutation wrapper) - * all calls are ignored. The mutation wrapper in `@effect-app/vue` provides a real - * implementation backed by a `Ref`. - */ -export const InvalidationKeysFromServer = Context.Reference( - "effect-app/client/InvalidationKeysFromServer", - { - defaultValue: () => ({ - add: (_key: InvalidationKey) => Effect.void, - get: Effect.succeed([] as ReadonlyArray) - }) - } -) -export type InvalidationKeysFromServer = typeof InvalidationKeysFromServer - -/** - * Creates a fresh `InvalidationKeysService` implementation backed by a `Ref`. - * - * @param ref - The `Ref` that stores the accumulated keys. - * @param onAdded - V3: Optional Effect run after a distinct key is added. Use to trigger - * mid-stream query invalidation without waiting for the stream to complete. Repeated keys - * stay accumulated for the final refresh but do not retrigger the callback. - */ -export const makeInvalidationKeysService = ( - ref: Ref.Ref>, - onAdded?: (key: InvalidationKey) => Effect.Effect -): InvalidationKeysService => ({ - add: (key) => - Ref - .modify(ref, (keys) => { - if (keys.some((existing) => Equal.equals(existing, key))) return [false, keys] - return [true, [...keys, key]] - }) - .pipe( - Effect.flatMap((added) => added && onAdded ? onAdded(key) : Effect.void) - ), - get: Ref.get(ref) -}) diff --git a/packages/effect-app/src/client/apiClientFactory.ts b/packages/effect-app/src/client/apiClientFactory.ts deleted file mode 100644 index b95783108..000000000 --- a/packages/effect-app/src/client/apiClientFactory.ts +++ /dev/null @@ -1,465 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { constant, flow } from "effect/Function" -import * as Layer from "effect/Layer" -import * as ManagedRuntime from "effect/ManagedRuntime" -import * as Option from "effect/Option" -import * as Predicate from "effect/Predicate" -import * as Record from "effect/Record" -import { Rpc, RpcClient, RpcGroup, RpcSerialization } from "effect/rpc" -import * as Schema from "effect/Schema" -import * as Stream from "effect/Stream" -import * as Struct from "effect/Struct" -import * as Config from "../Config.ts" -import * as Context from "../Context.ts" -import * as DataDependencies from "../DataDependencies.ts" -import * as Effect from "../Effect.ts" -import { HttpClient, HttpClientRequest } from "../http.ts" -import { Invalidation } from "../rpc.ts" -import type * as S from "../Schema.ts" -import type { Client, ClientForOptions, ExtractModuleName, RequestsAny } from "./clientFor.ts" -import { InvalidationKeysFromServer } from "./InvalidationKeys.ts" - -export interface ApiConfig { - url: string - headers: Option.Option> -} - -export const DefaultApiConfig = Config.all({ - url: Config.String("apiUrl").pipe(Config.withDefault("/api")), - headers: Config.Record(Schema.String, Schema.String, "headers").pipe(Config.option) -}) - -export type Req = S.Top & { - readonly make: (...args: any[]) => any - _tag: string - fields: S.Struct.Fields - success: S.Top - error: S.Top - /** Optional final-value schema for stream requests. When set, the execute effect resolves with the last stream value decoded to this type. */ - final?: S.Top - config?: Record - readonly id: string - readonly moduleName: string - readonly type: "command" | "query" - readonly stream: boolean - readonly middleware?: unknown -} - -class RequestName extends Context.Reference("RequestName", { - defaultValue: () => ({ requestName: "Unspecified", moduleName: "Error" }) -}) {} - -export const HttpClientLayer = (config: ApiConfig) => - Layer.effect( - HttpClient.HttpClient, - Effect - .gen(function*() { - const baseClient = yield* HttpClient.HttpClient - const client = baseClient.pipe( - HttpClient.mapRequest(HttpClientRequest.prependUrl(config.url + "/rpc")), - HttpClient.mapRequest( - HttpClientRequest.setHeaders(config.headers.pipe(Option.getOrElse(() => ({})))) - ), - HttpClient.mapRequestEffect((req) => - Effect.map(RequestName, (ctx) => - flow( - HttpClientRequest.appendUrlParam("action", ctx.requestName), - HttpClientRequest.appendUrl("/" + ctx.moduleName) - )(req)) - ) - ) - return client - }) - ) - -export const HttpClientFromConfigLayer = Layer.unwrap( - Effect.gen(function*() { - const config = yield* DefaultApiConfig - return HttpClientLayer(config) - }) -) - -export const RpcSerializationLayer = (config: ApiConfig) => - Layer.mergeAll( - RpcSerialization.layerNdjson, - HttpClientLayer(config) - ) - -type RpcHandlers = { - [K in keyof M]: Rpc.Rpc -} - -const getFiltered = (resource: M) => { - type Filtered = { - [K in keyof M as M[K] extends Req ? K : never]: M[K] extends Req ? M[K] : never - } - // TODO: Record.filter - const filtered = Struct.keys(resource).reduce((acc, cur) => { - if ( - Predicate.isObjectKeyword(resource[cur]) - && (resource[cur].success) - ) { - acc[cur as keyof Filtered] = resource[cur] as any - } - return acc - }, {} as Record) - - return filtered as unknown as Filtered -} - -export const getMeta = ( - resource: M -): { moduleName: ExtractModuleName; middleware?: unknown } => { - const first = Record.values(getFiltered(resource))[0] - if (first && "moduleName" in first) return { moduleName: first.moduleName, middleware: (first as any).middleware } - throw new Error("No moduleName on requests!") -} - -export const makeRpcGroupFromRequestsAndModuleName = ( - resource: M, - moduleName: ModuleName, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - middleware?: any -) => { - const filtered = getFiltered(resource) - type newM = typeof filtered - const baseRpcs = RpcGroup - .make( - ...Record.values(filtered).map((_) => { - const r = _ as any - const isStream = r.stream - const isCommand = r.type === "command" - return (isCommand - ? isStream - ? Invalidation.makeStreamRpc(r._tag, { - payload: r, - success: r.success, - error: r.error, - stream: true as const - }) - : Invalidation.makeCommandRpc(r._tag, { payload: r, success: r.success, error: r.error }) - : Rpc.make(r._tag, { - payload: r, - success: isStream ? r.success : Invalidation.QueryResponseWithMetaData(r.success), - error: r.error, - stream: isStream - })) as any - }) - ) - .prefix(`${moduleName}.`) - // Attach the middleware tag (schema-only on the client — no Live invoked) - // so its declared `error` joins the rpc failure union via - // `Rpc.exitSchema`'s `rpc.middlewares[*].error` walk. Required for stream - // rpcs whose top-level `errorSchema` is forced to `Never` by effect-rpc; - // without it, middleware-thrown errors fail client decode. - const rpcs = (middleware ? baseRpcs.middleware(middleware) : baseRpcs) as unknown as RpcGroup.RpcGroup< - Rpc.Prefixed[keyof newM], `${ModuleName}.`> - > - return rpcs -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const makeRpcTag = (resource: M, middleware?: any) => { - const meta = getMeta(resource) - const rpcs = makeRpcGroupFromRequestsAndModuleName(resource, meta.moduleName, middleware) - - // Use Object.assign instead of class extension to avoid TS2509 with complex generic return types. - // The first type arg is `any` because this is a dynamically created tag — its identity is the string key. - const TheClient = Context.Opaque< - any, - RpcClient.RpcClient> - >()(`RpcClient.${meta.moduleName}`) - // Use Layer.effect directly (not TheClient.toLayer) so TypeScript properly excludes Scope - const layer = Layer.effect( - TheClient, - RpcClient.make(rpcs) - ) - return Object.assign(TheClient, { layer }) -} - -const makeApiClientFactory = Effect - .gen(function*() { - const ctx = yield* Effect.context() - const makeClientFor = Effect.fnUntraced(function*( - resource: M, - requestLevelLayers = Layer.empty, - options?: ClientForOptions - ) { - const meta = getMeta(resource) - - const TheClient = makeRpcTag(resource, meta.middleware) - - // TODO: somehow we need a protocol per REQUEST kind of it seems ... - // otherwise it locks up on the client, navigation remains empty... - const clientLayer = TheClient.layer.pipe( - // add ApiClientFactory for nested schemas - // eslint-disable-next-line @typescript-eslint/no-use-before-define - Layer.provide(Layer.succeed(ApiClientFactory, makeClientForCached as any)), - Layer.provide( - RpcClient - .layerProtocolHttp({ url: "" }) // why not here set meta.moduleName as root? - .pipe( - Layer.provideMerge(Layer.succeedContext(ctx)) - ) - ) - ) - const mr = ManagedRuntime.make(clientLayer) - - const filtered = getFiltered(resource) - - // Unwrap `CommandResponseWithMetaData` (success) and `CommandFailureWithMetaData` - // (handler-thrown failure): forward accumulated invalidation keys to - // `InvalidationKeysFromServer` and yield the raw payload / re-fail with the raw - // error. Middleware-thrown failures arrive raw on the Cause already (no wrap to - // strip) — the `else` branch passes them through. - const forwardDependencies = ( - dependencies: DataDependencies.DataDependencySet | undefined, - options?: { readonly reads?: boolean } - ) => - dependencies === undefined - ? Effect.void - : Effect.gen(function*() { - const dependencyRecorder = yield* DataDependencies.getDataDependencyRecorder - if (options?.reads) { - yield* Effect.forEach(dependencies.reads, dependencyRecorder.read, { discard: true }) - } - yield* Effect.forEach(dependencies.writes, dependencyRecorder.write, { discard: true }) - }) - - const unwrapCommand = (eff: Effect.Effect): Effect.Effect => - eff.pipe( - Effect.flatMap((result: any) => - Effect.gen(function*() { - const keys: ReadonlyArray = result?.metadata?.invalidateQueries ?? [] - const invalidationKeys = yield* InvalidationKeysFromServer - yield* Effect.forEach(keys, (key) => invalidationKeys.add(key), { discard: true }) - yield* forwardDependencies(result?.metadata?.dataDependencies) - return result.payload - }) - ), - Effect.catch((result: any) => - result?._tag === "CommandFailureWithMetaData" - ? Effect.gen(function*() { - const keys: ReadonlyArray = result.metadata?.invalidateQueries ?? [] - const invalidationKeys = yield* InvalidationKeysFromServer - yield* Effect.forEach(keys, (key) => invalidationKeys.add(key), { discard: true }) - yield* forwardDependencies(result.metadata?.dataDependencies) - return yield* Effect.fail(result.error) - }) - : Effect.fail(result) - ) - ) - - const unwrapQuery = (eff: Effect.Effect): Effect.Effect => - eff.pipe( - Effect.flatMap((result: any) => - Effect.gen(function*() { - yield* forwardDependencies(result?.metadata?.dataDependencies, { reads: true }) - return result.payload - }) - ) - ) - - return { - mr, - client: Struct - .keys(filtered) - .reduce((prev, cur) => { - const h = filtered[cur]! - - const Request = h - - const id = `${meta.moduleName}.${cur}` - .replaceAll(".js", "") - - const requestMeta = { - Request, - id, - options, - disableQueryInvalidation: (h as { config?: { disableQueryInvalidation?: boolean } }) - .config - ?.disableQueryInvalidation === true - } - - const requestNameLayer = Layer.succeed(RequestName, { - requestName: cur, - moduleName: meta.moduleName - }) - - const layers = requestLevelLayers.pipe(Layer.provideMerge(requestNameLayer)) - - const fields = Struct.omit(Request.fields, ["_tag"] as const) - const requestAttr = `${meta.moduleName}.${h._tag}` - const isCommand = h.type === "command" - const isStream = h.stream - - const buildEffect = (input: any) => - mr.contextEffect.pipe( - Effect.flatMap((svcs) => { - const rpcEffect = TheClient - .use((client) => (client as any)[requestAttr]!(Request.make(input)) as Effect.Effect) - .pipe( - // local: true → isolate to this RPC call. `layers` is pure today - // (RequestName + caller-supplied requestLevelLayers), but the - // flag prevents a stateful caller-supplied layer from leaking - // across RPC calls via the ambient fiber MemoMap. - Effect.provide(layers, { local: true }), - Effect.provide(svcs) - ) - return isCommand ? unwrapCommand(rpcEffect) : isStream ? rpcEffect : unwrapQuery(rpcEffect) - }), - // Forwarding the server's dependency metadata needs a recorder in scope. A parent - // query/command (vue) provides one; a top-level call gets a fresh per-request one. - DataDependencies.ensureDataDependencyRecorder - ) - - const buildStream = (input: any) => { - const stream = Stream.unwrap( - mr.contextEffect.pipe( - Effect.flatMap((svcs) => - TheClient - .useSync((client) => { - const rpcStream = (client as any)[requestAttr]!( - Request.make(input) - ) as Stream.Stream - return rpcStream.pipe( - // Collect server invalidation keys from the "done" chunk, then discard it. - Stream.tap((item: any) => - item._tag === "done" || item._tag === "metadata" - ? Effect.gen(function*() { - const metadata = item.metadata as Invalidation.CommandMetaData - const invalidationKeys = yield* InvalidationKeysFromServer - yield* Effect.forEach(metadata.invalidateQueries, invalidationKeys.add, { - discard: true - }) - const dependencyRecorder = yield* DataDependencies.getDataDependencyRecorder - yield* Effect.forEach( - metadata.dataDependencies.reads, - dependencyRecorder.read, - { discard: true } - ) - yield* Effect.forEach( - metadata.dataDependencies.writes, - dependencyRecorder.write, - { discard: true } - ) - }) - : Effect.void - ), - Stream.filter((item: any) => item._tag === "value"), - Stream.map((item: any) => item.value), - // V2: unwrap StreamFailureChunk — forward keys from failures too, - // then re-fail with the original error so callers see the unmodified - // error type. - Stream.catch((err: any) => - err?._tag === "error" && err?.metadata - ? Stream.fromEffect( - Effect.gen(function*() { - const metadata = err.metadata as Invalidation.CommandMetaData - const invalidationKeys = yield* InvalidationKeysFromServer - yield* Effect.forEach(metadata.invalidateQueries, invalidationKeys.add, { - discard: true - }) - const dependencyRecorder = yield* DataDependencies.getDataDependencyRecorder - yield* Effect.forEach( - metadata.dataDependencies.writes, - dependencyRecorder.write, - { discard: true } - ) - return yield* Effect.fail(err.error) - }) - ) - : Stream.fail(err) - ), - // local: true — see buildEffect above. - Stream.provide(layers, { local: true }), - Stream.provide(svcs) - ) - }) - .pipe(Effect.provide(svcs)) - ) - ) - ) - // Forwarding the server's dependency metadata needs a recorder in scope. A parent - // query/command (vue) provides one; a top-level stream call gets a fresh per-request one. - return Stream.unwrap( - Effect.map( - DataDependencies.DataDependencyRecorder, - (current) => - current === "root" - ? Stream.provide(stream, DataDependencies.DataDependencyRecorder.layer) - : stream - ) - ) - } - - // @ts-expect-error doc - prev[cur] = Object.keys(fields).length === 0 - ? { - handler: isStream ? constant(buildStream({})) : constant(buildEffect({})), - ...requestMeta - } - : { - handler: isStream - ? (req: any) => buildStream(req) - : (req: any) => buildEffect(req), - ...requestMeta - } - - return prev - }, {} as Client>) - } - }) - - const register: ManagedRuntime.ManagedRuntime[] = [] - yield* Effect.addFinalizer(() => Effect.forEach(register, (mr) => mr.disposeEffect)) - - const cacheL = new Map>>() - - function makeClientForCached(requestLevelLayers: Layer.Layer, options?: ClientForOptions) { - let cache = cacheL.get(requestLevelLayers) - if (!cache) { - cache = new Map>() - cacheL.set(requestLevelLayers, cache) - } - - return Effect.fnUntraced(function*(models: M) { - const found = cache.get(models) as Client> | undefined - if (found) { - return found - } - const m = yield* makeClientFor(models, requestLevelLayers, options) - cache.set(models, m.client) - register.push(m.mr) - return m.client - }) - } - - return makeClientForCached - }) - -/** - * Used to create clients for resource modules. - */ -export class ApiClientFactory - extends Context.Opaque>()("ApiClientFactory") -{ - static readonly layer = (config: ApiConfig) => - ApiClientFactory.toLayer(makeApiClientFactory).pipe(Layer.provide(RpcSerializationLayer(config))) - static readonly layerFromConfig = Layer.unwrap( - Effect.gen(function*() { - const config = yield* DefaultApiConfig - return ApiClientFactory.layer(config) - }) - ) - - static readonly makeFor = - (requestLevelLayers: Layer.Layer, options?: ClientForOptions) => - ( - resource: M - ) => - ApiClientFactory.use((apiClientFactory) => { - const f = apiClientFactory(requestLevelLayers, options) - return f(resource) - }) -} diff --git a/packages/effect-app/src/client/clientFor.ts b/packages/effect-app/src/client/clientFor.ts deleted file mode 100644 index bb4da478e..000000000 --- a/packages/effect-app/src/client/clientFor.ts +++ /dev/null @@ -1,186 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as Record from "effect/Record" -import type * as Stream from "effect/Stream" -import type { Path } from "path-parser" -import qs from "query-string" -import type * as Effect from "../Effect.ts" -import type * as S from "../Schema.ts" -import { type Req } from "./apiClientFactory.ts" - -export function makePathWithQuery( - path: Path, - pars: Record< - string, - | string - | number - | boolean - | readonly string[] - | readonly number[] - | readonly boolean[] - | null - > -) { - const forQs = Record.filter(pars, (_, k) => !path.params.includes(k)) - const q = forQs // { ...forQs, _: JSON.stringify(forQs) } // TODO: drop completely individual keys from query?, sticking to json only - return ( - path.build(pars, { ignoreSearch: true, ignoreConstraints: true }) - + (Object.keys(q).length - ? "?" + qs.stringify(q) - : "") - ) -} - -export function makePathWithBody( - path: Path, - pars: Record< - string, - | string - | number - | boolean - | readonly string[] - | readonly number[] - | readonly boolean[] - | null - > -) { - return path.build(pars, { ignoreSearch: true, ignoreConstraints: true }) -} - -export type Requests = RequestsAny -export type RequestsAny = Record - -export type ExtractModuleName = - { [K in keyof M]: M[K] extends { moduleName: infer N extends string } ? N : never }[keyof M] extends - infer R extends string ? R - : string - -export type Client = RequestHandlers< - never, - never, - M, - ModuleName -> - -export type ExtractResponse = T extends S.Codec ? T["Type"] - : T extends unknown ? void - : never - -export type ExtractEResponse = T extends S.Codec ? T["Encoded"] - : T extends unknown ? void - : never - -export interface ClientForOptions { - readonly skipQueryKey?: readonly string[] - /** - * Middleware tag to attach to every rpc on the client. Schema-only — the - * client never invokes the middleware (no Live impl required), but its - * declared `error` schema joins the rpc failure union via - * `Rpc.exitSchema`'s `rpc.middlewares[*].error` walk. Required when - * middleware can throw errors that aren't part of the resource's declared - * error union (e.g. auth middleware throwing `NotLoggedInError`); without - * it the client decode would fail with a `SchemaError` for stream rpcs. - */ -} - -// $Project/$Configuration.Index -// -> "$Project", "$Configuration", "Index" -export const makeQueryKey = ({ id, options }: { id: string; options?: ClientForOptions }) => - id - .split("/") - .filter((segment: string) => !options || !options.skipQueryKey?.includes(segment)) - .map((segment: string) => "$" + segment) - .join(".") - .split(".") - -export interface RequestHandlerWithInput { - handler: (i: I) => Effect.Effect - id: Id - options?: ClientForOptions - Request: Request - queryKeyProjectionHash?: string - disableQueryInvalidation?: boolean -} - -/** Type alias: a no-input handler is simply `RequestHandlerWithInput`. */ -export type RequestHandler = RequestHandlerWithInput< - void, - A, - E, - R, - Request, - Id -> - -export interface RequestStreamHandlerWithInput { - handler: (i: I) => Stream.Stream - id: Id - options?: ClientForOptions - Request: Request - /** - * Phantom type property (never set at runtime) that carries the `Final` type to - * `StreamMutationWithExtensions`. The tilde prefix follows the Effect convention for - * phantom/virtual properties and prevents accidental runtime access. - * Stream failures bubble through the execute effect's typed error channel `E`; - * the reactive `AsyncResult` ref also mirrors the failure for live progress UI. - */ - readonly "~final"?: Final -} - -/** Type alias: a no-input stream handler is simply `RequestStreamHandlerWithInput`. */ -export type RequestStreamHandler = - RequestStreamHandlerWithInput - -// make sure this is exported or d.ts of apiClientFactory breaks?! -export type RequestInputFromMake any }> = Parameters extends - [] ? void : Parameters[0] - -// Has no input only when the request schema declares no payload fields (the auto-added -// `_tag` field is ignored). Any payload fields (even all-optional) produce a function handler. -type HasNoFields = I extends { readonly fields: infer F extends S.Struct.Fields } - ? [Exclude] extends [never] ? true : false - : false - -type RequestInput any }> = Parameters[0] - -/** - * Caller-facing input type for a request. `void` when the request schema has no fields; - * otherwise `make`'s first param type. - */ -export type HandlerInput any }> = HasNoFields extends true ? void - : RequestInput - -/** Extracts the final-value type from a stream request. Defaults to the success type when no `final` schema is set. */ -type FinalTypeOf = T extends { readonly final: infer F extends S.Top } ? F["Type"] - : T["success"]["Type"] - -// `T["success"]` / `T["error"]` are constrained to `S.Top` via `Req`, so we -// can read `["DecodingServices"]` directly. Avoids the conditional in -// `S.Codec.DecodingServices = X extends Top ? X["DecodingServices"] : never`, -// which tsgo (native) fails to reduce in this generic position and leaves as -// `unknown`, polluting the `R` channel of every client handler. -type RequestHandlerFor = T["stream"] extends true - ? RequestStreamHandlerWithInput< - HandlerInput, - T["success"]["Type"], - T["error"]["Type"] | E, - R | T["success"]["DecodingServices"] | T["error"]["DecodingServices"], - T, - Id, - FinalTypeOf - > - : RequestHandlerWithInput< - HandlerInput, - T["success"]["Type"], - T["error"]["Type"] | E, - R | T["success"]["DecodingServices"] | T["error"]["DecodingServices"], - T, - Id - > - -export type RequestHandlers = { - [K in keyof M as M[K] extends Req ? K : never]: Extract extends infer T extends Req - ? RequestHandlerFor - : never -} diff --git a/packages/effect-app/src/client/errors.ts b/packages/effect-app/src/client/errors.ts deleted file mode 100644 index 988b45f4d..000000000 --- a/packages/effect-app/src/client/errors.ts +++ /dev/null @@ -1,310 +0,0 @@ -/** @effect-diagnostics overriddenSchemaConstructor:skip-file */ -import { TaggedErrorClass } from "effect-app/Schema" -import * as Cause from "effect/Cause" -import * as S from "../Schema.ts" - -export const tryToJson = (error: unknown) => { - try { - const errorJson = (error as any).toJSON() - return errorJson && typeof errorJson === "object" ? errorJson as Record : { error } - } catch { - try { - return { error: (error as any).toString() } - } catch (err) { - try { - return { error: `Failed to convert error: ${err}` } - } catch { - return { error: `Failed to convert error: unknown failure` } - } - } - } -} - -// eslint-disable-next-line unused-imports/no-unused-vars -// @ts-expect-error type not used -export class NotFoundError extends TaggedErrorClass>()("NotFoundError", { - type: S.String, - id: S.Unknown -}) { - constructor( - props: { type: string; id: unknown; cause?: unknown }, - disableValidation?: boolean - ) { - super(props, disableValidation as any) - } - override get message() { - return `Didn't find ${(this as any).type}#${JSON.stringify((this as any).id)}` - } - override toString() { - return `NotFoundError: ${this.message}` - } -} - -const messageFallback = (messageOrObject?: string | { message: string }) => - typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject ?? "" } - -export class InvalidStateError extends TaggedErrorClass()("InvalidStateError", { - message: S.String -}) { - constructor(messageOrObject: string | { message: string; cause?: unknown }, disableValidation?: boolean) { - super( - typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject }, - disableValidation as any - ) - } - override toString() { - return `InvalidStateError: ${this.message}` - } -} - -export class ServiceUnavailableError extends TaggedErrorClass()("ServiceUnavailableError", { - message: S.String -}) { - constructor(messageOrObject: string | { message: string; cause?: unknown }, disableValidation?: boolean) { - super( - typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject }, - disableValidation as any - ) - } - override toString() { - return `ServiceUnavailableError: ${this.message}` - } -} - -export class ValidationError extends TaggedErrorClass()("ValidationError", { - errors: S.Array(S.Unknown) -}) { - constructor( - props: { errors: ReadonlyArray; cause?: unknown }, - disableValidation?: boolean - ) { - super(props, disableValidation as any) - } - override get message() { - return `Validation failed: ${(this as any).errors.map((e: any) => JSON.stringify(e, undefined, 2)).join(",\n")}` - } - override toString() { - return `ValidationError: ${this.message}` - } -} - -export class NotLoggedInError extends TaggedErrorClass()("NotLoggedInError", { - message: S.String -}) { - constructor(messageOrObject?: string | { message: string; cause?: unknown }, disableValidation?: boolean) { - super(messageFallback(messageOrObject), disableValidation as any) - } - override toString() { - return `NotLoggedInError: ${this.message}` - } -} - -/** - * The user carries a valid Userprofile, but there is a problem with the login none the less. - */ -export class LoginError extends TaggedErrorClass()("NotLoggedInError", { - message: S.String -}) { - constructor(messageOrObject?: string | { message: string; cause?: unknown }, disableValidation?: boolean) { - super(messageFallback(messageOrObject), disableValidation as any) - } - override toString() { - return `LoginError: ${this.message}` - } -} - -export class UnauthorizedError extends TaggedErrorClass()("UnauthorizedError", { - message: S.String -}) { - constructor(messageOrObject?: string | { message: string; cause?: unknown }, disableValidation?: boolean) { - super(messageFallback(messageOrObject), disableValidation as any) - } - override toString() { - return `UnauthorizedError: ${this.message}` - } -} - -type OptimisticConcurrencyDetails = { - readonly type: string - readonly id: string - readonly code: number - readonly current?: string | undefined - readonly found?: string | undefined -} - -export class OptimisticConcurrencyException extends TaggedErrorClass()( - "OptimisticConcurrencyException", - { message: S.String } -) { - readonly details?: OptimisticConcurrencyDetails - readonly raw?: unknown - constructor( - args: - | OptimisticConcurrencyDetails - | ({ message: string; cause?: unknown; raw?: unknown }), - disableValidation?: boolean - ) { - super( - "message" in args ? args : { message: `Existing ${args.type} ${args.id} record changed` }, - disableValidation as any - ) - if (!("message" in args)) { - this.details = args - } - } - override toString() { - return `OptimisticConcurrencyException: ${this.message}` - } -} - -/** - * Raised by a store adapter when a database operation fails for an - * infrastructure reason (request timeout, throttle, 5xx, dropped socket, or any - * non-conflict error). Distinct from `OptimisticConcurrencyException`, which is - * an expected etag conflict. - * - * `transient` is set by the adapter for failures worth retrying (timeout / - * throttle / 5xx / network). `cause` is the underlying adapter error, carried - * through a `Defect` schema so it serializes (an `Error` encodes to - * `{ name, message, cause }`) instead of breaking JSON encoding of the channel. - * - * Treated by the api/client/FE machinery like an unexpected (500-class) error. - */ -export class DatabaseError extends TaggedErrorClass()( - "DatabaseError", - { - message: S.String, - transient: S.Boolean, - cause: S.optional(S.Defect()) - } -) { - constructor( - args: { message?: string; transient?: boolean; cause?: unknown }, - disableValidation?: boolean - ) { - super( - { - message: args.message ?? "Database operation failed", - transient: args.transient ?? false, - cause: args.cause - }, - disableValidation as any - ) - } - override toString() { - return `DatabaseError: ${this.message}` - } -} - -const MutationOnlyErrors = [ - InvalidStateError, - OptimisticConcurrencyException -] as const - -const GeneralErrors = [ - NotFoundError, - NotLoggedInError, - LoginError, - UnauthorizedError, - ValidationError, - ServiceUnavailableError, - DatabaseError -] as const - -export const SupportedErrors = S.Union([ - ...MutationOnlyErrors, - ...GeneralErrors -]) -// .pipe(named("SupportedErrors")) -// .pipe(withDefaultMake) -export type SupportedErrors = S.Schema.Type - -// ideal? -// export const QueryErrors = union({ ...GeneralErrors }) -// .pipe(named("QueryErrors")) -// .pipe(withDefaultMake) -// export type QueryErrors = Schema.Type -// export const MutationErrors = union({ ...GeneralErrors, ...GeneralErrors }) -// .pipe(named("MutationErrors")) -// .pipe(withDefaultMake) - -// export type MutationErrors = Schema.Type - -export const MutationErrors = SupportedErrors -export const QueryErrors = SupportedErrors -export type MutationErrors = S.Schema.Type -export type QueryErrors = S.Schema.Type - -export const ErrorSilenced = Symbol.for("effect-app/error-silenced") -export const isErrorSilenced = (e: unknown): boolean => - typeof e === "object" && e !== null && ErrorSilenced in e ? !!e[ErrorSilenced] : false -export const silenceError = (e: Record) => { - e[ErrorSilenced] = true -} - -export class CauseException extends Error { - readonly originalCause: Cause.Cause - readonly _tag: string - - constructor(originalCause: Cause.Cause, _tag: string) { - const limit = Error.stackTraceLimit - Error.stackTraceLimit = 0 - super() - Error.stackTraceLimit = limit - this.originalCause = originalCause - this._tag = _tag - this.cause = Cause.squash(originalCause) - // v4: makeFiberFailure removed — use Cause.prettyErrors instead - const errors = Cause.prettyErrors(originalCause) - const first = errors[0] - if (first) { - this.name = first.name - this.message = first.message - if (first.stack) { - this.stack = first.stack - } - } else { - this.name = "CauseException" - this.message = Cause.pretty(originalCause) - } - } - toReport() { - return { - _tag: this._tag, - name: this.name, - message: this.message - } - } - - toJSON() { - return { - _tag: this._tag, - name: this.name, - message: this.message, - originalCause: this.originalCause - } - } - - [Symbol.for("nodejs.util.inspect.custom")]() { - return this.toJSON() - } - override toString() { - return `[${this._tag}] ` + Cause.pretty(this.originalCause) - } -} - -export const tryToReport = (error: { toReport(): Record; toString(): string }) => { - try { - return error.toReport() - } catch { - try { - return { error: error.toString() } - } catch (err) { - try { - return { error: `Failed to convert error: ${err}` } - } catch { - return { error: `Failed to convert error: unknown failure` } - } - } - } -} diff --git a/packages/effect-app/src/client/makeClient.ts b/packages/effect-app/src/client/makeClient.ts deleted file mode 100644 index c1785f084..000000000 --- a/packages/effect-app/src/client/makeClient.ts +++ /dev/null @@ -1,629 +0,0 @@ -import type * as Exit from "effect/Exit" -import { type GetContextConfig, type RpcContextMap } from "../rpc/RpcContextMap.ts" -import * as S from "../Schema.ts" -import { AST } from "../Schema.ts" -import type { ClientForOptions } from "./clientFor.ts" - -/** - * Minimal structural shape for an rpc-client middleware tag. - * Captures only what `makeRpcClient` and routing/client factory consume from it - * (no `Default` layer required — that is provided to `makeRouter`). - */ -export interface ClientMiddleware> { - readonly requestContextMap: RequestContextMap - readonly requestContext: unknown - readonly provides?: unknown - readonly requires?: unknown -} - -const merge = (a: any, b: Array) => - a !== undefined && b.length ? S.Union([a, ...b]) : a !== undefined ? a : b.length ? S.Union(b) : S.Never - -type SchemaOrFields = T extends S.Top ? T : T extends S.Struct.Fields ? S.Struct : S.Void - -type TaggedRequestSchema = S.Struct< - { readonly _tag: S.tag } & Payload -> - -type QueryOnlyRequests = { - [K in keyof Resource as Resource[K] extends { readonly type: "query" } ? K : never]: Resource[K] -} - -type QueryOnlyResources = { - [K in keyof Resources]: QueryOnlyRequests -} - -type InputFromPayload = keyof Payload extends never ? void - : S.Struct["Type"] - -type OutputFromSuccess = Success extends typeof S.Void ? void : Success["Type"] - -type InvalidationResources = Record> - -/** - * An invalidation instruction returned from `invalidatesQueries`. One of: - * - a raw query key (`ReadonlyArray`) - * - an RPC handler object (`{ id, options? }`) — query key derived from `id` - * - a compatibility `{ filters: { queryKey }, options? }` shape - * - * Predicate filters are intentionally not represented: atom invalidation is keyed by - * concrete query keys. - */ -export type InvalidateQueryInstruction< - Filters = { readonly queryKey: ReadonlyArray }, - Options = Record -> = - | ReadonlyArray - | { - readonly id: string - readonly options?: ClientForOptions - } - | { - readonly filters: Filters - readonly options?: Options - } - -export type InvalidationCallback = ( - queryKey: readonly string[], - resources: QueryOnlyResources, - ...args: [Input] extends [void] ? [exit: Exit.Exit] - : [input: Input, exit: Exit.Exit] -) => ReadonlyArray - -export type InvalidationConfig = { - readonly invalidatesQueries: InvalidationCallback - readonly invalidationResources?: Resources - readonly disableQueryInvalidation?: boolean -} - -type InvalidationConfigForCommand< - Resources, - Payload extends S.Struct.Fields, - Success extends S.Top, - Error extends S.Top -> = InvalidationConfig< - Resources, - InputFromPayload, - OutputFromSuccess, - Error["Type"] -> - -export const configureInvalidation = () => -( - invalidatesQueries: InvalidationCallback, NoInfer, NoInfer> -): InvalidationConfig => ({ invalidatesQueries }) - -export const configureInvalidationCallback = () => -( - invalidatesQueries: InvalidationCallback, NoInfer, NoInfer> -): InvalidationCallback => invalidatesQueries - -export const configureInvalidationResources = () => - ({}) as Pick, "invalidationResources"> - -type TaggedRequestForResult< - Self, - Tag extends string, - Payload extends S.Struct.Fields, - Success extends S.Top, - Error extends S.Top, - Config, - ModuleName extends string, - Type extends "command" | "query", - Stream extends boolean, - Resources = never, - Final extends S.Top = never, - Middleware = unknown -> = - & S.Opaque, {}> - & { - readonly fields: TaggedRequestSchema["fields"] - readonly _tag: Tag - readonly success: Success - readonly error: Error - readonly config: Config - readonly id: `${ModuleName}.${Tag}` - readonly moduleName: ModuleName - readonly type: Type - readonly stream: Stream - readonly middleware?: Middleware - readonly "~invalidationResources"?: Resources - } - & ([Final] extends [never] ? {} : { readonly final: Final }) - -export const makeRpcClient = < - Middleware extends ClientMiddleware>, - GeneralErrors extends S.Top = never ->(middleware: Middleware, generalErrors?: GeneralErrors) => { - // Long way around Context/C extends etc to support actual jsdoc from passed in RequestConfig etc... (??) - type ServiceMap = { - success: S.Top | S.Struct.Fields // SchemaOrFields will make a Schema type out of Struct.Fields - error: S.Top | S.Struct.Fields // SchemaOrFields will make a Schema type out of Struct.Fields - final?: S.Top | S.Struct.Fields // optional final-value schema for stream requests - stream?: boolean // request metadata — stripped from stored config - } - - type RequestConfig = GetContextConfig - - // Errors raised by RPC middleware (e.g. `NotLoggedInError` from auth) used to - // be merged into `resource.error` here so they would surface in the wire - // failure schema. That merging is gone — the canonical source for middleware - // errors is now the middleware tag attached to the rpc on both server and - // client (see `ApiClientFactory.makeFor({ middleware })`). `Rpc.exitSchema` - // unions in `rpc.middlewares[*].error` automatically. - type MergeError = [GeneralErrors] extends [never] ? SchemaOrFields : S.Union<[SchemaOrFields, GeneralErrors]> - type ErrorResult = C extends { error: infer E } ? MergeError - : [GeneralErrors] extends [never] ? typeof S.Never - : GeneralErrors - - function makeRequestClass>( - tag: Tag, - fields: Fields, - config?: C - ) { - const failureSchema = merge( - config?.error ? S.isSchema(config.error) ? config.error : S.Struct(config.error) : undefined, - [generalErrors].filter(Boolean) - ) - const successSchema = config?.success - ? S.isSchema(config.success) - ? AST.isVoid(config.success.ast) ? S.Void : config.success - : S.Struct(config.success) - : S.Void - - const finalConfig = (config as any)?.final - const finalSchema = finalConfig && S.isSchema(finalConfig) ? finalConfig : undefined - - // Strip stream from the stored config — it's request metadata, not handler config - const { stream: _stream, ...restConfig } = config ?? ({} as C) - - const RequestClass = S.Opaque()(S.TaggedStruct(tag, fields)) - Object.assign(RequestClass, { - _tag: tag, - success: successSchema, - error: failureSchema, - ...(finalSchema !== undefined ? { final: finalSchema } : {}), - config: restConfig - }) - - return RequestClass - } - - function makeTaggedRequestWithMeta< - ModuleName extends string, - Type extends "command" | "query" - >( - moduleName: ModuleName, - type: Type - ) { - function TaggedRequestWithMeta(): { - // ─── stream: true overloads (must come before non-stream so TypeScript picks them) ─────────── - < - Tag extends string, - Payload extends S.Struct.Fields, - Success extends S.Top | S.Struct.Fields, - Error extends S.Top | S.Struct.Fields, - Final extends S.Top | S.Struct.Fields = never, - C extends RequestConfig & Record = RequestConfig & Record - >( - tag: Tag, - fields: Payload, - config: - & Omit - & { stream: true; success: Success; error: Error; final?: Final }, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - OutputFromSuccess>, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - SchemaOrFields, - ErrorResult, - Omit< - & Omit - & { - success: Success - error: Error - } - & Partial< - InvalidationConfigForCommand< - Resources, - Payload, - SchemaOrFields, - ErrorResult - > - >, - "success" | "error" | "stream" - >, - ModuleName, - Type, - true, - Resources, - [Final] extends [never] ? never : SchemaOrFields, - Middleware - > - < - Tag extends string, - Payload extends S.Struct.Fields, - Success extends S.Top | S.Struct.Fields, - Final extends S.Top | S.Struct.Fields = never, - C extends RequestConfig & Record & { error?: never } = - & RequestConfig - & Record - & { error?: never } - >( - tag: Tag, - fields: Payload, - config: - & Omit - & { stream: true; success: Success; final?: Final }, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - OutputFromSuccess>, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - SchemaOrFields, - ErrorResult, - Omit< - & Omit - & { success: Success } - & Partial< - InvalidationConfigForCommand< - Resources, - Payload, - SchemaOrFields, - ErrorResult - > - >, - "success" | "error" | "stream" - >, - ModuleName, - Type, - true, - Resources, - [Final] extends [never] ? never : SchemaOrFields, - Middleware - > - // ─── stream: true without `success` overloads ──────────────────────────────────────────────── - < - Tag extends string, - Payload extends S.Struct.Fields, - Error extends S.Top | S.Struct.Fields, - C extends RequestConfig & Record & { success?: never } = - & RequestConfig - & Record - & { success?: never } - >( - tag: Tag, - fields: Payload, - config: - & Omit - & { stream: true; error: Error }, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - void, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - typeof S.Void, - ErrorResult, - Omit< - & Omit - & { error: Error } - & Partial< - InvalidationConfigForCommand< - Resources, - Payload, - typeof S.Void, - ErrorResult - > - >, - "success" | "error" | "stream" - >, - ModuleName, - Type, - true, - Resources, - never, - Middleware - > - < - Tag extends string, - Payload extends S.Struct.Fields, - C extends RequestConfig & Record & { success?: never; error?: never } = - & RequestConfig - & Record - & { success?: never; error?: never } - >( - tag: Tag, - fields: Payload, - config: - & Omit - & { stream: true }, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - void, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - typeof S.Void, - ErrorResult, - Omit< - & Omit - & Partial>>, - "success" | "error" | "stream" - >, - ModuleName, - Type, - true, - Resources, - never, - Middleware - > - // ─── non-stream overloads ──────────────────────────────────────────────────────────────────── - < - Tag extends string, - Payload extends S.Struct.Fields, - Success extends S.Top | S.Struct.Fields, - Error extends S.Top | S.Struct.Fields, - Final extends S.Top | S.Struct.Fields = never, - C extends RequestConfig & Record = RequestConfig & Record - >( - tag: Tag, - fields: Payload, - config: - & Omit - & { success: Success; error: Error; final?: Final }, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - OutputFromSuccess>, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - SchemaOrFields, - ErrorResult, - Omit< - & Omit - & { - success: Success - error: Error - } - & Partial< - InvalidationConfigForCommand< - Resources, - Payload, - SchemaOrFields, - ErrorResult - > - >, - "success" | "error" | "stream" - >, - ModuleName, - Type, - false, - Resources, - [Final] extends [never] ? never : SchemaOrFields, - Middleware - > - < - Tag extends string, - Payload extends S.Struct.Fields, - Success extends S.Top | S.Struct.Fields, - Final extends S.Top | S.Struct.Fields = never, - C extends RequestConfig & Record & { error?: never } = - & RequestConfig - & Record - & { - error?: never - } - >( - tag: Tag, - fields: Payload, - config: - & Omit - & { success: Success; final?: Final }, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - OutputFromSuccess>, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - SchemaOrFields, - ErrorResult, - Omit< - & Omit - & { - success: Success - } - & Partial< - InvalidationConfigForCommand< - Resources, - Payload, - SchemaOrFields, - ErrorResult - > - >, - "success" | "error" | "stream" - >, - ModuleName, - Type, - false, - Resources, - [Final] extends [never] ? never : SchemaOrFields, - Middleware - > - < - Tag extends string, - Payload extends S.Struct.Fields, - Error extends S.Top | S.Struct.Fields, - C extends RequestConfig & Record & { success?: never } - >( - tag: Tag, - fields: Payload, - config: - & Omit - & { error: Error }, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - void, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - typeof S.Void, - ErrorResult, - Omit< - & Omit - & { - error: Error - } - & Partial< - InvalidationConfigForCommand< - Resources, - Payload, - typeof S.Void, - ErrorResult - > - >, - "success" | "error" | "stream" - >, - ModuleName, - Type, - false, - Resources, - never, - Middleware - > - < - Tag extends string, - Payload extends S.Struct.Fields, - C extends RequestConfig & Record & { success?: never; error?: never } - >( - tag: Tag, - fields: Payload, - config: Omit, - invalidatesQueries?: InvalidationCallback< - Resources, - InputFromPayload, - void, - ErrorResult["Type"] - > - ): TaggedRequestForResult< - Self, - Tag, - Payload, - typeof S.Void, - ErrorResult, - Omit< - & Omit - & Partial>>, - "success" | "error" | "stream" - >, - ModuleName, - Type, - false, - Resources, - never, - Middleware - > - ( - tag: Tag, - fields: Payload - ): TaggedRequestForResult< - Self, - Tag, - Payload, - typeof S.Void, - ErrorResult<{}>, - // Empty config, NOT `Record`: the config type flows into - // `GetEffectContext`/`GetEffectError` as `T`, and `keyof Record` - // is `string` — so an inverted key like `allowAnonymous` would satisfy - // `key extends keyof T`, then `T[key]` (`never`) `extends true` (true, since - // `never` is assignable to everything) and the middleware wrongly takes the - // "allowAnonymous: true" (anonymous) branch, dropping `UserProfile`. `{}` has - // `keyof {} = never`, so no config key is treated as set and the defaults - // (auth required, `UserProfile` provided) apply — same as passing `{}` explicitly. - {}, - ModuleName, - Type, - false, - never, - never, - Middleware - > - } { - return (( - tag: Tag, - fields: Fields, - config?: C, - invalidatesQueries?: InvalidationCallback - ) => { - const isStream = (config as any)?.stream === true - const requestConfig = invalidatesQueries === undefined ? config : { ...config, invalidatesQueries } - const cls = makeRequestClass(tag, fields, requestConfig) - Object.assign(cls, { - id: `${moduleName}.${tag}`, - moduleName, - type, - stream: isStream, - middleware - }) - return cls - }) as any - } - return Object.assign(TaggedRequestWithMeta, { moduleName, type } as const) - } - - function TaggedRequestFor(moduleName: ModuleName) { - const Query = makeTaggedRequestWithMeta(moduleName, "query") - const Command = makeTaggedRequestWithMeta(moduleName, "command") - - return { - moduleName, - /** - * Create query request classes for this module. - * Queries read state and should not mutate server state. - * Pass `stream: true` in the config to produce a Stream of `success` values (QueryStream behaviour). - */ - Query, - /** - * Create command request classes for this module. - * Commands mutate state and should avoid returning complex read models. - * Pass `stream: true` in the config to produce a Stream of `success` values (CommandStream behaviour). - */ - Command - } as const - } - - return { - TaggedRequestFor - } -} diff --git a/packages/effect-app/src/faker.ts b/packages/effect-app/src/faker.ts deleted file mode 100644 index 601683305..000000000 --- a/packages/effect-app/src/faker.ts +++ /dev/null @@ -1,32 +0,0 @@ -// FILE HAS SIDE EFFECTS! -import type { Faker } from "@faker-js/faker" -import * as FC from "fast-check" - -// TODO: inject faker differently, so we dont care about multiple instances of library. - -// eslint-disable-next-line prefer-const -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let faker: Faker = undefined as any as Faker -export function setFaker(f: Faker) { - faker = f -} - -export function getFaker() { - if (!faker) throw new Error("You forgot to load faker library") - return faker -} - -export const fakerToArb = (fakerGen: () => T) => (fc: typeof FC) => { - return FC.noBias(FC.noShrink( - fc - .integer() - .map((seed) => { - faker.seed(seed) // seed the generator - return fakerGen() // call it - }) - )) -} - -export const fakerArb = ( - gen: (fake: Faker) => () => T -): (a: any) => FC.Arbitrary => fakerToArb(() => gen(getFaker())()) diff --git a/packages/effect-app/src/http.ts b/packages/effect-app/src/http.ts deleted file mode 100644 index 51bad709f..000000000 --- a/packages/effect-app/src/http.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./http/internal/lib.ts" diff --git a/packages/effect-app/src/http/Request.ts b/packages/effect-app/src/http/Request.ts deleted file mode 100644 index c42206ee1..000000000 --- a/packages/effect-app/src/http/Request.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { HttpClientResponse } from "effect/http/HttpClientResponse" -import * as Option from "effect/Option" -import * as Effect from "../Effect.ts" -import { HttpClient, HttpClientError, HttpClientRequest, HttpHeaders } from "./internal/lib.ts" - -export interface ResponseWithBody extends Pick { - readonly body: A -} - -// TODO: consider rebuilding the text/json helpers to use a cached effect -// https://discord.com/channels/795981131316985866/1098177242598756412/1168565257569046712 - -export const responseWithJsonBody = ( - response: HttpClientResponse -) => - Effect.map(response.json, (body): ResponseWithBody => ({ - body, - headers: response.headers, - status: response.status, - remoteAddress: response.remoteAddress - })) - -export const demandJson = (client: HttpClient.HttpClient) => - HttpClient - .mapRequest(client, (_) => HttpClientRequest.acceptJson(_)) - .pipe(HttpClient.transform((r, request) => - Effect.tap(r, (response) => - Option - .exists( - HttpHeaders.get(response.headers, "Content-Type"), - (_) => _.startsWith("application/json") - ) - ? Effect.void - : Effect.fail( - new HttpClientError.DecodeError({ - request, - response, - description: "not json response: " - + Option.getOrElse(HttpHeaders.get(response.headers, "Content-Type"), () => "") - }) - )) - )) diff --git a/packages/effect-app/src/http/internal/lib.ts b/packages/effect-app/src/http/internal/lib.ts deleted file mode 100644 index 6c035c281..000000000 --- a/packages/effect-app/src/http/internal/lib.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * as HttpHeaders from "effect/http/Headers" -export * as HttpBody from "effect/http/HttpBody" -export * as HttpClient from "effect/http/HttpClient" -export * as HttpClientError from "effect/http/HttpClientError" -export * as HttpClientRequest from "effect/http/HttpClientRequest" -export * as HttpClientResponse from "effect/http/HttpClientResponse" -// HttpLayerRouter removed in v4 — use HttpRouter instead -export * as HttpMiddleware from "effect/http/HttpMiddleware" -export * as HttpRouter from "effect/http/HttpRouter" -export * as HttpServer from "effect/http/HttpServer" -export * as HttpServerError from "effect/http/HttpServerError" -export * as HttpServerRequest from "effect/http/HttpServerRequest" -export * as HttpServerResponse from "effect/http/HttpServerResponse" diff --git a/packages/effect-app/src/ids.ts b/packages/effect-app/src/ids.ts deleted file mode 100644 index 09dc25f4b..000000000 --- a/packages/effect-app/src/ids.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { brandedStringId, type Codec, NonEmptyString255, StringId, type StringIdBrand, withDefaultMake } from "effect-app/Schema" -import type { B } from "effect-app/Schema/schema" -import * as Effect from "effect/Effect" -import type { Simplify } from "effect/Types" -import * as S from "./Schema.ts" -import { extendM } from "./utils.ts" - -export interface RequestIdBrand extends StringIdBrand { - readonly RequestId: unique symbol -} - -export type RequestId = NonEmptyString255 -/** - * Schema for a request id. - * - * A request id may be made from a span id, which does not comply with the - * `StringId` schema (hence the looser `NonEmptyString255` base). - * - * `.withConstructorDefault` => fresh `StringId` (construction-only; not - * applied during decode — cannot be used to JIT-migrate database fields). - * See `./Schema/ext.ts` for the full policy note. - */ -const requestIdSamples = S.makeFilter(() => undefined, { - arbitraryConstraint: { - minLength: 21, - maxLength: 21, - patterns: [{ source: "^[A-Za-z0-9_-]{21}$", flags: "" }] - } -}) - -export const RequestId = extendM( - Object - .assign( - Object.create(NonEmptyString255) as {}, - NonEmptyString255.pipe(S.check(requestIdSamples)) as unknown as Codec - ), - (s) => { - function make(): NonEmptyString255 - function make(input: string, options?: S.MakeOptions): NonEmptyString255 - function make(input?: string, options?: S.MakeOptions): NonEmptyString255 { - const id = input === undefined ? StringId.make() : StringId.make(input, options) - return id - } - return ({ - make, - /** - * Construction-only default: fresh `StringId`. Applied only when the - * field is omitted from `.make(...)` input. NOT applied during decode — - * cannot be used to JIT-migrate database fields. See `./Schema/ext.ts` - * file-level note. - */ - withConstructorDefault: S.withConstructorDefault(Effect.sync(make))(s as typeof s & S.WithoutConstructorDefault) - }) - } -) - .pipe(withDefaultMake) - -export interface UserProfileIdBrand extends Simplify & StringIdBrand> {} -export type UserProfileId = string & UserProfileIdBrand -/** - * Branded `StringId` for user profiles. - * - * Exposes `.withConstructorDefault` (fresh id) — construction-only; not - * applied during decode. See `./Schema/ext.ts` for the full policy note. - */ -export const UserProfileId = brandedStringId() diff --git a/packages/effect-app/src/index.ts b/packages/effect-app/src/index.ts deleted file mode 100644 index 5f15b05be..000000000 --- a/packages/effect-app/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -// eslint-disable-next-line import/no-unassigned-import -import "./builtin.ts" - -export * as Fnc from "./Function.ts" -export * as Utils from "./utils.ts" - -export * as Array from "./Array.ts" -export * as Config from "./Config.ts" -export * as ConfigProvider from "./ConfigProvider.ts" -export * as Context from "./Context.ts" -export * as Effect from "./Effect.ts" -export * as Layer from "./Layer.ts" -export * as Model from "./Model.ts" -export * as NonEmptySet from "./NonEmptySet.ts" -export * as Set from "./Set.ts" -export * as Store from "./Store.ts" - -export { type NonEmptyArray, type NonEmptyReadonlyArray } from "./Array.ts" - -export * from "effect" - -export type * as Types from "./Types.ts" - -export * as SecretURL from "./Config/SecretURL.ts" -export * as RpcX from "./rpc.ts" -export * as S from "./Schema.ts" -export { copy } from "./utils.ts" diff --git a/packages/effect-app/src/logger.ts b/packages/effect-app/src/logger.ts deleted file mode 100644 index 3b1c9822b..000000000 --- a/packages/effect-app/src/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { makeLog } from "effect-app/utils/logger" - -export const PreludeLogger = makeLog("effect-app", "info") diff --git a/packages/effect-app/src/middleware.ts b/packages/effect-app/src/middleware.ts deleted file mode 100644 index 4cb4937af..000000000 --- a/packages/effect-app/src/middleware.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as Context from "effect-app/Context" -import { RpcMiddleware } from "./rpc.ts" - -export class DevMode extends Context.Reference("DevMode", { defaultValue: () => false }) {} - -export class RequestCacheMiddleware extends RpcMiddleware.Tag()("RequestCacheMiddleware") {} - -export class ConfigureInterruptibilityMiddleware - extends RpcMiddleware.Tag()("ConfigureInterruptibilityMiddleware") -{} - -export class LoggerMiddleware extends RpcMiddleware.Tag()("LoggerMiddleware") {} - -export class DevModeMiddleware extends RpcMiddleware.Tag()("DevModeMiddleware") {} - -/** - * Generic middlewares attached by `makeRouter` to every request. - * - * Invalidation key wrap/unwrap is handled by the routing layer (server) and the - * api client factory (client) directly — there is no middleware tag for it. - */ -export const DefaultGenericMiddlewares = [ - RequestCacheMiddleware, - ConfigureInterruptibilityMiddleware, - LoggerMiddleware, - DevModeMiddleware -] as const diff --git a/packages/effect-app/src/migration/nativeEffect.ts b/packages/effect-app/src/migration/nativeEffect.ts deleted file mode 100644 index 59dc42e43..000000000 --- a/packages/effect-app/src/migration/nativeEffect.ts +++ /dev/null @@ -1,645 +0,0 @@ -/** - * Rewrites source that imported effect-app helpers now provided by Effect. - * - * The rewrite is textual and keeps the rest of the file. It understands named - * imports, aliases, and `import * as` namespaces from the effect-app entry points - * listed in {@link nativeEffectReplacements}. - */ - -export interface NativeEffectReplacement { - readonly module: string - readonly name: string - /** Expression that replaces a reference to `name`. */ - readonly expression: string - readonly note: string - readonly imports: ReadonlyArray - /** When set, a call `name(...args)` is rewritten instead of the bare identifier. */ - readonly call?: "date-add" | "date-sub" | "chunk" -} - -const struct = [["Struct", "effect/Struct"]] as const -const record = [["Record", "effect/Record"]] as const -const str = [["String", "effect/String"]] as const -const option = [["Option", "effect/Option"]] as const -const array = [["Array", "effect/Array"]] as const -const chunk = [["Chunk", "effect/Chunk"]] as const -const tuple = [["Tuple", "effect/Tuple"]] as const -const effect = [["Effect", "effect/Effect"]] as const -const dateTime = [["DateTime", "effect/DateTime"], ["pipeZonedLocal", "effect-app/DateTime"]] as const -const deferred = [["Deferred", "effect/Deferred"]] as const -const result = [["Result", "effect/Result"]] as const -const absurd = [["absurd", "effect/Function"]] as const - -export const nativeEffectReplacements: ReadonlyArray = [ - { - module: "effect-app/utils", - name: "typedKeysOf", - expression: "Struct.keys", - imports: struct, - note: "Struct.keys returns `(keyof T & string)[]`." - }, - { - module: "effect-app/utils", - name: "typedValuesOf", - expression: "Record.values", - imports: record, - note: - "Record.values reads a record with one value type. When each value's type depends on its key, use Object.values." - }, - { - module: "effect-app/utils", - name: "capitalize", - expression: "String.capitalize", - imports: str, - note: "Same first-character capitalization, including the empty string." - }, - { - module: "effect-app/utils", - name: "uncapitalize", - expression: "String.uncapitalize", - imports: str, - note: "Same first-character uncapitalization, including the empty string." - }, - { - module: "effect-app/utils", - name: "assertUnreachable", - expression: "absurd", - imports: absurd, - note: "absurd throws when an impossible value is reached. The error text no longer includes the value." - }, - { - module: "effect-app/Function", - name: "tuple", - expression: "Tuple.make", - imports: tuple, - note: "Tuple.make returns a mutable tuple. It is assignable wherever a readonly tuple was expected." - }, - { - module: "effect-app/Array", - name: "toNonEmptyArray", - expression: "Option.liftPredicate(Array.isReadonlyArrayNonEmpty)", - imports: [...option, ...array], - note: "Option of the same readonly array when it is non-empty." - }, - { - module: "effect-app/Array", - name: "NEROArrayFromArray", - expression: "Option.liftPredicate(Array.isReadonlyArrayNonEmpty)", - imports: [...option, ...array], - note: "Same as toNonEmptyArray." - }, - { - module: "effect-app/Array", - name: "NEAFromArray", - expression: "Option.liftPredicate(Array.isArrayNonEmpty)", - imports: [...option, ...array], - note: "Option of the same mutable array when it is non-empty." - }, - { - module: "effect-app/Array", - name: "chunk_", - expression: "Array.chunksOf", - imports: [...array, ...chunk], - call: "chunk", - note: "Was a Chunk of mutable slices. Now a Chunk of Array.chunksOf groups. An empty input is an empty Chunk." - }, - { - module: "effect-app/Array", - name: "_chunk_", - expression: "Array.chunksOf", - imports: array, - note: "The generator form is gone. Array.chunksOf returns the groups directly." - }, - { - module: "effect-app/Option", - name: "toBool", - expression: "Option.isSome", - imports: option, - note: "isSome is the boolean test and also narrows to Some." - }, - { - module: "effect-app/Effect", - name: "annotateLogscoped", - expression: "Effect.annotateLogsScoped", - imports: effect, - note: "The old name was a typo of Effect.annotateLogsScoped." - }, - { - module: "effect-app/Effect", - name: "await_", - expression: "Deferred.await", - imports: deferred, - note: "Await a Deferred. Effect.await_ was Deferred.await." - }, - { - module: "effect-app/Chunk", - name: "findFirstSimple", - expression: "Chunk.findFirst", - imports: chunk, - note: "The alias passed the chunk first. Chunk.findFirst is dual, so the data-first call still typechecks." - }, - { - module: "effect-app/Chunk", - name: "findLastSimple", - expression: "Chunk.findLast", - imports: chunk, - note: "Alias of Chunk.findLast." - }, - { - module: "effect-app/Chunk", - name: "ChunkPartition", - expression: "Chunk.partition", - imports: chunk, - note: "Alias of Chunk.partition." - }, - { - module: "effect-app/Chunk", - name: "uniq", - expression: - "(equivalence) => (self) => Chunk.fromIterable(Array.dedupeWith(Chunk.toReadonlyArray(self), equivalence))", - imports: [...chunk, ...array], - note: "Keeps the first match under a custom equivalence. Equal values can use Chunk.dedupe instead." - }, - { - module: "effect-app/Chunk", - name: "elem", - expression: "(equivalence, value) => Chunk.containsWith(equivalence)(value)", - imports: chunk, - note: "Equivalence is treated as symmetric. containsWith applies it as equivalence(needle, element)." - }, - { - module: "effect-app/Schema", - name: "DateValid", - expression: "Date", - imports: [["Date", "effect-app/Schema"]], - note: "DateValid was an alias of effect-app Schema.Date. Schema.Date already rejects invalid dates." - }, - { - module: "effect-app/_ext/misc", - name: "encaseMaybeInEffect_", - expression: "Effect.fromOption", - imports: effect, - note: "Effect.fromOption(option, onNone) fails with the lazy error on None." - }, - { - module: "effect-app/_ext/misc", - name: "encaseMaybeEither_", - expression: "Result.fromOption", - imports: result, - note: "Result.fromOption(option, onNone)." - }, - { - module: "effect-app/_ext/misc", - name: "toNullable", - expression: "(effect) => Effect.map(effect, Option.getOrNull)", - imports: [...effect, ...option], - note: "Maps a successful Option to its value or null." - }, - { - module: "effect-app/_ext/misc", - name: "flatMapScoped", - expression: "(scoped, f) => Effect.scoped(Effect.flatMap(scoped, f))", - imports: effect, - note: "Closes the scope after the continuation." - }, - { - module: "effect-app/_ext/misc", - name: "scope", - expression: "(scoped, effect) => Effect.scoped(Effect.andThen(scoped, effect))", - imports: effect, - note: "Runs the second effect inside the first effect's scope, then closes it." - }, - ...([ - ["DateAddDays", "days", "date-add"], - ["DateAddHours", "hours", "date-add"], - ["DateAddMinutes", "minutes", "date-add"], - ["DateAddSeconds", "seconds", "date-add"], - ["DateAddWeeks", "weeks", "date-add"], - ["DateAddMonths", "months", "date-add"], - ["DateAddYears", "years", "date-add"], - ["DateSubDays", "days", "date-sub"], - ["DateSubHours", "hours", "date-sub"], - ["DateSubMinutes", "minutes", "date-sub"], - ["DateSubSeconds", "seconds", "date-sub"], - ["DateSubWeeks", "weeks", "date-sub"], - ["DateSubMonths", "months", "date-sub"], - ["DateSubYears", "years", "date-sub"] - ] as const) - .map(([name, unit, call]): NativeEffectReplacement => ({ - module: "effect-app/_ext/date", - name, - expression: unit, - imports: dateTime, - call, - note: - "pipeZonedLocal attaches the process timezone, runs DateTime.add or DateTime.subtract, and returns the instant with toDateUtc." - })) -] - -interface Binding { - readonly replacement: NativeEffectReplacement - readonly local: string -} - -const byModule = new Map>() -for (const replacement of nativeEffectReplacements) { - const existing = byModule.get(replacement.module) ?? new Map() - existing.set(replacement.name, replacement) - byModule.set(replacement.module, existing) -} - -const moduleNames = new Set(byModule.keys()) - -interface Rendered { - readonly text: string - readonly end: number -} - -/** Rewrite one source file. `source` is the file text. */ -export const migrateEffectAppSource = (source: string): string => { - const imports = findImports(source) - const bindings = new Map() - const namespaceBindings = new Map>() - const edits: Array<{ start: number; end: number; text: string }> = [] - const needed = new Map>() - const maskedSpans: Array<{ start: number; end: number }> = [] - - for (const statement of imports) { - const table = byModule.get(statement.module) - if (table === undefined) continue - maskedSpans.push({ start: statement.start, end: statement.end }) - - if (statement.namespace !== undefined) { - namespaceBindings.set(statement.namespace, table) - continue - } - - const removed: Array = [] - for (const specifier of statement.specifiers) { - const replacement = table.get(specifier.imported) - if (replacement === undefined) continue - bindings.set(specifier.local, { replacement, local: specifier.local }) - removed.push(specifier) - rememberImports(needed, replacement) - } - if (removed.length === 0) continue - if (removed.length === statement.specifiers.length) { - edits.push({ start: statement.start, end: statement.end, text: "" }) - } else { - const kept = statement.specifiers.filter((specifier) => !removed.includes(specifier)) - edits.push({ - start: statement.clauseStart, - end: statement.clauseEnd, - text: `{ ${kept.map((specifier) => specifier.text).join(", ")} }` - }) - } - } - - const mask = maskSpans(maskNonCode(source), maskedSpans) - - for (const [local, binding] of bindings) { - for (const match of identifierSpans(mask, local)) { - const rendered = render(binding.replacement, source, match + local.length) - edits.push({ start: match, end: rendered.end, text: rendered.text }) - } - } - - for (const [namespace, table] of namespaceBindings) { - let used = false - for (const match of identifierSpans(mask, namespace)) { - const after = skipSpaces(source, match + namespace.length) - if (source[after] !== ".") continue - const propertyStart = after + 1 - const property = readIdentifier(source, propertyStart) - if (property === undefined) continue - const replacement = table.get(property.name) - if (replacement === undefined) { - used = true - continue - } - rememberImports(needed, replacement) - const rendered = render(replacement, source, propertyStart + property.name.length) - edits.push({ start: match, end: rendered.end, text: rendered.text }) - } - if (!used) { - const statement = imports.find((item) => item.namespace === namespace && byModule.has(item.module)) - if (statement !== undefined) edits.push({ start: statement.start, end: statement.end, text: "" }) - } - } - - return insertImports(applyEdits(source, edits), needed) -} - -const render = (replacement: NativeEffectReplacement, source: string, afterName: number): Rendered => { - const bare = { text: replacement.expression, end: afterName } - if (replacement.call === undefined) return bare - const open = skipSpaces(source, afterName) - if (source[open] !== "(") return bare - const args = splitArgs(source, open) - if (args === undefined) return bare - if (replacement.call === "chunk" && args.args.length === 2) { - return { - text: `Chunk.fromIterable(Array.chunksOf(${args.args[0]}, ${args.args[1]}))`, - end: args.end - } - } - if ((replacement.call === "date-add" || replacement.call === "date-sub") && args.args.length === 2) { - const method = replacement.call === "date-add" ? "add" : "subtract" - return { - text: `pipeZonedLocal(${args.args[0]}, DateTime.${method}({ ${replacement.expression}: ${args.args[1]} }))`, - end: args.end - } - } - return bare -} - -const rememberImports = (needed: Map>, replacement: NativeEffectReplacement) => { - for (const [local, from] of replacement.imports) { - const names = needed.get(from) ?? new Set() - names.add(local) - needed.set(from, names) - } -} - -interface Specifier { - readonly imported: string - readonly local: string - readonly text: string -} - -interface ImportStatement { - readonly start: number - readonly end: number - readonly clauseStart: number - readonly clauseEnd: number - readonly module: string - readonly namespace?: string - readonly specifiers: ReadonlyArray -} - -const findImports = (source: string): ReadonlyArray => { - const found: Array = [] - const pattern = /\bimport\s+(type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["']/g - for (const match of source.matchAll(pattern)) { - const clause = match[2] ?? "" - const moduleName = match[3] ?? "" - if (!moduleNames.has(moduleName)) continue - const start = lineStart(source, match.index ?? 0) - const end = lineEnd(source, (match.index ?? 0) + match[0].length) - const clauseStart = (match.index ?? 0) + match[0].indexOf(clause) - const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause.trim()) - found.push({ - start, - end, - clauseStart, - clauseEnd: clauseStart + clause.length, - module: moduleName, - ...(namespace?.[1] === undefined ? {} : { namespace: namespace[1] }), - specifiers: namespace === null ? parseSpecifiers(clause) : [] - }) - } - return found -} - -const parseSpecifiers = (clause: string): ReadonlyArray => { - const brace = clause.indexOf("{") - if (brace < 0) return [] - const end = clause.lastIndexOf("}") - const body = clause.slice(brace + 1, end) - return body.split(",").flatMap((part) => { - const text = part.trim() - if (text === "" || text.startsWith("type ")) return [] - const alias = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(text) - if (alias === null) return [] - return [{ imported: alias[1]!, local: alias[2] ?? alias[1]!, text }] - }) -} - -const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - -const identifierSpans = (mask: string, name: string): ReadonlyArray => { - const spans: Array = [] - const pattern = new RegExp(`(? { - const match = /^[A-Za-z_$][\w$]*/.exec(source.slice(index)) - return match === null ? undefined : { name: match[0] } -} - -const splitArgs = ( - source: string, - open: number -): { readonly args: ReadonlyArray; readonly end: number } | undefined => { - const balanced = readBalanced(source, open, "(", ")") - if (balanced === undefined) return undefined - const args: Array = [] - let depth = 0 - let quote: "'" | "\"" | "`" | undefined - let start = 0 - const body = balanced.body - for (let i = 0; i < body.length; i++) { - const char = body[i] - if (quote !== undefined) { - if (char === "\\") { - i++ - continue - } - if (char === quote) quote = undefined - continue - } - if (char === "'" || char === "\"" || char === "`") { - quote = char - continue - } - if (char === "(" || char === "[" || char === "{") depth++ - if (char === ")" || char === "]" || char === "}") depth-- - if (char === "," && depth === 0) { - args.push(body.slice(start, i).trim()) - start = i + 1 - } - } - const last = body.slice(start).trim() - if (last !== "") args.push(last) - return { args, end: balanced.end } -} - -const readBalanced = ( - source: string, - open: number, - left: string, - right: string -): { readonly body: string; readonly end: number } | undefined => { - if (source[open] !== left) return undefined - let depth = 0 - let quote: "'" | "\"" | "`" | undefined - for (let i = open; i < source.length; i++) { - const char = source[i] - if (quote !== undefined) { - if (char === "\\") { - i++ - continue - } - if (char === quote) quote = undefined - continue - } - if (char === "'" || char === "\"" || char === "`") { - quote = char - continue - } - if (char === left) depth++ - if (char === right) { - depth-- - if (depth === 0) return { body: source.slice(open + 1, i), end: i + 1 } - } - } - return undefined -} - -const maskSpans = (mask: string, spans: ReadonlyArray<{ start: number; end: number }>): string => { - const chars = [...mask] - for (const span of spans) { - for (let i = span.start; i < span.end && i < chars.length; i++) { - if (chars[i] !== "\n") chars[i] = " " - } - } - return chars.join("") -} - -const skipSpaces = (source: string, index: number): number => { - let i = index - while (i < source.length && (source[i] === " " || source[i] === "\n" || source[i] === "\t")) i++ - return i -} - -const lineStart = (source: string, index: number): number => { - const start = source.lastIndexOf("\n", index - 1) - return start < 0 ? 0 : start + 1 -} - -const lineEnd = (source: string, index: number): number => { - const end = source.indexOf("\n", index) - return end < 0 ? source.length : end + 1 -} - -const applyEdits = (source: string, edits: ReadonlyArray<{ start: number; end: number; text: string }>): string => { - const ordered = [...edits].sort((a, b) => b.start - a.start) - let next = source - for (const edit of ordered) next = next.slice(0, edit.start) + edit.text + next.slice(edit.end) - return next -} - -const insertImports = (source: string, needed: ReadonlyMap>): string => { - const lines: Array = [] - for (const [from, names] of needed) { - for (const name of names) { - if (hasImport(source, name, from)) continue - lines.push( - namedImportNames.has(name) - ? `import { ${name} } from "${from}"` - : `import * as ${name} from "${from}"` - ) - } - } - if (lines.length === 0) return source.replace(/^\n+/, "") - const lastImport = lastImportEnd(source) - const block = `${lines.sort().join("\n")}\n` - if (lastImport === 0) return `${block}${source.replace(/^\n+/, "")}` - return `${source.slice(0, lastImport)}${block}${source.slice(lastImport)}`.replace(/^\n+/, "") -} - -const namedImportNames = new Set(["absurd", "Date", "pipeZonedLocal"]) - -const hasImport = (source: string, name: string, from: string): boolean => { - const fromPattern = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - const namespace = new RegExp(`import\\s+\\*\\s+as\\s+${name}\\s+from\\s+["']${fromPattern}["']`) - const named = new RegExp(`import\\s+(type\\s+)?\\{[^}]*\\b${name}\\b[^}]*}\\s+from\\s+["']${fromPattern}["']`) - const barrel = name === name[0]?.toUpperCase() - ? new RegExp(`import\\s+(type\\s+)?\\{[^}]*\\b${name}\\b[^}]*}\\s+from\\s+["']effect["']`) - : undefined - return namespace.test(source) || named.test(source) || barrel?.test(source) === true -} - -const lastImportEnd = (source: string): number => { - const pattern = /\bimport\s+(type\s+)?[\s\S]*?\s+from\s+["'][^"']+["']/g - let end = 0 - for (const match of source.matchAll(pattern)) { - end = lineEnd(source, (match.index ?? 0) + match[0].length) - } - return end -} - -const maskNonCode = (source: string): string => { - const chars = [...source] - let i = 0 - const blank = (from: number, to: number) => { - for (let j = from; j < to; j++) { - if (chars[j] !== "\n") chars[j] = " " - } - } - while (i < source.length) { - const two = source.slice(i, i + 2) - if (two === "//") { - const end = source.indexOf("\n", i) - blank(i, end < 0 ? source.length : end) - i = end < 0 ? source.length : end - continue - } - if (two === "/*") { - const end = source.indexOf("*/", i + 2) - const stop = end < 0 ? source.length : end + 2 - blank(i, stop) - i = stop - continue - } - const char = source[i] - if (char === "'" || char === "\"") { - const stop = scanQuoted(source, i, char) - blank(i, stop) - i = stop - continue - } - if (char === "`") { - const stop = scanTemplate(source, i) - blank(i, stop) - i = stop - continue - } - i++ - } - return chars.join("") -} - -const scanQuoted = (source: string, start: number, quote: "'" | "\""): number => { - for (let i = start + 1; i < source.length; i++) { - if (source[i] === "\\") { - i++ - continue - } - if (source[i] === quote) return i + 1 - } - return source.length -} - -const scanTemplate = (source: string, start: number): number => { - for (let i = start + 1; i < source.length; i++) { - if (source[i] === "\\") { - i++ - continue - } - if (source[i] === "`") return i + 1 - if (source.slice(i, i + 2) === "${") { - let depth = 1 - i += 2 - while (i < source.length && depth > 0) { - if (source[i] === "{") depth++ - else if (source[i] === "}") depth-- - i++ - } - i-- - } - } - return source.length -} diff --git a/packages/effect-app/src/rpc.ts b/packages/effect-app/src/rpc.ts deleted file mode 100644 index e07797b67..000000000 --- a/packages/effect-app/src/rpc.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * as Invalidation from "./rpc/Invalidation.ts" -export * as MiddlewareMaker from "./rpc/MiddlewareMaker.ts" -export * as RpcContextMap from "./rpc/RpcContextMap.ts" -export * as RpcMiddleware from "./rpc/RpcMiddleware.ts" diff --git a/packages/effect-app/src/rpc/Invalidation.ts b/packages/effect-app/src/rpc/Invalidation.ts deleted file mode 100644 index 2e6db6dee..000000000 --- a/packages/effect-app/src/rpc/Invalidation.ts +++ /dev/null @@ -1,278 +0,0 @@ -import * as Ref from "effect/Ref" -import { Rpc } from "effect/rpc" -import { type ClientForOptions, makeQueryKey } from "../client/clientFor.ts" -import * as Context from "../Context.ts" -import * as DataDependencies from "../DataDependencies.ts" -import * as Effect from "../Effect.ts" -import * as S from "../Schema.ts" - -/** - * Shorthand for a handler-derived invalidation key. - * Accepts an RPC handler object so `add(GetMe)` is equivalent to - * `add(makeQueryKey(GetMe))`. - */ -export type InvalidationKeyInput = InvalidationKey | { readonly id: string; readonly options?: ClientForOptions } - -const normalizeKey = (input: InvalidationKeyInput): InvalidationKey => { - if (Array.isArray(input)) return input - const handler = input as { id: string; options?: ClientForOptions } - return makeQueryKey(handler.options ? { id: handler.id, options: handler.options } : { id: handler.id }) -} - -// Disambiguates `add` overloads: a `string[]` means a single key; an array -// containing arrays-or-objects means a batch of inputs. -const isBatch = ( - input: InvalidationKeyInput | ReadonlyArray -): input is ReadonlyArray => - Array.isArray(input) && input[0] !== undefined && typeof input[0] !== "string" - -const normalizeInputs = ( - input: InvalidationKeyInput | ReadonlyArray -): ReadonlyArray => isBatch(input) ? input.map(normalizeKey) : [normalizeKey(input)] - -/** - * A single segment within an `InvalidationKey` array. - * Accepts any JSON-compatible value: string, number, boolean, null, - * arrays and objects recursively — matching TanStack Query's `queryKey` element type. - */ -export const InvalidationKeySegment = S.Json -export type InvalidationKeySegment = S.Schema.Type - -/** Schema for a single invalidation key – an array of segments compatible with TanStack Query `queryKey`. */ -export const InvalidationKey = S.Array(InvalidationKeySegment) -export type InvalidationKey = S.Schema.Type - -/** Schema for the full set of invalidation keys – an array of `InvalidationKey`. */ -export const InvalidationKeys = S.Array(InvalidationKey) -export type InvalidationKeys = S.Schema.Type - -/** Metadata included in every command response for server-driven cache invalidation. */ -export const CommandMetaData = S.Struct({ - invalidateQueries: InvalidationKeys, - dataDependencies: DataDependencies.DataDependencySet -}) -export type CommandMetaData = S.Schema.Type - -export const emptyDataDependencies: DataDependencies.DataDependencySet = { - reads: DataDependencies.empty(), - writes: DataDependencies.empty() -} - -export const makeMetaData = ( - invalidateQueries: InvalidationKeys, - dataDependencies: DataDependencies.DataDependencySet = emptyDataDependencies -): CommandMetaData => ({ invalidateQueries, dataDependencies }) - -export const QueryResponseWithMetaData = (success: S) => - S.Struct({ payload: success, metadata: S.Struct({ dataDependencies: DataDependencies.DataDependencySet }) }) - -/** - * Wraps a command's success schema so that the wire format carries both the `payload` - * (the handler's actual return value) and `metadata` (server-driven cache invalidation keys). - * Transparent to users: the server handler returns the plain payload and the client receives - * the plain payload — wrapping/unwrapping is handled internally by the routing layer. - */ -export const CommandResponseWithMetaData = (success: S) => - S.Struct({ payload: success, metadata: CommandMetaData }) - -/** - * Wraps a command's failure schema so that the wire format carries both the `error` - * (the handler's actual failure value) and `metadata` (server-driven cache invalidation keys - * accumulated thus far before the failure occurred). - * Transparent to users: the server handler fails with the plain error and the client receives - * the plain error — wrapping/unwrapping is handled internally by the routing layer. - */ -export const CommandFailureWithMetaData = (error: E) => - S.Struct({ _tag: S.Literal("CommandFailureWithMetaData"), error, metadata: CommandMetaData }) - -/** - * Stream chunk schema for stream responses with metadata. - * Each item is either a data value, an intermediate "metadata" signal carrying cache - * invalidation keys accumulated since the previous drain, or a final "done" signal. - * Transparent to users: stream handlers return plain values and clients receive plain values — - * wrapping/unwrapping is handled internally by the routing layer. - * - * The "done" chunk is always the last item in the stream and carries any remaining invalidation - * keys. An optional "metadata" chunk may appear after any "value" chunk and carries keys - * accumulated since the last drain (V3: mid-stream invalidation). - */ -export const StreamResponseChunk = (success: S) => - S.Union([ - S.Struct({ _tag: S.Literal("value"), value: success }), - S.Struct({ _tag: S.Literal("metadata"), metadata: CommandMetaData }), - S.Struct({ _tag: S.Literal("done"), metadata: CommandMetaData }) - ]) - -export type StreamResponseChunk = - | { readonly _tag: "value"; readonly value: A } - | { readonly _tag: "metadata"; readonly metadata: CommandMetaData } - | { readonly _tag: "done"; readonly metadata: CommandMetaData } - -/** - * Stream chunk schema for stream failures with metadata. - * Used to signal a stream failure while still carrying cache invalidation keys - * accumulated thus far. - */ -export const StreamFailureChunk = (error: E) => - S.Struct({ _tag: S.Literal("error"), error, metadata: CommandMetaData }) - -export type StreamFailureChunk = { readonly _tag: "error"; readonly error: E; readonly metadata: CommandMetaData } - -/** - * Context annotation for declaring static cache invalidation keys on a low-level `Rpc` definition. - * These keys are always included in the command response metadata, regardless of the handler logic. - * - * Prefer using `makeQueryKey` over raw string arrays to stay in sync with the actual query - * definitions without manual string maintenance: - * - * ```ts - * import { makeQueryKey } from "effect-app/client" - * import { Invalidation } from "effect-app/rpc" - * import * as UserRsc from "../User/index.ts" // separate module to avoid circular deps - * - * class UpdateProfile extends Rpc.make("UpdateProfile", { ... }) - * .annotate(Invalidation.Invalidates, [makeQueryKey(UserRsc.GetMe), makeQueryKey(UserRsc.GetProfile)]) {} - * ``` - * - * **Circular dependency note:** if mutations and queries live in the same file you may hit a - * circular reference at evaluation time. The idiomatic fix is to move mutations into their own - * module (e.g. `User/mutations.ts`) that directly imports the relevant query classes rather than - * re-exporting them through a barrel. - * - * For the higher-level `Command`/`Query` builders from `makeRpcClient`, use the - * `invalidatesQueries` callback argument instead (it receives the same query keys at runtime). - */ -export const Invalidates = Context.Reference>( - "effect-app/rpc/Invalidates", - { defaultValue: () => [] } -) -export type Invalidates = typeof Invalidates - -/** The shape of the per-request service that accumulates invalidation keys. */ -export interface InvalidationSetService { - readonly add: ( - input: InvalidationKeyInput | ReadonlyArray - ) => Effect.Effect - readonly get: Effect.Effect> - /** - * V3: Reads all currently accumulated keys and resets the bucket to empty. - * Used by the stream routing layer to emit intermediate "metadata" chunks - * without re-sending keys that have already been forwarded to the client. - */ - readonly drain: Effect.Effect> -} - -/** - * Request-scoped service for accumulating invalidation keys dynamically inside a handler. - * Provided by `InvalidationMiddlewareLive` for every RPC call; has a no-op default so it is - * safe to use even when the HTTP middleware is absent (tests, workers, etc.). - * - * Use `InvalidationSet.add(key)` as a shorthand to skip `.use(_ => _.add(key))`. The - * underlying service is still available via `.use` / `.useSync` for advanced cases. - * - * `add` accepts an RPC handler directly (e.g. `UserRsc.GetMe`) — its query key is derived via - * `makeQueryKey` so keys stay in sync with the actual query definitions. Raw `InvalidationKey` - * arrays and arrays of either form are also accepted. - * - * ```ts - * import * as Effect from "effect/Effect" - * import { Invalidation } from "effect-app/rpc" - * import * as CartRsc from "../Cart/queries.ts" - * import * as UserRsc from "../User/queries.ts" - * - * const handler = Effect.fnUntraced(function*(req: UpdateCartRequest) { - * const cart = yield* CartRepo.save(req.cart) - * - * // single handler - * yield* Invalidation.InvalidationSet.add(UserRsc.GetMe) - * - * // batch - * if (cart.isCheckedOut) { - * yield* Invalidation.InvalidationSet.add([CartRsc.GetCartStats, UserRsc.GetMe]) - * } - * - * return cart - * }) - * ``` - * - * You can combine static (`Invalidates` annotation) and dynamic (`InvalidationSet.use`) keys: - * the annotation pre-populates the set before the handler runs; dynamic additions accumulate - * throughout the handler. All keys are included in the command response metadata. - */ -const InvalidationSetRef = Context.Reference( - "effect-app/rpc/InvalidationSet", - { - defaultValue: () => ({ - add: (_input: InvalidationKeyInput | ReadonlyArray) => Effect.void, - get: Effect.succeed([] as ReadonlyArray), - drain: Effect.succeed([] as ReadonlyArray) - }) - } -) -export const InvalidationSet = Object.assign(InvalidationSetRef, { - /** - * Shortcut for `InvalidationSet.use(_ => _.add(input))`. Accepts a single - * `InvalidationKeyInput` or an array of them. - */ - add: (input: InvalidationKeyInput | ReadonlyArray) => - InvalidationSetRef.use((_) => _.add(input)) -}) -export type InvalidationSet = typeof InvalidationSetRef - -/** Creates a fresh `InvalidationSet` implementation backed by a `Ref`. */ -export const makeInvalidationSet = (ref: Ref.Ref>): InvalidationSetService => ({ - add: (input) => Ref.update(ref, (keys) => [...keys, ...normalizeInputs(input)]), - get: Ref.get(ref), - drain: Ref.getAndSet(ref, []) -}) - -/** - * `Rpc.Custom` definition for command RPCs that wrap the success/error schemas - * with `CommandResponseWithMetaData` / `CommandFailureWithMetaData`. The wrap - * lets server forward accumulated invalidation keys on both success and - * handler-thrown failure paths. Middleware-thrown errors bypass the wrap - * (the handler never ran, so no metadata) and flow raw at the Cause level — - * the client decodes them via the `rpc.middlewares[*].error` failure-union - * channel of `Rpc.exitSchema`. - */ -// eslint-disable-next-line import/namespace -export interface CommandRpc extends Rpc.Custom { - readonly out: Rpc.Custom.Out< - ReturnType>, - ReturnType> - > -} - -/** - * Custom Rpc constructor for command RPCs. - * Wraps the success schema with `CommandResponseWithMetaData` and the error - * schema with `CommandFailureWithMetaData`. - */ -export const makeCommandRpc = Rpc.custom(({ defect, error, success }) => ({ - success: CommandResponseWithMetaData(success), - error: CommandFailureWithMetaData(error), - defect -})) - -/** - * `Rpc.Custom` definition for stream RPCs that wrap the success/error schemas - * with `StreamResponseChunk` / `StreamFailureChunk`. - */ -// eslint-disable-next-line import/namespace -export interface StreamRpc extends Rpc.Custom { - readonly out: Rpc.Custom.Out< - ReturnType>, - ReturnType> - > -} - -/** - * Custom Rpc constructor for stream RPCs. - * Wraps the success schema with `StreamResponseChunk` and - * the error schema with `StreamFailureChunk`. - */ -export const makeStreamRpc = Rpc.custom(({ defect, error, success }) => ({ - success: StreamResponseChunk(success), - error: StreamFailureChunk(error), - defect -})) diff --git a/packages/effect-app/src/rpc/MiddlewareMaker.ts b/packages/effect-app/src/rpc/MiddlewareMaker.ts deleted file mode 100644 index 4c5b67469..000000000 --- a/packages/effect-app/src/rpc/MiddlewareMaker.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { type NonEmptyArray, type NonEmptyReadonlyArray } from "effect/Array" -import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" -import { Rpc, type RpcGroup, type RpcSchema } from "effect/rpc" -import { type HandlersFrom } from "effect/rpc/RpcGroup" -import * as S from "effect/Schema" -import type * as Scope from "effect/Scope" -import { type Simplify } from "effect/Types" -import * as Context from "../Context.ts" -import { PreludeLogger } from "../logger.ts" -import { type TypeTestId } from "../TypeTest.ts" -import { type GetContextConfig, type RequestContextMapTagAny, type RpcContextMap } from "./RpcContextMap.ts" -import { type AddMiddleware, type AnyDynamic, type RpcDynamic, type RpcMiddlewareV4, type TagClassAny } from "./RpcMiddleware.ts" -import * as RpcMiddlewareX from "./RpcMiddleware.ts" - -// adapter for effect/rpc v3 middleware provides. (in effect-smol (v4), it's just a Service Identifier, no tags.) -// hm? -type MakeTags = A - -export interface MiddlewareMaker< - Self, - Id extends string, - RequestContextMap extends Record, - MiddlewareProviders extends ReadonlyArray -> extends - RpcMiddlewareX.TagClass< - Self, - Id, - Simplify< - & (Exclude< - MiddlewareMaker.ManyRequired, - MiddlewareMaker.ManyProvided - > extends never ? {} : { - readonly requires: MakeTags< - Exclude< - MiddlewareMaker.ManyRequired, - MiddlewareMaker.ManyProvided - > - > - }) - & (MiddlewareMaker.ManyErrors extends never ? {} - : { - readonly error: S.Codec> - }) - & (MiddlewareMaker.ManyProvided extends never ? {} - : { readonly provides: MakeTags> }) - >, - { - provides: MiddlewareMaker.ManyProvided extends never ? never - : MakeTags> - requires: Exclude< - MiddlewareMaker.ManyRequired, - MiddlewareMaker.ManyProvided - > extends never ? never - : MakeTags< - Exclude< - MiddlewareMaker.ManyRequired, - MiddlewareMaker.ManyProvided - > - > - } - > -{ - readonly layer: Layer.Layer> - readonly requestContext: RequestContextTag - readonly requestContextMap: RequestContextMap -} - -export interface RequestContextTag> - extends Context.Service<"RequestContextConfig", GetContextConfig> -{} - -export namespace MiddlewareMaker { - export type Any = TagClassAny - - export type ApplyServices = Exclude> | Required - - export type ApplyManyServices, R> = - | Exclude }[number]> - | { [K in keyof A]: Required }[number] - - export type ManyProvided> = A extends NonEmptyReadonlyArray - ? { [K in keyof A]: Provided }[number] - : Provided - export type ManyRequired> = A extends NonEmptyReadonlyArray - ? { [K in keyof A]: Required }[number] - : Required - export type ManyErrors> = A extends NonEmptyReadonlyArray - ? { [K in keyof A]: Errors }[number] - : Errors - - export type Provided = T extends TagClassAny ? T extends { provides: infer _P } ? _P - : never - : never - - export type Errors = T extends TagClassAny ? T extends { error: S.Top } ? T["error"]["Type"] - : never - : never - - export type Required = T extends TagClassAny ? T extends { requires: infer _R } ? _R - : never - : never -} - -// the following implements sort of builder pattern -// we support both sideways and upwards elimination of dependencies - -// it's for dynamic middlewares -type GetDependsOnKeys = MW extends { dependsOn: NonEmptyReadonlyArray } ? { - [K in keyof MW["dependsOn"]]: MW["dependsOn"][K] extends AnyDynamic ? MW["dependsOn"][K]["dynamic"]["key"] - : never - }[keyof MW["dependsOn"]] - : never - -type FilterInDynamicMiddlewares< - MWs extends ReadonlyArray, - RequestContextMap extends Record -> = { - [K in keyof MWs]: MWs[K] extends { dynamic: RpcDynamic } ? MWs[K] - : never -} - -type RecursiveHandleMWsSideways< - MWs, - R extends { - self: any - id: string - rcm: Record - provided: keyof R["rcm"] // that's fine - middlewares: ReadonlyArray - dmp: any - middlewareR: any - } -> = MWs extends [] ? R - : MWs extends [infer F, ...infer Rest extends ReadonlyArray] - ? F extends MiddlewareMaker.Any ? RecursiveHandleMWsSideways[number]["dynamic"]["key"], - // F is fine here because only dynamic middlewares will have 'dependsOn' prop - GetDependsOnKeys - > - middlewares: [...R["middlewares"], F] - dmp: [FilterInDynamicMiddlewares<[F], R["rcm"]>[number]] extends [never] ? R["dmp"] - : - & R["dmp"] - & { - [U in FilterInDynamicMiddlewares<[F], R["rcm"]>[number] as U["dynamic"]["key"]]: U - } - middlewareR: MiddlewareMaker.ApplyManyServices<[F], R["middlewareR"]> - }> - // TypeScript inference fails when checking F extends MiddlewareMaker.Any during F's inference - // if F is a class with static properties - deferring the check avoids this limitation - : `Absurd: F must extend MiddlewareMaker.Any` - : never - -export interface BuildingMiddleware< - Self, - Id extends string, - RequestContextMap extends Record, - Provided extends keyof RequestContextMap, - Middlewares extends ReadonlyArray, - DynamicMiddlewareProviders, - out MiddlewareR extends { _tag: string } = never -> { - rpc: < - const Tag extends string, - Payload extends S.Top | S.Struct.Fields = typeof S.Void, - Success extends S.Top = typeof S.Void, - Error extends S.Top = typeof S.Never, - const Stream extends boolean = false, - Config extends GetContextConfig = {} - >(tag: Tag, options?: { - readonly payload?: Payload - readonly success?: Success - readonly error?: Error - readonly stream?: Stream - readonly config?: Config - readonly primaryKey?: [Payload] extends [S.Struct.Fields] ? (( - payload: Payload extends S.Struct.Fields ? Simplify["Type"]> : Payload["Type"] - ) => string) - : never - }) => - & Rpc.Rpc< - Tag, - Payload extends S.Struct.Fields ? S.Struct : Payload, - Stream extends true ? RpcSchema.Stream : Success, - Stream extends true ? typeof S.Never : Error - > - & { readonly config: Config } - - middleware>( - ...mw: MWs - ): RecursiveHandleMWsSideways extends infer Res extends { - self: any - id: string - rcm: RequestContextMap - provided: keyof RequestContextMap - middlewares: ReadonlyArray - dmp: any - middlewareR: any - } ? MiddlewaresBuilder< - Res["self"], - Res["id"], - Res["rcm"], - Res["provided"], - Res["middlewares"], - Res["dmp"], - Res["middlewareR"] - > - : never - - // helps debugging what are the missing requirements (type only) - readonly [TypeTestId]: { - missingDynamicMiddlewares: Exclude - missingContext: MiddlewareR - } -} - -export type MiddlewaresBuilder< - Self, - Id extends string, - RequestContextMap extends Record, - Provided extends keyof RequestContextMap = never, - Middlewares extends ReadonlyArray = [], - DynamicMiddlewareProviders = unknown, - MiddlewareR extends { _tag: string } = never -> = - & BuildingMiddleware< - Self, - Id, - RequestContextMap, - Provided, - Middlewares, - DynamicMiddlewareProviders, - MiddlewareR - > - & // keyof Omit extends never is true when all the dynamic middlewares are provided - // MiddlewareR is never when all the required services from generic & dynamic middlewares are provided - (keyof Omit extends never ? [MiddlewareR] extends [never] ? MiddlewareMaker< - Self, - Id, - RequestContextMap, - Middlewares - > - : { new(_: never): {} } - : { new(_: never): {} }) - -const middlewareMaker = Effect.fnUntraced(function*< - MiddlewareProviders extends ReadonlyArray ->(middlewares: MiddlewareProviders) { - type Middleware = RpcMiddlewareV4< - MiddlewareMaker.ManyProvided, - MiddlewareMaker.ManyErrors, - Exclude< - MiddlewareMaker.ManyRequired, - MiddlewareMaker.ManyProvided - > extends never ? never - : Exclude, MiddlewareMaker.ManyProvided> - > - type Next = Parameters[0] - type Options = Parameters[1] - - // we want to run them in reverse order because latter middlewares will provide context to former ones - const reversed = middlewares.toReversed() - const context = yield* Effect.context() - - // returns a Effect/RpcMiddlewareV4 with Scope.Scope in requirements - // v4: wrap middleware takes (effect, options) as two params instead of a single options bag - return (next: Next, options: Options) => { - // we start with the actual handler - let handler = next - - // inspired from Effect/RpcMiddleware - for (const tag of reversed) { - // use the tag to get the middleware from context - const middleware = Context.getUnsafe(context, tag) - - // wrap the current handler, allowing the middleware to run before and after it - handler = PreludeLogger.logDebug("Applying middleware wrap " + tag.key).pipe( - Effect.andThen(middleware(handler, options)) - ) as any - } - return handler - } -}) - -const makeMiddlewareBasic = () => -// by setting RequestContextMap beforehand, execute contextual typing does not fuck up itself to anys -< - const Id extends string, - RequestContextMap extends Record, - MiddlewareProviders extends ReadonlyArray ->( - id: Id, - rcm: RequestContextMap, - ...make: MiddlewareProviders -) => { - // reverse middlewares and wrap one after the other - const middleware = middlewareMaker(make) - - // Per-middleware error: union of the static `error` on the tag (if any) AND - // the rcm config entry pointed at by the middleware's `dynamic.key` (if any). - // Reason: middlewares declared with `dynamic: RequestContextMap.get("foo")` - // don't set a static `error` field — at runtime their `.error` defaults to - // `S.Never`. Without pulling from rcm, the composite middleware's - // `.error` collapses to `Never`, and `Rpc.exitSchema` (which walks - // `rpc.middlewares[*].error` to build the wire failure union) can't decode - // the actual middleware-thrown error type. Critical for stream rpcs whose - // top-level `errorSchema` is force-set to `Never` by effect-rpc. - const isMeaningfulError = (e: S.Top | undefined): e is S.Top => e !== undefined && e !== null && e !== S.Never - const rcmRecord = rcm as Record - const failures: Array = make.flatMap((_) => { - const out: Array = [] - if (isMeaningfulError(_.error)) out.push(_.error) - const key = _.dynamic?.key as string | undefined - if (key && rcmRecord[key] && isMeaningfulError(rcmRecord[key].error)) { - out.push(rcmRecord[key].error) - } - return out - }) - const provides = make.flatMap((_) => !_.provides ? [] : Array.isArray(_.provides) ? _.provides : [_.provides]) - const requires = make - .flatMap((_) => !_.requires ? [] : Array.isArray(_.requires) ? _.requires : [_.requires]) - .filter((_) => !provides.includes(_)) - - const [firstFailure, ...restFailures] = failures - - const MiddlewareMaker = RpcMiddlewareX.Tag()(id, { - error: (firstFailure - ? S.Union([firstFailure, ...restFailures]) - : S.Never) as unknown as MiddlewareMaker.ManyErrors extends never ? never - : S.Codec>, - requires: (requires.length > 0 - ? requires - : undefined) as unknown as Exclude< - MiddlewareMaker.ManyRequired, - MiddlewareMaker.ManyProvided - > extends never ? never : [ - MakeTags< - Exclude< - MiddlewareMaker.ManyRequired, - MiddlewareMaker.ManyProvided - > - > - ], - provides: (provides.length > 0 - ? provides - : undefined) as unknown as MiddlewareMaker.ManyProvided extends never ? never - : MakeTags> - }) - - const layer = Layer - .effect( - MiddlewareMaker, - middleware as Effect.Effect< - any - > - // todo; they dont change the type.. - // Effect.Error, - // Effect.Services - ) - - // add to the tag a default implementation - return Object.assign(MiddlewareMaker, { - layer, - // tag to be used to retrieve the RequestContextConfig from Rpc annotations - requestContext: Context.Service<"RequestContextConfig", GetContextConfig>( - "RequestContextConfig" - ), - requestContextMap: rcm - }) -} - -export const Tag = () => -< - const Id extends string, - RequestContextMap extends RequestContextMapTagAny ->(id: Id, rcm: RequestContextMap): MiddlewaresBuilder => { - const allMiddleware: MiddlewareMaker.Any[] = [] - const requestContext = Context.Service<"RequestContextConfig", GetContextConfig>( - "RequestContextConfig" - ) - const it = { - id, - // rpc with config - rpc: < - const Tag extends string, - Payload extends S.Top | S.Struct.Fields = typeof S.Void, - Success extends S.Top = typeof S.Void, - Error extends S.Top = typeof S.Never, - const Stream extends boolean = false, - Config extends GetContextConfig = {} - >(tag: Tag, options?: { - readonly payload?: Payload - readonly success?: Success - readonly error?: Error - readonly stream?: Stream - readonly config?: Config - readonly primaryKey?: [Payload] extends [S.Struct.Fields] ? (( - payload: Payload extends S.Struct.Fields ? Simplify["Type"]> : Payload["Type"] - ) => string) - : never - }): - & Rpc.Rpc< - Tag, - Payload extends S.Struct.Fields ? S.Struct : Payload, - // TODO: enhance `Error`. type based on middleware config. - Stream extends true ? RpcSchema.Stream : Success, - Stream extends true ? typeof S.Never : Error - > - & { config: Config } => - { - const config = options?.config ?? {} as Config - - // The rpc's `error` schema carries ONLY the request's own declared errors. - // Middleware errors (rcm-derived) reach the wire via the middleware tag - // attached to the rpc group later (`RpcGroup.middleware(...)` at the - // routing/client level), and are unioned into the failure schema by - // `Rpc.exitSchema`'s `rpc.middlewares[*].error` walk. - // @ts-expect-error — TypeScript can't prove Simplify ≡ { [K in keyof T]: T[K] } for unresolved generics (primaryKey) - const rpc = Rpc.make(tag, { - ...options?.payload !== undefined ? { payload: options.payload } : {}, - ...options?.success !== undefined ? { success: options.success } : {}, - ...options?.error !== undefined ? { error: options.error } : {}, - ...options?.stream !== undefined ? { stream: options.stream } : {}, - ...options?.primaryKey !== undefined ? { primaryKey: options.primaryKey } : {} - }) as any - - return Object.assign(rpc.annotate(requestContext, config), { config }) - }, - middleware: (...middlewares: any[]) => { - for (const mw of middlewares) { - // recall that we run middlewares in reverse order - allMiddleware.unshift(mw) - } - return allMiddleware.filter((m) => !!m.dynamic).length !== Object.keys(rcm.config).length - // for sure, until all the dynamic middlewares are provided it's non sensical to call makeMiddlewareBasic - ? it - // actually, we don't know yet if MiddlewareR is never, but we can't easily check it at runtime - : Object.assign(makeMiddlewareBasic()(id, rcm.config, ...allMiddleware), it) - } - } - return it as any -} - -// alternatively consider group.serverMiddleware? hmmm -export const middlewareGroup = < - RequestContextMap extends Record, - Middleware extends RpcMiddlewareX.TagClassAny & { - readonly requestContext: RequestContextTag - readonly requestContextMap: RequestContextMap - } ->( - middleware: Middleware -) => -(group: RpcGroup.RpcGroup) => { - type RN = AddMiddleware - const middlewaredGroup = group.middleware(middleware) as unknown as RpcGroup.RpcGroup - const toLayerOriginal = middlewaredGroup.toLayer.bind(middlewaredGroup) - return Object.assign(middlewaredGroup, { - toLayerDynamic: < - Handlers extends HandlersFrom, - EX = never, - RX = never - >( - build: - | Handlers - | Effect.Effect - ): Layer.Layer< - Rpc.ToHandler, - EX, - | Exclude - | RpcMiddlewareX.HandlersContext - > => { - return toLayerOriginal(build as any) as any // ?? - } - }) -} diff --git a/packages/effect-app/src/rpc/README.md b/packages/effect-app/src/rpc/README.md deleted file mode 100644 index 192abc189..000000000 --- a/packages/effect-app/src/rpc/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Effect RPC extensions - -The extensions use V4 format of RPC middleware: - -- supports `requires` besides `provides` -- `requires` and `provides` should be set as second generic argument: `Tag`. -- `wrap: true` is the default, there is no classic `provides: Tag` - -## Features - -### Bundle Middleware - -Compose multiple middleware into a single middleware, so that you can use them easily together as a unit. -The middleware group is fully typesafe, supporting chained or sideways elimination of requirements. -All configured dynamic middleware must be provided or the bundle is not complete. - -### Dynamic Middleware - -Dynamic middleware is middleware that is being controlled by Configuration set on the RPC. -The middleware generally runs globally, but it's behaviour depends on configuration on the RPC. - -For instance, `AllowAnonymous`, with a default of `false`, would either provide a `UserProfile` service, or fail with a `NotLoggedInError`. -therefore `UserProfile` is eliminated from a handler implementing that rpc. -If the RPC however opts out and specifies: `allowAnonymous: true`, the `UserProfile` service is no longer eliminated from the handler, -and thus one has to use `Effect.serviceOption(UserProfile)` instead, to not get a type error. - -For instance, `RequireRoles` would by default require a `manager` role, but can be opted out from per RPC, perhaps lowering to `user` or `anonymous` role. - -### Create RPCGroups and Handlers from a common set of Middleware - -You create a bundle of middleware, containing standard or dynamic middleware, and from it you can create all your rpc groups and handlers. -NOTE: perhaps not as useful anymore if support for dynamic middleware gets integrated directly into effect RPC. -`middleware.group`, `middleware.rpc` and `group.toLayerDynamic` would become obsolete, and `RpcGroup.make().middleware(globalMiddleware)` would be simpler. - -## Examples - -See [tests](../../../infra/test/rpc-multi-middleware.test.ts) - -## Future - -Gauge interest to integrate natively into `@effect/rpc` package, and then integrate. diff --git a/packages/effect-app/src/rpc/RpcContextMap.ts b/packages/effect-app/src/rpc/RpcContextMap.ts deleted file mode 100644 index a3de0a89a..000000000 --- a/packages/effect-app/src/rpc/RpcContextMap.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { type AnyWithProps } from "effect/rpc/Rpc" -import type * as S from "effect/Schema" -import * as Context from "../Context.ts" -import { type RpcDynamic } from "./RpcMiddleware.ts" - -type Values> = T[keyof T] - -/** - * Middleware is inactivate by default, the Key is optional in route context, and the service is optionally provided as Effect Context. - * Unless explicitly configured as `true`. - */ -export type RpcContextMap = { - // todo; rename Provides - service: Service - error: E - contextActivation: true - inverted: false -} - -export declare namespace RpcContextMap { - /** - * Middleware is active by default, and provides the Service at Key in route context, and the Service is provided as Effect Context. - * Unless explicitly omitted. - */ - export type Inverted = { - service: Service - error: E - contextActivation: false - inverted: true - } - - export type Custom = { - service: Service - error: E - contextActivation: C - inverted: false - } - - export type Any = { - service: any - error: S.Top - contextActivation: any - inverted: boolean - } -} - -export type GetContextConfig> = { - [K in keyof RequestContextMap]?: RequestContextMap[K]["inverted"] extends true - ? RequestContextMap[K]["contextActivation"] extends true ? false - : RequestContextMap[K]["contextActivation"] extends false ? true - : RequestContextMap[K]["contextActivation"] - : RequestContextMap[K]["contextActivation"] -} - -export type GetEffectContext, T> = Values< - // inverted: contextActivation is false => remove if explicitly set to true (like allowAnonymous: true disables auth and auth service and related errors) - & { - [ - key in keyof RequestContextMap as RequestContextMap[key]["contextActivation"] extends true ? never - : key extends keyof T ? T[key] extends true ? never : key - : key - ]: // TODO: or as an Optional available? - RequestContextMap[key]["service"] - } - // normal: contextActivation is true => add if explicitly set to true - & { - [ - key in keyof RequestContextMap as RequestContextMap[key]["contextActivation"] extends false ? never - : key extends keyof T ? T[key] extends true ? key : never - : never - ]: // TODO: or as an Optional available? - RequestContextMap[key]["service"] - } -> - -export type GetEffectError, T> = Values< - // inverted: contextActivation is false => remove if explicitly set to true (like allowAnonymous: true disables auth and auth service and related errors) - & { - [ - key in keyof RequestContextMap as RequestContextMap[key]["contextActivation"] extends true ? never - : key extends keyof T ? T[key] extends true ? never : key - : key - ]: // TODO: or as an Optional available? - RequestContextMap[key]["error"] - } - // normal: contextActivation is true => add if explicitly set to true - & { - [ - key in keyof RequestContextMap as RequestContextMap[key]["contextActivation"] extends false ? never - : key extends keyof T ? T[key] extends true ? key : never - : never - ]: // TODO: or as an Optional available? - RequestContextMap[key]["error"] - } -> - -const tag = Context.Service("RequestContextConfig") - -export const makeMap = >(config: Config) => { - const cls = class { - readonly config: Config - constructor() { - this.config = config - } - } - return Object.assign(cls, { - config, /** Retrieves RequestContextConfig out of the Rpc annotations */ - getConfig: (rpc: AnyWithProps): GetContextConfig => { - return Context.getOrElse(rpc.annotations, tag as any, () => ({})) - }, - /** Adapter used when setting the dynamic prop on a middleware implementation */ - get: < - Key extends (keyof Config) & string - >(key: Key): RpcDynamic => ({ - key, - settings: { service: config[key]!["service"] } as Config[Key] - }) - }) -} - -export const make = () => -( - error: E -): RpcContextMap => ({ - service: null as Service, - error, - contextActivation: true, - inverted: false -}) - -export const makeInverted = () => -( - error: E -): RpcContextMap.Inverted => ({ - service: null as Service, - error, - contextActivation: false, - inverted: true -}) - -export const makeCustom = () => -( - error: E, - contextActivation: C -): RpcContextMap.Custom => ({ - service: null as Service, - error, - contextActivation, - inverted: false -}) - -export type RequestContextMapTagAny = { readonly config: Record } diff --git a/packages/effect-app/src/rpc/RpcMiddleware.ts b/packages/effect-app/src/rpc/RpcMiddleware.ts deleted file mode 100644 index d4090157e..000000000 --- a/packages/effect-app/src/rpc/RpcMiddleware.ts +++ /dev/null @@ -1,244 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { type NonEmptyReadonlyArray } from "effect/Array" -import type * as Effect from "effect/Effect" -import { type Rpc, RpcMiddleware } from "effect/rpc" -import { type TypeId } from "effect/rpc/RpcMiddleware" -import type * as S from "effect/Schema" -import type * as Scope from "effect/Scope" -import type * as Stream from "effect/Stream" -import type * as Context from "../Context.ts" -import { type GetEffectContext, type RpcContextMap } from "./RpcContextMap.ts" - -export type RpcMiddlewareV4 = RpcMiddleware.RpcMiddleware - -export type RpcOptionsOriginal = { - readonly optional?: boolean - readonly error?: S.Top - readonly requiredForClient?: boolean -} - -export type RpcDynamic = { - key: Key - settings: A -} - -export type AnyDynamic = { dynamic: RpcDynamic } - -export type DependsOn = { - readonly dependsOn: NonEmptyReadonlyArray | undefined -} - -export interface RpcOptionsDynamic extends RpcOptionsOriginal { - readonly dynamic: RpcDynamic - readonly dependsOn?: NonEmptyReadonlyArray | undefined -} - -export type Dynamic = Options extends RpcOptionsDynamic ? true : false - -export interface RpcMiddlewareDynamic extends RpcMiddleware.RpcMiddleware {} - -export interface TagClassAny extends RpcMiddleware.AnyService { - readonly optional: boolean - readonly provides: any - readonly requires: any - readonly error: S.Top - readonly dynamic?: RpcDynamic | undefined - readonly dependsOn?: NonEmptyReadonlyArray | undefined -} - -export declare namespace TagClass { - /** - * @since 1.0.0 - * @category models - */ - export type FailureSchema = Options extends { readonly error: S.Top; readonly optional?: false } - ? Options["error"] - // actually not, the Failure depends on Dynamic Middleware Configuration! - // : Options extends { readonly dynamic: RpcDynamic } ? A["error"] - : typeof S.Never - - /** - * @since 1.0.0 - * @category models - */ - export type Failure = Options extends { readonly error: S.Codec; readonly optional?: false } ? _A - // actually not, the Failure depends on Dynamic Middleware Configuration! - : Options extends { readonly dynamic: RpcDynamic } ? A["error"]["Type"] - : never - - /** - * @since 1.0.0 - * @category models - */ - // Avoid `S.Codec.DecodingServices = X extends Top ? X["DecodingServices"] : never`: - // tsgo fails to reduce it here and leaves `unknown`. `FailureSchema` is - // always `S.Top` (either `Options["error"]` or `typeof S.Never`), so read directly. - export type FailureContext = FailureSchema["DecodingServices"] - - /** - * @since 1.0.0 - * @category models - */ - export type FailureService = Optional extends true ? unknown : Failure - - /** - * @since 1.0.0 - * @category models - */ - export type Optional = Options extends { readonly optional: true } ? true : false - - /** - * @since 1.0.0 - * @category models - */ - export type RequiredForClient = Options extends { readonly requiredForClient: true } ? true : false - - /** - * @since 1.0.0 - * @category models - */ - export interface Base< - Self, - Name extends string, - Options, - Service, - Config extends { - requires?: any - provides?: any - } - > extends Context.Service { - new(_: never): Context.ServiceClass.Shape - readonly [TypeId]: TypeId - readonly optional: Optional - readonly error: FailureSchema - readonly "~ClientError": Options extends { readonly clientError: infer CE } ? CE : never - readonly provides: "provides" extends keyof Config ? Config["provides"] : never - readonly requires: "requires" extends keyof Config ? Config["requires"] : never - readonly dynamic: Options extends RpcOptionsDynamic ? Options["dynamic"] - : undefined - readonly dependsOn: Options extends DependsOn ? Options["dependsOn"] : undefined - readonly requiredForClient: RequiredForClient - } -} - -export interface TagClass< - Self, - Name extends string, - Options, - Config extends { - requires?: any - provides?: any - } = { requires: never; provides: never } -> extends - TagClass.Base< - Self, - Name, - Options, - Options extends RpcOptionsDynamic ? RpcMiddlewareDynamic< - TagClass.FailureService, - "requires" extends keyof Config ? Config["requires"] : never, - { [K in Options["dynamic"]["key"]]?: Options["dynamic"]["settings"]["contextActivation"] } - > - : RpcMiddlewareV4< - "provides" extends keyof Config ? Config["provides"] : never, - TagClass.Failure, - "requires" extends keyof Config ? Config["requires"] : never - >, - Config - > -{} - -export const Tag = < - Self, - Config extends { - requires?: any - provides?: any - } = { requires: never; provides: never } ->() => -< - const Name extends string, - const Options extends RpcOptionsOriginal | RpcOptionsDynamic = {} ->( - id: Name, - options?: Options -): TagClass => - class extends RpcMiddleware.Service()(id, options as any) { - static readonly requires: "requires" extends keyof Config ? Config["requires"] : never - static readonly provides: "provides" extends keyof Config ? Config["provides"] : never - static readonly dynamic = options && "dynamic" in options ? options.dynamic : undefined - static readonly dependsOn = options && "dependsOn" in options ? options.dependsOn : undefined - } as any - -// not needed if there's official support in Rpc.Rpc. -export type AddMiddleware = R extends Rpc.Rpc< - infer _Tag, - infer _Payload, - infer _Success, - infer _Error, - infer _Middleware -> ? - & Rpc.Rpc< - _Tag, - _Payload, - _Success, - _Error, - _Middleware | Middleware - > - & { readonly config: R extends { readonly config: infer _C } ? _C : never } - : never - -// customized versions to handle dynamically eliminated context. -export type HandlersContext = keyof Handlers extends infer K - ? K extends keyof Handlers & string ? HandlerContext : never - : never - -export type HandlerContext = [Rpc.IsStream] extends - [true] ? Handler extends (...args: any) => - | Stream.Stream - | Rpc.Wrapper> - | Effect.Effect< - infer _A, - infer _EX, - infer _R - > - | Rpc.Wrapper< - Effect.Effect< - infer _A, - infer _EX, - infer _R - > - > ? Exclude, Scope.Scope> - : never - : Handler extends ( - ...args: any - ) => Effect.Effect | Rpc.Wrapper> - ? ExcludeProvides<_R, Rpcs, K> - : never - -// new -export type ExtractDynamicallyProvides = R extends - Rpc.Rpc ? _Middleware extends { - readonly requestContextMap: infer _RC - } ? _RC extends Record // ? GetEffectContext<_RC, { allowAnonymous: false }> - ? R extends { readonly config: infer _C } ? GetEffectContext<_RC, _C> - : GetEffectContext<_RC, {}> - : never - : never - : never - -export type ExtractProvides = R extends - Rpc.Rpc ? _Middleware extends { - readonly provides: infer _P - } ? [_P] extends [never] ? never - : _P /*_P extends Context.Service ? _I - : never */ - : never - : never - -export type ExcludeProvides = Exclude< - Env, - // customisation is down here. - ExtractProvides | ExtractDynamicallyProvides -> diff --git a/packages/effect-app/src/runtime.ts b/packages/effect-app/src/runtime.ts deleted file mode 100644 index a8b004539..000000000 --- a/packages/effect-app/src/runtime.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as Exit from "effect/Exit" -import { flow } from "effect/Function" -import * as Logger from "effect/Logger" -import * as ManagedRuntime from "effect/ManagedRuntime" -import { CauseException } from "./client/errors.ts" -import { type Context } from "./Context.ts" -import * as Effect from "./Effect.ts" -import * as Layer from "./Layer.ts" - -export const makeAppRuntime = Effect.fnUntraced(function*(layer: Layer.Layer) { - const l = layer.pipe( - Layer.provide(Logger.layer([Logger.consolePretty()])), - Layer.orDie - ) - const mrt = ManagedRuntime.make(l) - yield* mrt.contextEffect - return Object.assign(mrt, { - [Symbol.dispose]() { - return Effect.runSync(mrt.disposeEffect) - }, - - [Symbol.asyncDispose]() { - return mrt.dispose() - } - }) // as we initialise here, there is no more error left. -}) - -export function initializeSync(layer: Layer.Layer) { - const runtime = Effect.runSync(makeAppRuntime(layer)) - return runtime -} - -export function initializeAsync(layer: Layer.Layer) { - return Effect - .runPromise(makeAppRuntime(layer)) -} - -// we wrap into CauseException because we want to keep the full cause of the failure. -export const makeRunPromise = (services: Context) => - flow(Effect.runPromiseExitWith(services), (_) => - _.then( - Exit.match({ - onFailure: (cause) => Promise.reject(new CauseException(cause, "runPromise")), - onSuccess: (value) => Promise.resolve(value) - }) - )) - -export const makeRunSync = (services: Context) => - flow( - Effect.runSyncExitWith(services), - Exit.match({ - onFailure: (cause) => { - throw new CauseException(cause, "runSync") - }, - onSuccess: (value) => value - }) - ) diff --git a/packages/effect-app/src/setupRequest.ts b/packages/effect-app/src/setupRequest.ts deleted file mode 100644 index a4c02eda7..000000000 --- a/packages/effect-app/src/setupRequest.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { SqlClient } from "effect/sql" -import * as Tracer from "effect/Tracer" -import { DataDependencyRecorder } from "./DataDependencies.ts" -import * as Effect from "./Effect.ts" -import * as Layer from "./Layer.ts" -import * as Option from "./Option.ts" -import { LocaleRef, RequestContext, spanAttributes } from "./RequestContext.ts" -import * as RequestScopedDependencies from "./RequestScopedDependencies.ts" -import { NonEmptyString255 } from "./Schema.ts" -import { ContextMapContainer, storeId } from "./Store.ts" - -const withSqlTransaction = (self: Effect.Effect): Effect.Effect => - Effect.serviceOption(SqlClient.SqlClient).pipe( - Effect.flatMap(Option.match({ - onNone: () => self, - onSome: (sql) => sql.withTransaction(self).pipe(Effect.orDie) - })) - ) - -export const getRequestContext = Effect - .all({ - span: Effect.currentSpan.pipe(Effect.orDie), - locale: LocaleRef, - namespace: storeId - }) - .pipe( - Effect.map(({ locale, namespace, span }) => - RequestContext.make({ - span: Tracer.externalSpan(span), - locale, - namespace, - name: NonEmptyString255(span.name) - }) - ) - ) - -export const getRC = Effect.all({ - locale: LocaleRef, - namespace: storeId -}) - -const withRequestSpan = (name = "request", options?: Tracer.SpanOptions) => (f: Effect.Effect) => - Effect.andThen( - getRC, - (ctx) => - f.pipe( - Effect.withSpan(name, { - ...options, - attributes: { ...spanAttributes({ ...ctx, name: NonEmptyString255(name) }), ...options?.attributes } - }, { - captureStackTrace: options?.captureStackTrace ?? false - }), - // TODO: false - // request context info is picked up directly in the logger for annotations. - Effect.withLogSpan(name) - ) - ) - -export interface SetupRequestOptions { - readonly withTransaction?: boolean -} - -export const requestStateLayer = RequestScopedDependencies.layer(ContextMapContainer, DataDependencyRecorder) - -// Build `layer` against the ambient (request) scope rather than a sub-scope of the -// returned Effect. Required when the returned value is a streaming HttpServerResponse: -// the response body keeps producing chunks (and using layer-provided state) after the -// Effect returns, so a sub-scope would close too early and run finalizers mid-stream. -export const provideOnRequestScope = - (layer: Layer.Layer) => (self: Effect.Effect) => - Effect.gen(function*() { - const requestScope = yield* Effect.scope - // Fresh MemoMap per request: `Layer.buildWithScope` would otherwise reuse - // the ambient MemoMap living on the HTTP server fiber, sharing the built - // value (e.g. ContextMap) across every request handled by that server. - const memoMap = yield* Layer.makeMemoMap - const ctx = yield* Layer.buildWithMemoMap(layer, memoMap, requestScope) - return yield* Effect.provide(self, ctx) - }) - -export const setupRequestContextFromCurrent = - (name = "request", options?: Tracer.SpanOptions & SetupRequestOptions) => (self: Effect.Effect) => - self - .pipe( - options?.withTransaction === true ? withSqlTransaction : (_) => _, - withRequestSpan(name, options), - Effect.provide(requestStateLayer, { local: true }) - ) - -// Streaming variant: binds ContextMapContainer to the ambient (request) scope so its -// finalizer (clear()) runs only after the response body is fully drained, not when the -// outer Effect returns its HttpServerResponse value. Use for handlers that return a -// streaming HttpServerResponse (e.g. SSE) — see RequestContextMiddleware for context. -export const setupStreamingRequestContextFromCurrent = - (name = "request", options?: Tracer.SpanOptions & SetupRequestOptions) => (self: Effect.Effect) => - self.pipe( - options?.withTransaction === true ? withSqlTransaction : (_) => _, - withRequestSpan(name, options), - provideOnRequestScope(requestStateLayer) - ) - -// TODO: consider integrating Effect.withParentSpan -export function setupRequestContext( - self: Effect.Effect, - requestContext: RequestContext, - options?: SetupRequestOptions -) { - const layer = Layer.mergeAll( - requestStateLayer, - Layer.succeed(LocaleRef, requestContext.locale), - Layer.succeed(storeId, requestContext.namespace) - ) - return self - .pipe( - options?.withTransaction === true ? withSqlTransaction : (_) => _, - withRequestSpan(requestContext.name), - Effect.provide(layer, { local: true }) - ) -} - -export function setupRequestContextWithCustomSpan( - self: Effect.Effect, - requestContext: RequestContext, - name: string, - options?: Tracer.SpanOptions & SetupRequestOptions -) { - const layer = Layer.mergeAll( - requestStateLayer, - Layer.succeed(LocaleRef, requestContext.locale), - Layer.succeed(storeId, requestContext.namespace) - ) - return self - .pipe( - options?.withTransaction === true ? withSqlTransaction : (_) => _, - withRequestSpan(name, options), - Effect.provide(layer, { local: true }) - ) -} diff --git a/packages/effect-app/src/toast.ts b/packages/effect-app/src/toast.ts deleted file mode 100644 index d9b674db4..000000000 --- a/packages/effect-app/src/toast.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as Context from "./Context.ts" -import { accessEffectFn } from "./Context.ts" -import * as Effect from "./Effect.ts" -import * as Option from "./Option.ts" - -export type ToastId = string | number -export type ToastOpts = { id?: ToastId; timeout?: number; groupId?: string; requestId?: string } -export type ToastOptsInternal = { id?: ToastId | null; timeout?: number; groupId?: string; requestId?: string } - -export type UseToast = () => { - error: (this: void, message: string, options?: ToastOpts) => ToastId - warning: (this: void, message: string, options?: ToastOpts) => ToastId - success: (this: void, message: string, options?: ToastOpts) => ToastId - info: (this: void, message: string, options?: ToastOpts) => ToastId - dismiss: (this: void, id: ToastId) => void -} - -export class CurrentToastId extends Context.Opaque()("CurrentToastId") {} - -/** fallback to CurrentToastId when available unless id is explicitly set to a value or null */ -export const wrap = (toast: ReturnType) => { - const wrap = (toastHandler: (message: string, options?: ToastOpts) => ToastId) => { - return (message: string, options?: ToastOptsInternal) => - Effect.serviceOption(CurrentToastId).pipe( - Effect.flatMap((currentToast) => - Effect.sync(() => { - const { id: _id, ...rest } = options ?? {} - const id = _id !== undefined - ? _id ?? undefined - : Option.getOrUndefined(Option.map(currentToast, (_) => _.toastId)) - // when id is undefined, we may end up with no toast at all.. - return toastHandler(message, id !== undefined ? { ...rest, id } : rest) - }) - ) - ) - } - return { - error: wrap(toast.error), - info: wrap(toast.info), - success: wrap(toast.success), - warning: wrap(toast.warning), - dismiss: (toastId: ToastId) => Effect.sync(() => toast.dismiss(toastId)) - } -} - -type ToastShape = ReturnType - -export class Toast extends Context.Opaque()("Toast") { - static readonly error = accessEffectFn(this, "error") - static readonly info = accessEffectFn(this, "info") - static readonly success = accessEffectFn(this, "success") - static readonly warning = accessEffectFn(this, "warning") - static readonly dismiss = accessEffectFn(this, "dismiss") -} diff --git a/packages/effect-app/src/transform.ts b/packages/effect-app/src/transform.ts deleted file mode 100644 index 0855cb3a0..000000000 --- a/packages/effect-app/src/transform.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { NonEmptyReadonlyArray } from "effect/Array" -import * as Option from "effect/Option" -import type { Misc, Union } from "ts-toolbelt" -import type * as SET from "./Set.ts" - -// type SomeObject = { -// 0: Option.Option -// a: { -// b: Option.Option -// g: Option.Option> -// h: Option.Option<{ i: Option.Option }> -// } -// c: { d: Array }>> } -// } -// type test0 = Transform -// type test1 = Transform - -type OptOf = Union.Exclude< - A extends Option.Some ? X | null : A, - Option.None -> -// eslint-disable-next-line @typescript-eslint/no-explicit-any - -export type TransformRoot = O extends Option.Option ? Transform> - : Transform -export type Transform = O extends Misc.BuiltIn | Misc.Primitive ? O - : { - [K in keyof O]: OptOf extends infer X ? X extends (infer Y)[] ? OptOf>[] - : X extends NonEmptyReadonlyArray ? NonEmptyReadonlyArray>> - : X extends SET.Set ? SET.Set>> - : X extends readonly (infer Y)[] ? readonly OptOf>[] - : Transform - : never - } - -export const encodeOptsAsNullable = (root: T): TransformRoot => encodeOptsAsNullable_(root, new Map()) - -const encodeOptsAsNullable_ = (value: any, cacheMap: Map): any => { - const cacheEntry = cacheMap.get(value) - if (cacheEntry) { - return cacheEntry - } - - if (Array.isArray(value)) { - const newAr: typeof value = [] - cacheMap.set(value, newAr) - value.forEach((x) => newAr.push(encodeOptsAsNullable_(x, cacheMap))) - return newAr - } - - if ( - value instanceof Date - || typeof value === "function" - || (typeof value === "object" && value !== null && "then" in value && typeof value.then === "function") - ) { - return value - } - - if (value instanceof Set) { - const newValue = [...value] - cacheMap.set(value, newValue) - return newValue - } - - if (value instanceof Object) { - if (value._tag === "Some" || value._tag === "None") { - const v = value as Option.Option - return encodeOptsAsNullable_(Option.getOrNull(v), cacheMap) - } - const newObj = {} as Record - cacheMap.set(value, newObj) - - Object.keys(value).forEach((key) => { - newObj[key] = encodeOptsAsNullable_(value[key], cacheMap) - }) - return newObj - } - return value -} diff --git a/packages/effect-app/src/utils.ts b/packages/effect-app/src/utils.ts deleted file mode 100644 index 6c96e8f55..000000000 --- a/packages/effect-app/src/utils.ts +++ /dev/null @@ -1,884 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ -import * as Cause from "effect/Cause" -import * as Effect from "effect/Effect" -import * as Exit from "effect/Exit" -import * as Fiber from "effect/Fiber" -import { dual } from "effect/Function" -import * as Option from "effect/Option" -import { isFunction } from "effect/Predicate" -import * as Record from "effect/Record" -import * as Result from "effect/Result" -import { identity, pipe } from "./Function.ts" -import type { DeepMutable, Equals, Mutable } from "./Types.ts" - -// codegen:start {preset: barrel, include: ./utils/*.ts, nodir: false } -export * from "./utils/effectify.ts" -export * from "./utils/extend.ts" -export * from "./utils/gen.ts" -export * from "./utils/logger.ts" -export * from "./utils/logLevel.ts" -// codegen:end - -export * from "effect/Utils" - -export const cloneTrait = Symbol.for("clone-trait") -export interface Clone { - [cloneTrait](this: this, that: any): this -} - -export const unsafeRight = (ei: Result.Result) => { - if (Result.isFailure(ei)) { - console.error(ei.failure) - throw ei.failure - } - return ei.success -} - -export const unsafeSome = (makeErrorMessage: () => string) => (o: Option.Option) => { - if (Option.isNone(o)) { - throw new Error(makeErrorMessage()) - } - return o.value -} - -export function toString(v: unknown) { - return `${v}` -} - -export type Constructor = { new(...args: any[]): T } -export type ThenArg = T extends Promise ? U - : T extends (...args: any[]) => Promise ? V - : T - -/** - * @deprecated TODO: does not filter optional fields! - */ -type NoUndefinedField = { [P in keyof T]: Exclude } -export function dropUndefinedT>( - input: A -): NoUndefinedField { - const newR = pipe( - input, - Record.filter((x): x is A => x !== undefined) - ) - return newR as any -} - -type IsOptional, K extends keyof T> = {} extends Pick ? true : false - -type WithUndefined> = { - [K in keyof T]: IsOptional extends true ? T[K] | undefined : T[K] -} - -type WithUndefinedRecursive> = { - [K in keyof T]: WithUndefinedRecursive extends infer $RTK - ? IsOptional extends true ? $RTK | undefined : $RTK - : never -} - -/** a version of @see dropUndefinedT that keeps support for go to definition, auto completion, and documentation of fields! */ -export function dropUndefinedT2>() { - return ( - input: WithUndefined - ): Desired => { - const newR = pipe( - input as any, - Record.filter((x): x is Desired => x !== undefined) - ) - return newR as any - } -} - -/** - * A recursive version of @see dropUndefinedT that preserves IDE support for - * go-to-definition, auto-completion, and field documentation. - * - * Note: This utility does not physically remove `undefined` fields at runtime. - * It provides type-level compatibility with libraries that use `prop?: type` - * syntax. Before the introduction of `exactOptionalPropertyTypes`, this syntax - * implicitly allowed `undefined` values. Libraries that adopted `prop?: type` - * before this flag was introduced still accept `undefined` at runtime. - */ - -export function dropUndefinedRec>() { - return ( - input: WithUndefinedRecursive - ): Desired => { - // doesn't drop for real - return input as any - } -} - -export type Dictionary = { - readonly [P in string]: T -} - -export function dropUndefined( - input: Dictionary -): Dictionary { - const newR = pipe( - input, - Record.filter((x): x is A => x !== undefined) - ) - return newR -} - -type GetTag = T extends { _tag: infer K } ? K : never -export const isOfType = (tag: GetTag) => (e: { _tag: string }): e is T => e._tag === tag - -export function pretty(o: unknown): string { - return JSON.stringify(o, undefined, 2) ?? "undefined" -} - -export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( - k: infer I -) => void ? I - : never - -// generally A | B is not assignable to A & B -export type IsUnion = [T] extends [UnionToIntersection] ? false : true - -export type EnforceNonEmptyRecord = keyof R extends never ? never : R - -export function intersect( - ...as: AS -): UnionToIntersection<{ [k in keyof AS]: AS[k] }[number]> { - return as.reduce((a: any, b: any) => Object.assign(a, b), {}) as any -} - -export type IsEqualTo = (() => T extends X ? 1 : 2) extends < - T ->() => T extends Y ? 1 : 2 ? true - : false - -export const unifyIndex = Symbol() -export type unifyIndex = typeof unifyIndex - -export interface UnifiableIndexed<_X> {} -export type UnifiableIndexedURI = keyof UnifiableIndexed - -export interface Unifiable { - // Sync: [X] extends [Sync] ? Sync : never - // Effect: [X] extends [Effect] - // ? [X] extends [Sync] - // ? never - // : Effect.Effect - // : never - Unify: [X] extends [{ readonly [unifyIndex]: infer K }] ? K extends UnifiableIndexedURI ? UnifiableIndexed[K] - : never - : never -} - -export type Unify = Unifiable[keyof Unifiable] extends never ? X - : Unifiable[keyof Unifiable] - -// forked from https://github.com/Alorel/typescript-lazy-get-decorator - -type DecoratorReturn = PropertyDescriptor | NewDescriptor - -function decorateNew( - inp: NewDescriptor, - setProto: boolean, - makeNonConfigurable: boolean, - resultSelector: ResultSelectorFn -): NewDescriptor { - const out: NewDescriptor = Object.assign({}, inp) - if (out.descriptor) { - out.descriptor = Object.assign({}, out.descriptor) - } - const actualDesc: PropertyDescriptor = out.descriptor || /* istanbul ignore next */ out - - const originalMethod = validateAndExtractMethodFromDescriptor(actualDesc) - const isStatic = inp.placement === "static" - - actualDesc.get = function(this: any): any { - return getterCommon( - isStatic ? this : Object.getPrototypeOf(this), - out.key, - isStatic, - !!actualDesc.enumerable, - originalMethod, - this, - // eslint-disable-next-line prefer-rest-params - arguments, - setProto, - makeNonConfigurable, - resultSelector - ) - } - - return out -} - -function decorateLegacy( - target: any, - key: PropertyKey, - descriptor: PropertyDescriptor | undefined, - setProto: boolean, - makeNonConfigurable: boolean, - // tslint:enable:bool-param-default - resultSelector: ResultSelectorFn -): PropertyDescriptor { - /* istanbul ignore if */ - if (!descriptor) { - descriptor = Object.getOwnPropertyDescriptor(target, key) - if (!descriptor) { - const e = Object.assign(new Error("@LazyGetter is unable to determine the property descriptor"), { - $key: key, - $target: target - }) - throw e - } - } - - const originalMethod = validateAndExtractMethodFromDescriptor(descriptor) - - return Object.assign({}, descriptor, { - get(this: any): any { - return getterCommon( - target, - key, - Object.getPrototypeOf(target) === Function.prototype, - !!descriptor.enumerable, - originalMethod, - this, - // eslint-disable-next-line prefer-rest-params - arguments, - setProto, - makeNonConfigurable, - resultSelector - ) - } - }) -} - -/** Signifies that the modified property descriptor can be reset to its original state */ -interface ResettableDescriptor { - /** - * Restore the property descriptor on the given class instance or prototype and re-apply the lazy getter. - * @param on The class instance or prototype - */ - reset(on: any): void -} - -/** ES7 proposal descriptor, tweaked for Babel */ -interface NewDescriptor extends PropertyDescriptor { - descriptor?: PropertyDescriptor - - key: PropertyKey - - kind: string - - placement: string -} - -/** A filter function that must return true for the value to cached */ -export type ResultSelectorFn = (v: any) => boolean - -function defaultFilter(): boolean { - return true -} - -function validateAndExtractMethodFromDescriptor(desc: PropertyDescriptor): Function { - // eslint-disable-next-line @typescript-eslint/unbound-method - const originalMethod = desc.get - - if (!originalMethod) { - throw new Error("@LazyGetter can only decorate getters!") - } else if (!desc.configurable) { - throw new Error("@LazyGetter target must be configurable") - } - - return originalMethod -} - -function getterCommon( // tslint:disable-line:parameters-max-number - target: any, - key: PropertyKey, - isStatic: boolean, - enumerable: boolean, - originalMethod: Function, - thisArg: any, - args: IArguments, - setProto: boolean, - makeNonConfigurable: boolean, - resultSelector: ResultSelectorFn -): any { - const value = originalMethod.apply(thisArg, args) - - if (resultSelector(value)) { - const newDescriptor: PropertyDescriptor = { - configurable: !makeNonConfigurable, - enumerable, - value - } - - if (isStatic || setProto) { - Object.defineProperty(target, key, newDescriptor) - } - - if (!isStatic) { - Object.defineProperty(thisArg, key, newDescriptor) - } - } - - return value -} - -/** - * Evaluate the getter function and cache the result - * @param [setProto=false] Set the value on the class prototype as well. Only applies to non-static getters. - * @param [makeNonConfigurable=false] Set to true to make the resolved property non-configurable - * @param [resultSelector] A filter function that must return true for the value to cached - * @return A decorator function - */ -export function LazyGetter( - setProto = false, - makeNonConfigurable = false, - resultSelector: ResultSelectorFn = defaultFilter -): MethodDecorator & ResettableDescriptor { - let desc: PropertyDescriptor - let prop: PropertyKey - let args: [targetOrDesc: any, key: PropertyKey | undefined, descriptor: PropertyDescriptor | undefined] | undefined - let isLegacy: boolean - - function decorator( - targetOrDesc: any, - key: PropertyKey | undefined, - descriptor: PropertyDescriptor | undefined - ): DecoratorReturn { - args = [targetOrDesc, key, descriptor] - if (key === undefined) { - if (typeof desc === "undefined") { - const newDescriptor = targetOrDesc as NewDescriptor - isLegacy = false - prop = newDescriptor.key - desc = Object.assign( - {}, - newDescriptor.descriptor - /* istanbul ignore next */ || targetOrDesc - ) - } - - return decorateNew(targetOrDesc, setProto, makeNonConfigurable, resultSelector) - } else { - if (typeof desc === "undefined") { - isLegacy = true - prop = key - desc = Object.assign( - {}, - descriptor - /* istanbul ignore next */ || Object.getOwnPropertyDescriptor( - targetOrDesc, - key - ) - ) - } - - return decorateLegacy( - targetOrDesc, - key, - descriptor, - setProto, - makeNonConfigurable, - resultSelector - ) - } - } - - decorator.reset = setProto - ? thrower - : (on: any): void => { - if (!on) { - throw new Error("Unable to restore descriptor on an undefined target") - } - if (!desc) { - throw new Error( - "Unable to restore descriptor. Did you remember to apply your decorator to a method?" - ) - } - if (!args) { - throw new Error("Unable to restore descriptor before applying the decorator.") - } - // Restore descriptor to its original state - Object.defineProperty(on, prop, desc) - const ret = decorator.apply(null, args) - const restoredDescriptor = isLegacy ? ret : "descriptor" in ret && ret.descriptor ? ret.descriptor : ret - Object.defineProperty(on, prop, restoredDescriptor) - } - - return decorator -} - -function thrower(): never { - throw new Error("This decoration modifies the class prototype and cannot be reset.") -} - -export type RefinementWithIndex = (i: I, a: A) => a is B - -export type PredicateWithIndex = (i: I, a: A) => boolean - -export type Erase = R & K extends K & infer R1 ? R1 : R - -/** from ts-toolbelt, minimal port of Compute */ - -export type Depth = "flat" | "deep" - -type Errors = Error -// | EvalError -// | RangeError -// | ReferenceError -// | SyntaxError -// | TypeError -// | URIError - -type Numeric = - // | Number - // | BigInt // not needed - // | Math - Date - -type Textual = - // | String - RegExp - -type Arrays = - // | Array - // | ReadonlyArray - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array -// | BigInt64Array -// | BigUint64Array - -type Maps = - // | Map - // | Set - | ReadonlyMap - | ReadonlySet - | WeakMap - | WeakSet - -type Structures = - | ArrayBuffer - // | SharedArrayBuffer - // | Atomics - | DataView -// | JSON - -type Abstractions = Function | Promise | Generator -// | GeneratorFunction - -type WebAssembly = never - -export type BuiltInObject = - | Errors - | Numeric - | Textual - | Arrays - | Maps - | Structures - | Abstractions - | WebAssembly - -export type ComputeRaw = A extends Function ? A - : - & { - [K in keyof A]: A[K] - } - & {} - -export type ComputeFlat = A extends BuiltInObject ? A - : - & { - [K in keyof A]: A[K] - } - & {} - -export type ComputeDeep = A extends BuiltInObject ? A - : - & { - [K in keyof A]: ComputeDeep - } - & {} - -export type Compute = { - flat: ComputeFlat - deep: ComputeDeep -}[depth] - -///// - -export const LazySymbol = Symbol("lazy") - -interface Lazy { - [LazySymbol]: Record -} - -export function lazyGetter(creator: (target: T) => T2) { - const key = Symbol(creator.name) - const f = (target: T): T2 => { - let lazy = (target as unknown as Lazy)[LazySymbol] - if (!lazy) { - lazy = {} - Object.defineProperty(target, LazySymbol, { enumerable: false, value: lazy }) - } else if (lazy[key]) { - return lazy[key] - } - const value = creator(target) - lazy[key] = value - return value - } - Object.defineProperty(f, "name", { - enumerable: false, - value: `Lazy<${creator.name}>` - }) - return f -} - -export function exhaustiveMatch() { - return any>>(handlers: Out) => (t: T): ReturnType => handlers[t](t) -} - -export function exhaustiveMatch_(t: T) { - return any>>(handlers: Out): ReturnType => handlers[t](t) -} - -export type OptPromise any> = ( - ...args: Parameters -) => Promise> | ReturnType - -export function access(t: Record) { - return (key: T) => t[key] -} - -export function todayAtUTCNoon() { - const localDate = new Date() - const utcDateAtNoon = Date.UTC( - localDate.getFullYear(), - localDate.getMonth(), - localDate.getDate(), - 12 - ) - return new Date(utcDateAtNoon) -} - -function anyOp_$(self: T) { - return { - get subject() { - return self - } - } -} - -export function anyOp$(self: T): AnyOps { - return anyOp_$(self) -} - -export interface AnyOps { - subject: T -} - -export const clone = dual< - (f: NoInfer) => (self: A) => A, - (self: A, f: A) => A ->(2, (self, f) => { - if (cloneTrait in (self as any)) { - const selfWithClone = self as typeof self & Clone - return selfWithClone[cloneTrait](f) - } - return Object.setPrototypeOf(f, Object.getPrototypeOf(self)) as typeof self -}) - -export const copy = dual< - { - (f: (a: A) => Partial>): (self: A) => A - (f: Partial>): (self: A) => A - }, - { - (self: A, f: (a: A) => Partial): A - (self: A, f: Partial): A - } ->(2, (self: A, f: Partial | ((a: A) => Partial)) => clone(self, { ...self, ...(isFunction(f) ? f(self) : f) })) - -export type CopyOriginU any> = - & { - [K in keyof U & keyof InstanceType]?: U[K] - } - & {} - -export type CopyOriginRet = - & { - [K in keyof A | keyof U]: K extends keyof U ? U[K] : A[K & keyof A] - } - & {} - -export type CopyOriginSelf = Equals<{}, U> extends true - ? Equals extends true ? `updates argument is empty or contains only extra properties` - : A - : A - -export interface StructuralCopyOrigin { - >( - f: (a: A) => - & { - [K in keyof U & keyof Self]?: U[K] - } - & {} - ): (self: CopyOriginSelf) => CopyOriginRet - >( - updates: - & { - [K in keyof U & keyof Self]?: U[K] - } - & {} - ): (self: CopyOriginSelf) => CopyOriginRet - >( - self: CopyOriginSelf, - f: (a: A) => - & { - [K in keyof U & keyof Self]?: U[K] - } - & {} - ): CopyOriginRet - >( - self: CopyOriginSelf, - updates: - & { - [K in keyof U & keyof Self]?: U[K] - } - & {} - ): CopyOriginRet -} - -// just one input param: the convention is that the ctor takes an object -// containing the properties of the value (I can't put object there as type because of contravariance) -/** - * By design this does not return `Self` directly. - * - * The return type is computed from `Self` and the update payload so callers can - * expose an explicit structural return type that remains assignable to `Self`. - */ -export const copyOrigin = any>(ctor: Ctor) => - dual< - { - , U extends Partial>>( - f: (a: A) => CopyOriginU - ): (self: CopyOriginSelf) => CopyOriginRet - , U extends Partial>>( - updates: CopyOriginU - ): (self: CopyOriginSelf) => CopyOriginRet - }, - { - , U extends Partial>>( - self: CopyOriginSelf, - f: (a: A) => CopyOriginU - ): CopyOriginRet - , U extends Partial>>( - self: CopyOriginSelf, - updates: CopyOriginU - ): CopyOriginRet - } - >( - 2, - , U extends Partial>>( - self: Equals<{}, U> extends true ? `updates argument is empty or contains only extra properties` : A, - f: - | CopyOriginU - | ((a: A) => CopyOriginU) - ): CopyOriginRet => { - const o = { ...self, ...(isFunction(f) ? f(self as any) : f) } - - if (cloneTrait in (self as any)) { - const selfWithClone = self as typeof self & Clone - return selfWithClone[cloneTrait](o) - } - - return new ctor(o) - } - ) - -export function debug(a: AnyOps, name: string) { - let r: string | A = a.subject - try { - r = pretty(a.subject) - } catch { /* empty */ } - return Effect - .logDebug("print") - .pipe( - Effect.annotateLogs(name, `${r}`), - Effect - .map(() => a.subject) - ) -} - -export function debugUnsafe(a: AnyOps, name: string) { - console.log(name, a.subject) - return a.subject -} - -export function spread< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Fields extends Record, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - NewProps ->(fields: Fields, fnc: (fields: Fields) => NewProps) { - return fnc(fields) -} - -export function spreadS< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Fields extends Record ->(fields: Fields, fnc: (fields: Fields) => Fields) { - return fnc(fields) -} - -export function makeAzureFriendly(path: string) { - return path.replace(/\//g, "___SL@SH___") -} - -export function undoAzureFriendly(path: T): T { - return path.replace(/___SL@SH___/g, "/") as T -} - -export function arrayMove( - arrInput: readonly T[], - oldIndex: number, - newIndex: number -) { - const arr: (T | undefined)[] = [...arrInput] - while (oldIndex < 0) { - oldIndex += arr.length - } - while (newIndex < 0) { - newIndex += arr.length - } - if (newIndex >= arr.length) { - let k = newIndex - arr.length + 1 - while (k--) { - arr.push(undefined) - } - } - arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]) - return arr -} - -export function arrayMoveDropUndefined( - arrInput: readonly (T | undefined)[], - oldIndex: number, - newIndex: number -): T[] { - return arrayMove(arrInput, oldIndex, newIndex).filter((x): x is T => x !== undefined) -} - -export function arMoveElDropUndefined(el: T, newIndex: number) { - return (arrInput: ReadonlyArray): Option.Option> => { - const ar = [...arrInput] - const index = ar.findIndex((x) => x === el) - if (index === -1) { - return Option.none() - } - return Option.some(arrayMoveDropUndefined(ar, index, newIndex)) - } -} - -export function setMoveElDropUndefined(el: T, newIndex: number) { - return (arrInput: ReadonlySet): Option.Option> => - pipe([...arrInput], arMoveElDropUndefined(el, newIndex), Option.map((ar) => new Set(ar))) -} - -type RemoveNonArray = T extends readonly any[] ? T : never -export function isNativeTuple(a: A): a is RemoveNonArray { - return Array.isArray(a) -} - -export const mutable: { (a: A, deep: true): DeepMutable; (a: A): Mutable } = identity - -/** - * Recursively removes all elements assignable to type `E` from tuple `T`. - * - * @example - * ```typescript - * type Mixed = [1, "hello", 2, "world", 3] - * type Numbers = ExcludeFromTuple - * // Result: [1, 2, 3] - * ``` - */ -export type ExcludeFromTuple = T extends [infer F, ...infer R] - ? [F] extends [E] ? ExcludeFromTuple - : [F, ...ExcludeFromTuple] - : [] - -export const addAbortToRuntimeFiber = (fiber: Fiber.Fiber, signal: AbortSignal) => { - const abort = () => Effect.runSync(Fiber.interrupt(fiber)) - if (signal.aborted) { - abort() - return fiber - } - signal.addEventListener("abort", abort) - return fiber -} - -export const runtimeFiberAsPromise = (fiber: Fiber.Fiber, signal?: AbortSignal) => { - if (signal) addAbortToRuntimeFiber(fiber, signal) - return new Promise((resolve, reject) => - fiber.addObserver((exit) => { - if (Exit.isSuccess(exit)) { - resolve(exit.value) - } else { - // eslint-disable-next-line - reject(Cause.squash(exit.cause)) - } - }) - ) -} - -// unifies any input to be an effect. -export const wrapEffect = >( - m: ((...args: Args) => A) | ((...args: Args) => Effect.Effect) | I -) => { - if (typeof m === "function") { - return (...args: Args): Effect.Effect => { - const r = (m as any)(...args) - if (Effect.isEffect(r)) return r as Effect.Effect - return Effect.succeed(r) - } - } - return (): Effect.Effect => Effect.succeed(m) -} - -export type UnionToTuples = [T] extends [never] ? [] - : T extends any ? - | [T, ...UnionToTuples>] - | UnionToTuples> - : [] - -const genConstructor = (function*() {}).constructor - -/** - * @example - * ```ts - * import * as Utils from "effect/Utils" - * - * function* generatorFn() { - * yield 1 - * yield 2 - * } - * - * console.log(Utils.isGeneratorFunction(generatorFn)) // true - * console.log(Utils.isGeneratorFunction(() => {})) // false - * ``` - * - * @category predicates - * @since 3.11.0 - */ -export const isGeneratorFunction = (u: unknown): u is (...args: Array) => Generator => - isFunction(u) && u.constructor === genConstructor diff --git a/packages/effect-app/src/utils/effectify.ts b/packages/effect-app/src/utils/effectify.ts deleted file mode 100644 index 2aab5cad9..000000000 --- a/packages/effect-app/src/utils/effectify.ts +++ /dev/null @@ -1,260 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ - -import * as Effect from "effect/Effect" - -interface Callback { - (err: E, a?: A): void -} - -type ArgsWithCallback, E, A> = [...args: Args, cb: Callback] - -type WithoutNull = unknown extends A ? void : Exclude - -type Effectify = T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - (...args: ArgsWithCallback): infer _R8 - (...args: ArgsWithCallback): infer _R9 - (...args: ArgsWithCallback): infer _R10 -} ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - (...args: Args4): Effect.Effect, E> - (...args: Args5): Effect.Effect, E> - (...args: Args6): Effect.Effect, E> - (...args: Args7): Effect.Effect, E> - (...args: Args8): Effect.Effect, E> - (...args: Args9): Effect.Effect, E> - (...args: Args10): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - (...args: ArgsWithCallback): infer _R8 - (...args: ArgsWithCallback): infer _R9 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - (...args: Args4): Effect.Effect, E> - (...args: Args5): Effect.Effect, E> - (...args: Args6): Effect.Effect, E> - (...args: Args7): Effect.Effect, E> - (...args: Args8): Effect.Effect, E> - (...args: Args9): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - (...args: ArgsWithCallback): infer _R8 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - (...args: Args4): Effect.Effect, E> - (...args: Args5): Effect.Effect, E> - (...args: Args6): Effect.Effect, E> - (...args: Args7): Effect.Effect, E> - (...args: Args8): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - (...args: Args4): Effect.Effect, E> - (...args: Args5): Effect.Effect, E> - (...args: Args6): Effect.Effect, E> - (...args: Args7): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - (...args: Args4): Effect.Effect, E> - (...args: Args5): Effect.Effect, E> - (...args: Args6): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - (...args: Args4): Effect.Effect, E> - (...args: Args5): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - (...args: Args4): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - (...args: Args3): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - } ? { - (...args: Args1): Effect.Effect, E> - (...args: Args2): Effect.Effect, E> - } - : T extends { - (...args: ArgsWithCallback): infer _R1 - } ? { - (...args: Args1): Effect.Effect, E> - } - : never - -type CallbackError = T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - (...args: ArgsWithCallback): infer _R8 - (...args: ArgsWithCallback): infer _R9 - (...args: ArgsWithCallback): infer _R10 -} ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - (...args: ArgsWithCallback): infer _R8 - (...args: ArgsWithCallback): infer _R9 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - (...args: ArgsWithCallback): infer _R8 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - (...args: ArgsWithCallback): infer _R7 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - (...args: ArgsWithCallback): infer _R6 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - (...args: ArgsWithCallback): infer _R5 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - (...args: ArgsWithCallback): infer _R4 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - (...args: ArgsWithCallback): infer _R3 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - (...args: ArgsWithCallback): infer _R2 - } ? NonNullable - : T extends { - (...args: ArgsWithCallback): infer _R1 - } ? NonNullable - : never - -export const effectify: { - ) => any>(fn: F): Effectify> - ) => any, E>( - fn: F, - onError: (error: CallbackError, args: Parameters) => E - ): Effectify - ) => any, E, E2>( - fn: F, - onError: (error: CallbackError, args: Parameters) => E, - onSyncError: (error: unknown, args: Parameters) => E2 - ): Effectify -} = - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - ((fn: Function, onError?: (e: any, args: any) => any, onSyncError?: (e: any, args: any) => any) => - (...args: Array) => - Effect.callback((resume) => { - try { - fn(...args, (err: Error | null, result: A) => { - if (err) { - resume(Effect.fail(onError ? onError(err, args) : err)) - } else { - resume(Effect.succeed(result)) - } - }) - } catch (err) { - resume(onSyncError ? Effect.fail(onSyncError(err, args)) : Effect.die(err)) - } - })) as any diff --git a/packages/effect-app/src/utils/extend.ts b/packages/effect-app/src/utils/extend.ts deleted file mode 100644 index b956b3781..000000000 --- a/packages/effect-app/src/utils/extend.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Own-property define that shadows getter-only prototype fields such as - * Effect 4 `Schema.make`. `Object.assign` uses [[Set]] and cannot. - */ -export const defineOwn = (target: object, key: PropertyKey, value: unknown) => { - Object.defineProperty(target, key, { - value, - writable: true, - enumerable: true, - configurable: true - }) -} - -const assignOwn = (target: T, source: object): T => { - for (const key of Reflect.ownKeys(source)) { - const desc = Object.getOwnPropertyDescriptor(source, key) - if (!desc?.enumerable) continue - defineOwn(target, key, (source as Record)[key]) - } - return target -} - -export const extend = (a: T, ext: X) => { - assignOwn(a, ext) - return a as T & X -} - -export const extendM = (a: T, ext: (a: T) => X) => extend(a, ext(a)) diff --git a/packages/effect-app/src/utils/gen.ts b/packages/effect-app/src/utils/gen.ts deleted file mode 100644 index e2bae01ce..000000000 --- a/packages/effect-app/src/utils/gen.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { type Effect } from "effect/Effect" - -export namespace EffectGenUtils { - export type Success = EG extends Effect ? A - // there could be a case where the generator function does not yield anything, so we need to handle that - : EG extends (..._: infer _3) => Generator ? A - // generators yield Effect values - : EG extends (..._: infer _3) => Generator, infer A, infer _2> ? A - : never - - export type Error = EG extends Effect ? E - // there could be a case where the generator function does not yield anything, so we need to handle that - : EG extends (..._: infer _3) => Generator ? never - // generators yield Effect values - : EG extends (..._: infer _3) => Generator, infer _A, infer _2> ? E - : never - - export type Context = EG extends Effect ? R - // there could be a case where the generator function does not yield anything, so we need to handle that - : EG extends (..._: infer _3) => Generator ? never - // generators yield Effect values - : EG extends (..._: infer _3) => Generator, infer _A, infer _2> ? R - : never -} diff --git a/packages/effect-app/src/utils/logLevel.ts b/packages/effect-app/src/utils/logLevel.ts deleted file mode 100644 index dba7d1e27..000000000 --- a/packages/effect-app/src/utils/logLevel.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type * as LogLevel from "effect/LogLevel" - -export const LogLevelToSentry = (level: LogLevel.LogLevel) => { - switch (level) { - case "Debug": - return "debug" as const - case "Info": - return "info" as const - case "Warn": - return "warning" as const - case "Error": - return "error" as const - case "Fatal": - return "fatal" as const - } - return "log" as const -} diff --git a/packages/effect-app/src/utils/logger.ts b/packages/effect-app/src/utils/logger.ts deleted file mode 100644 index 68c0f9ab6..000000000 --- a/packages/effect-app/src/utils/logger.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as Effect from "effect/Effect" -import type * as LogLevel from "effect/LogLevel" -import * as Context from "../Context.ts" - -type Levels = "info" | "debug" | "warn" | "error" - -export class LogLevels extends Context.Reference("LogLevels", { - defaultValue: () => new Map() -}) {} - -export const makeLog = (namespace: string, defaultLevel: Levels = "warn") => { - const level = LogLevels.use((levels) => Effect.succeed(levels.get(namespace) ?? defaultLevel)) - const withLogNamespace = Effect.annotateLogs({ logNamespace: namespace }) - return { - logWarning: (...message: ReadonlyArray) => - Effect.flatMap(level, (l) => - l === "info" || l === "debug" || l === "warn" - ? Effect.logWarning(...message).pipe(withLogNamespace) - : Effect.void), - logError: (...message: ReadonlyArray) => Effect.logError(...message).pipe(withLogNamespace), - logFatal: (...message: ReadonlyArray) => Effect.logFatal(...message).pipe(withLogNamespace), - logInfo: (...message: ReadonlyArray) => - Effect.flatMap( - level, - (l) => l === "info" || l === "debug" ? Effect.logInfo(...message).pipe(withLogNamespace) : Effect.void - ), - logDebug: (...message: ReadonlyArray) => - Effect.flatMap(level, (l) => l === "debug" ? Effect.logDebug(...message).pipe(withLogNamespace) : Effect.void), - // for now always log - logWithLevel: (level: LogLevel.Severity, ...message: ReadonlyArray) => - Effect.logWithLevel(level)(...message).pipe(withLogNamespace) - } -} diff --git a/packages/effect-app/src/validation.ts b/packages/effect-app/src/validation.ts deleted file mode 100644 index 24f9ade4b..000000000 --- a/packages/effect-app/src/validation.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Predicate } from "./Function.ts" - -export const all_ = (v: T, ...a: Predicate[]) => !a.some((x) => !x(v)) -export const all = (...a: Predicate[]) => (v: T) => all_(v, ...a) - -export const maxN = (max: number) => (v: number) => v <= max -export const minN = (min: number) => (v: number) => v >= min - -export const max = (max: number) => { - const f = maxN(max) - return (v: { length: number }) => f(v.length) -} -export const min = (min: number) => { - const f = minN(min) - return (v: { length: number }) => f(v.length) -} - -export * from "./validation/validators.ts" diff --git a/packages/effect-app/src/validation/validators.ts b/packages/effect-app/src/validation/validators.ts deleted file mode 100644 index f2ec83f69..000000000 --- a/packages/effect-app/src/validation/validators.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Not importing from /es/lib because they are .js files and bundlers probably assume they're cjs :( -import isEmail, { type IsEmailOptions } from "validator/lib/isEmail.js" - -// Source https://emailregex.com/ -// eslint-disable-next-line no-control-regex -// const EMAIL_REGEX = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/ - -// allow for "/", "+", "-", " .js" and digits -const PHONE_REGEX = /^\+?[\d-/ ]*$/ - -const PHONE_LENGTH = 7 - -/** - * Validate emails according to RFC 5322 - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isValidEmail = isEmail as any as (str: string, options?: IsEmailOptions) => boolean - -/** - * Validates that phone number contains at least 7 numbers. - * Allowed characters are [0-1] "/", "+", "-", " .js" - */ -export const isValidPhone = (phone: string): boolean => { - const isValidPhone = PHONE_REGEX.test(phone) - if (!isValidPhone) return false - - const isValidLength = phone.split("").filter((c) => !isNaN(Number(c))).length >= PHONE_LENGTH - if (!isValidLength) return false - - return true -} diff --git a/packages/effect-app/src/waitingGate.ts b/packages/effect-app/src/waitingGate.ts deleted file mode 100644 index a5bf0096d..000000000 --- a/packages/effect-app/src/waitingGate.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as Context from "./Context.ts" -import * as Effect from "./Effect.ts" - -/** - * Notified with the suspension depth after each change. `0` means the command - * is doing work. A positive depth means it is parked on a caller. - */ -export type WaitingGateController = (depth: number) => Effect.Effect - -export interface WaitingGateShape { - /** `false` for the default gate. Suspension then changes nothing. */ - readonly active: boolean - readonly depth: number - readonly add: (delta: number) => Effect.Effect - /** Stack a controller. The returned function restores the previous one. */ - readonly bind: (controller: WaitingGateController) => () => void -} - -const inactiveWaitingGate: WaitingGateShape = { - active: false, - depth: 0, - add: () => Effect.void, - bind: () => () => {} -} - -/** - * Per-command gate for {@link suspendWaiting}. Commands provide an active gate. - * The default is inactive, so the helper is a no-op outside a command. - */ -export const WaitingGate = Context.Reference("effect-app/WaitingGate", { - defaultValue: () => inactiveWaitingGate -}) - -/** Build the gate one command owns. `onDepth` runs synchronously inside `add`. */ -export const makeWaitingGate = (onDepth?: (depth: number) => void): WaitingGateShape => { - let depth = 0 - let controller: WaitingGateController | undefined - return { - active: true, - get depth() { - return depth - }, - add: (delta) => - Effect.uninterruptible(Effect.gen(function*() { - depth += delta - if (depth < 0) depth = 0 - const next = depth - onDepth?.(next) - if (controller) yield* controller(next) - })), - bind: (next) => { - const previous = controller - controller = next - return () => { - if (controller === next) controller = previous - } - } - } -} - -/** - * Run `self` while command chrome is quiet. - * - * The spinner and the waiting toast pause for `self` and resume when it - * finishes, including when it is interrupted. The command stays blocked for - * the whole run, so the button cannot start again. - * - * Outside a command the default gate is inactive and `self` runs unchanged. - * - * ```ts - * const decision = yield* suspendWaiting(ask(pickupDate)) - * yield* close(orderId) - * ``` - */ -export const suspendWaiting = (self: Effect.Effect): Effect.Effect => - Effect.uninterruptibleMask((restore) => - Effect.gen(function*() { - const gate = yield* WaitingGate - yield* gate.add(1) - return yield* Effect.ensuring(restore(self), gate.add(-1)) - }) - ) diff --git a/packages/effect-app/src/withToast.ts b/packages/effect-app/src/withToast.ts deleted file mode 100644 index 95d4ac406..000000000 --- a/packages/effect-app/src/withToast.ts +++ /dev/null @@ -1,179 +0,0 @@ -import * as Cause from "effect/Cause" -import * as Fiber from "effect/Fiber" -import * as Context from "./Context.ts" -import * as Effect from "./Effect.ts" -import * as Layer from "./Layer.ts" -import type * as Option from "./Option.ts" -import * as S from "./Schema.ts" -import { CurrentToastId, Toast, type ToastId } from "./toast.ts" -import { wrapEffect } from "./utils.ts" -import { WaitingGate } from "./waitingGate.ts" - -export interface ToastOptions, WaiR, SucR, ErrR> { - stableToastId?: undefined | string | ((...args: Args) => string | undefined) - timeout?: number - showSpanInfo?: false - groupId?: string - onWaiting: - | string - | ((...args: Args) => string | null) - | null - | (( - ...args: Args - ) => Effect.Effect) - onSuccess: - | string - | ((a: A, ...args: Args) => string | null) - | null - | (( - a: A, - ...args: Args - ) => Effect.Effect) - onFailure: - | string - | (( - error: Option.Option, - ...args: Args - ) => string | { level: "warn" | "error"; message: string }) - | (( - error: Option.Option, - ...args: Args - ) => Effect.Effect) -} - -// @effect-diagnostics-next-line missingEffectServiceDependency:off -export class WithToast extends Context.Service()("WithToast", { - make: Effect.gen(function*() { - const toast = yield* Toast - return ( - options: ToastOptions - ) => - Effect.fnUntraced(function*(self: Effect.Effect, ...args: Args) { - const baseTimeout = options.timeout ?? 3_000 - - const stableToastId = typeof options.stableToastId === "function" - ? options.stableToastId(...args) - : options.stableToastId - - const requestId: string = yield* Effect.currentSpan.pipe( - Effect.map((span) => span.traceId), - Effect.orElseSucceed(() => S.StringId.make()) - ) - const groupId = options.groupId - const meta = { ...(groupId !== undefined ? { groupId } : {}), requestId } - - const t = yield* wrapEffect(options.onWaiting)(...args) - const toastId: ToastId | undefined = t === null - ? stableToastId - : stableToastId ?? `wait-${Math.random().toString(36).slice(2)}` - - const gate = yield* WaitingGate - let waitingFiber: Fiber.Fiber | undefined - let toastShown = false - let unbind = () => {} - - const armWaiting = () => - Effect.gen(function*() { - if (waitingFiber !== undefined || t === null || toastId === undefined) return - waitingFiber = yield* Effect.forkChild( - Effect.sleep("1 seconds").pipe( - Effect.andThen(Effect.uninterruptible(Effect.gen(function*() { - toastShown = true - yield* toast.info(t, { id: toastId, timeout: Infinity, ...meta }) - }))), - Effect.asVoid - ) - ) - }) - - const disarmWaiting = () => - Effect.gen(function*() { - const fiber = waitingFiber - waitingFiber = undefined - if (fiber) yield* Fiber.interrupt(fiber) - if (toastShown && toastId !== undefined) { - toastShown = false - yield* toast.dismiss(toastId) - } - }) - - // An active gate pauses the toast while `suspendWaiting` is parked and - // starts a fresh one-second delay when work resumes. The inactive - // default keeps the original fork. - if (t !== null && gate.active) { - unbind = gate.bind((depth) => (depth === 0 ? armWaiting() : disarmWaiting())) - if (gate.depth === 0) yield* armWaiting() - } else if (t !== null) { - waitingFiber = yield* Effect.forkChild( - Effect.sleep("1 seconds").pipe( - Effect.andThen(toast.info(t, { id: toastId!, timeout: Infinity, ...meta })), - Effect.asVoid - ) - ) - } - - const interruptWaiting = Effect.uninterruptible(Effect.gen(function*() { - unbind() - const fiber = waitingFiber - waitingFiber = undefined - if (fiber) yield* Fiber.interrupt(fiber) - })) - - return yield* self.pipe( - Effect.tap(Effect.fnUntraced(function*(a) { - yield* interruptWaiting - const t = yield* wrapEffect(options.onSuccess)(a, ...args) - if (t === null) { - return - } - yield* toast.success( - t, - toastId !== undefined - ? { id: toastId, timeout: baseTimeout, ...meta } - : { timeout: baseTimeout, ...meta } - ) - })), - Effect.tapCause(Effect.fnUntraced(function*(cause) { - yield* interruptWaiting - yield* Effect.logDebug( - "WithToast - caught error cause: " + Cause.squash(cause), - Cause.hasInterruptsOnly(cause), - cause - ) - - if (Cause.hasInterruptsOnly(cause)) { - if (toastId) yield* toast.dismiss(toastId) - return - } - - const spanInfo = options.showSpanInfo !== false - ? yield* Effect.currentSpan.pipe( - Effect.map((span) => `\nTrace: ${span.traceId}\nSpan: ${span.spanId}`), - Effect.orElseSucceed(() => "") - ) - : "" - - const t = yield* wrapEffect(options.onFailure)(Cause.findErrorOption(cause), ...args) - const opts = { timeout: baseTimeout * 2, ...meta } - - if (typeof t === "object") { - const message = t.message + spanInfo - return t.level === "warn" - ? yield* toast.warning(message, toastId !== undefined ? { ...opts, id: toastId } : opts) - : yield* toast.error(message, toastId !== undefined ? { ...opts, id: toastId } : opts) - } - yield* toast.error(t + spanInfo, toastId !== undefined ? { ...opts, id: toastId } : opts) - }, Effect.uninterruptible)), - toastId !== undefined ? Effect.provideService(CurrentToastId, CurrentToastId.of({ toastId })) : (_) => _ - ) - }) - }) -}) { - static readonly DefaultWithoutDependencies = Layer.effect(this, this.make) - static readonly Default = this.DefaultWithoutDependencies - - static readonly handle = , R, WaiR = never, SucR = never, ErrR = never>( - options: ToastOptions - ): (self: Effect.Effect, ...args: Args) => Effect.Effect => - (self, ...args) => this.use((_) => _(options)(self, ...args)) -} diff --git a/packages/effect-app/test/dateTimeLocal.test.ts b/packages/effect-app/test/dateTimeLocal.test.ts deleted file mode 100644 index 924cd9f52..000000000 --- a/packages/effect-app/test/dateTimeLocal.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { makeZonedLocal, pipeZonedLocal } from "effect-app/DateTime" -import * as DateTime from "effect/DateTime" -import { describe, expect, test } from "vitest" - -describe("local Date helpers", () => { - const start = new Date("2024-03-09T17:00:00.000Z") - - test("makeZonedLocal keeps the instant and uses the process zone", () => { - const zoned = makeZonedLocal(start) - expect(zoned.epochMilliseconds).toBe(start.getTime()) - expect(DateTime.zoneToString(zoned.zone)).toBe(DateTime.zoneToString(DateTime.zoneMakeLocal())) - }) - - test("pipeZonedLocal adds a local calendar day and returns the instant", () => { - const expected = new Date(start.getTime()) - expected.setDate(expected.getDate() + 1) - expect(pipeZonedLocal(start, DateTime.add({ days: 1 })).getTime()).toBe(expected.getTime()) - }) - - test("pipeZonedLocal runs every step before toDateUtc", () => { - const expected = new Date(start.getTime()) - expected.setDate(expected.getDate() + 1) - expected.setMonth(expected.getMonth() + 1) - const result = pipeZonedLocal(start, DateTime.add({ days: 1 }), DateTime.add({ months: 1 })) - expect(result.getTime()).toBe(expected.getTime()) - }) - - test("pipeZonedLocal accepts as many functions as pipe", () => { - const keep = (self: A) => self - const expected = new Date(start.getTime()) - expected.setDate(expected.getDate() + 1) - const result = pipeZonedLocal( - start, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - keep, - DateTime.add({ days: 1 }) - ) - expect(result.getTime()).toBe(expected.getTime()) - }) -}) diff --git a/packages/effect-app/test/moreStrings.test.ts b/packages/effect-app/test/moreStrings.test.ts deleted file mode 100644 index 88d014338..000000000 --- a/packages/effect-app/test/moreStrings.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as S from "effect-app/Schema" -import * as Arbitrary from "effect/Arbitrary" -import * as Effect from "effect/Effect" -import { customRandom, urlAlphabet } from "nanoid" -import { test } from "vitest" - -const nanoidAlphabet = new Set(urlAlphabet) - -const isNanoId = (value: string) => value.length === 21 && Array.from(value).every((char) => nanoidAlphabet.has(char)) - -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) - } -}) - -test("StringId native Arbitrary samples unique nanoid-shaped values", () => { - const samples = Effect.runSync(Arbitrary.sampleEffect(Arbitrary.schema(S.StringId), { count: 20, seed: 5 })) - expect(new Set(samples).size).toBe(20) - for (const value of samples) { - expect(isNanoId(value)).toBe(true) - expect(S.is(S.StringId)(value)).toBe(true) - } -}) - -// Pre Effect 4.0.0-rc.114 StringIdArb (rc.112, 8bd5bfe64): -// fc.uint8Array({ minLength: 10 * 21, maxLength: 10 * 21 }) -// .map((bytes) => customRandom(urlAlphabet, 21, (n) => bytes.subarray(0, n))()) -test("StringId toCodecArbitrary uses the pre-rc.114 StringIdArb byte map", () => { - const size = 21 - const length = 10 * size - const stringIdArbMap = (bytes: Uint8Array) => customRandom(urlAlphabet, size, (n) => bytes.subarray(0, n))() - const bytes = Uint8Array.from({ length }, (_, i) => (i * 17 + 3) & 0xff) - const fromOld = stringIdArbMap(bytes) - expect(isNanoId(fromOld)).toBe(true) - // Same mapping the schema's toCodecArbitrary decode runs (customRandom + subarray). - expect(customRandom(urlAlphabet, size, (n) => bytes.subarray(0, n))()).toBe(fromOld) -}) - -test("prefixedStringId native Arbitrary matches pre-rc.114 prefix + nanoid map", () => { - const FooId = S.prefixedStringId()("foo", "FooId") - const samples = Effect.runSync(Arbitrary.sampleEffect(Arbitrary.schema(FooId), { count: 10, seed: 5 })) - expect(new Set(samples).size).toBe(10) - for (const value of samples) { - expect(value.startsWith("foo-")).toBe(true) - expect(S.is(FooId)(value)).toBe(true) - const rest = value.slice("foo-".length) - expect(rest.length).toBeGreaterThan(0) - expect(Array.from(rest).every((char) => nanoidAlphabet.has(char))).toBe(true) - } -}) - -test("Url native Arbitrary samples http(s) URLs", () => { - const samples = Effect.runSync(Arbitrary.sampleEffect(Arbitrary.schema(S.Url), { count: 10, seed: 5 })) - expect(new Set(samples).size).toBeGreaterThan(1) - for (const value of samples) { - expect(value.startsWith("https://")).toBe(true) - expect(S.is(S.Url)(value)).toBe(true) - } -}) diff --git a/packages/effect-app/test/nativeEffectMigration.test.ts b/packages/effect-app/test/nativeEffectMigration.test.ts deleted file mode 100644 index 27e3c4eef..000000000 --- a/packages/effect-app/test/nativeEffectMigration.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { migrateEffectAppSource } from "effect-app/migration/nativeEffect" -import { describe, expect, test } from "vitest" - -describe("migrateEffectAppSource", () => { - test("rewrites named helpers and keeps the other imports", () => { - const source = ` -import { typedKeysOf, pretty } from "effect-app/utils" -import { tuple } from "effect-app/Function" -import { toBool, fromBool } from "effect-app/Option" - -const keys = typedKeysOf(value) -const pair = tuple(keys, pretty(value)) -const present = toBool(option) -const flag = fromBool(enabled) -` - const migrated = migrateEffectAppSource(source) - expect(migrated).toContain(`import { pretty } from "effect-app/utils"`) - expect(migrated).toContain(`import { fromBool } from "effect-app/Option"`) - expect(migrated).toContain(`import * as Struct from "effect/Struct"`) - expect(migrated).toContain(`import * as Tuple from "effect/Tuple"`) - expect(migrated).toContain(`import * as Option from "effect/Option"`) - expect(migrated).toContain("const keys = Struct.keys(value)") - expect(migrated).toContain("const pair = Tuple.make(keys, pretty(value))") - expect(migrated).toContain("const present = Option.isSome(option)") - expect(migrated).toContain("const flag = fromBool(enabled)") - expect(migrated).not.toContain("typedKeysOf") - expect(migrated).not.toContain("effect-app/Function") - }) - - test("rewrites aliases, namespaces, calls, and leaves strings and comments", () => { - const source = ` -import { dropUndefined as compact, assertUnreachable as unreachable } from "effect-app/utils" -import * as Dates from "effect-app/_ext/date" -import { chunk_, toNonEmptyArray } from "effect-app/Array" -import { DateValid } from "effect-app/Schema" - -// typedKeysOf stays in this comment -const label = "tuple(typedKeysOf)" -const fields = compact({ a: 1, b: undefined }) -const next = Dates.DateAddDays(today, 2) -const parts = chunk_(items, 10) -const some = toNonEmptyArray(items) -const Day = DateValid -const missing = unreachable(value) -` - const migrated = migrateEffectAppSource(source) - expect(migrated).toContain("// typedKeysOf stays in this comment") - expect(migrated).toContain(`const label = "tuple(typedKeysOf)"`) - expect(migrated).toContain("const fields = compact({ a: 1, b: undefined })") - expect(migrated).toContain("dropUndefined as compact") - expect(migrated).toContain( - "const next = pipeZonedLocal(today, DateTime.add({ days: 2 }))" - ) - expect(migrated).toContain("const parts = Chunk.fromIterable(Array.chunksOf(items, 10))") - expect(migrated).toContain("const some = Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(items)") - expect(migrated).toContain(`import { Date } from "effect-app/Schema"`) - expect(migrated).toContain("const Day = Date") - expect(migrated).toContain("const missing = absurd(value)") - expect(migrated).toContain(`import { pipeZonedLocal } from "effect-app/DateTime"`) - expect(migrated).toContain(`import * as DateTime from "effect/DateTime"`) - expect(migrated).not.toContain("effect-app/_ext/date") - expect(migrated).not.toContain("DateValid") - }) - - test("rewrites a namespace member and keeps the namespace when it still has other members", () => { - const source = ` -import * as Chunk from "effect-app/Chunk" - -const first = Chunk.findFirstSimple(chunk, predicate) -const grouped = Chunk.groupByTChunk_(chunk, key) -` - const migrated = migrateEffectAppSource(source) - expect(migrated).toContain(`import * as Chunk from "effect-app/Chunk"`) - expect(migrated).toContain("const first = Chunk.findFirst(chunk, predicate)") - expect(migrated).toContain("const grouped = Chunk.groupByTChunk_(chunk, key)") - }) -}) diff --git a/packages/effect-app/test/rpc.test.ts b/packages/effect-app/test/rpc.test.ts deleted file mode 100644 index d76ed2e00..000000000 --- a/packages/effect-app/test/rpc.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type * as Effect from "effect/Effect" -import type * as Option from "effect/Option" -import { expect, test } from "vitest" -import { makeRpcClient, NotLoggedInError, UnauthorizedError } from "../src/client.js" -import { S } from "../src/index.js" -import { RpcContextMap } from "../src/rpc.js" - -export class RequestContextMap extends RpcContextMap.makeMap({ - allowAnonymous: RpcContextMap.makeInverted()(NotLoggedInError), - requireRoles: RpcContextMap.makeCustom()(UnauthorizedError, Array()), - test: RpcContextMap.make()(S.Never) -}) {} - -const stubMiddleware = { - requestContextMap: RequestContextMap.config, - requestContext: undefined as never -} -const { TaggedRequestFor } = makeRpcClient(stubMiddleware) -const TaggedRequest = TaggedRequestFor("Test").Query - -export class Stats extends TaggedRequest()("Stats", {}, { - allowedRoles: ["manager"], - success: { - usersActive24Hours: S.Finite, - usersActiveLastWeek: S.Finite, - newUsersLast24Hours: S.Finite, - newUsersLastWeek: S.Finite - } -}) {} - -declare const _stats: typeof Stats.Type -declare const _statsSuccess: typeof Stats.success.Type -declare const _statsError: typeof Stats.error.Type -declare const _statsRequestType: typeof Stats.type - -test("Void decodes and encodes as void", () => { - const statsFromMake = Stats.make({}) - const statsFromMakeOption = Stats.makeOption({}) - const statsFromMakeEffect = Stats.makeEffect({}) - - expect(S.decodeUnknownSync(S.Void)(undefined)).toBe(undefined) - expect(S.is(S.Void)(undefined)).toBe(true) - expect(S.decodeUnknownSync(S.Void)("test")).toBe(undefined) - expect(S.is(S.Void)("test")).toBe(true) - expect(S.encodeUnknownSync(S.Void)("test")).toBe(undefined) - expect(S.encodeUnknownSync(S.toCodecJson(S.Void))("test")).toBe(null) - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<{ - readonly usersActive24Hours: number - readonly usersActiveLastWeek: number - readonly newUsersLast24Hours: number - readonly newUsersLastWeek: number - }>() - // Resource error carries only `config.error` (and optional `generalErrors`); rcm-derived - // middleware errors no longer leak into `resource.error` — they reach the wire via the - // middleware tag attached to the rpc group (`rpc.middlewares[*].error` failure-union). - expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf<"query">() - expectTypeOf(statsFromMake).toEqualTypeOf() - expectTypeOf(statsFromMakeOption).toEqualTypeOf>() - expectTypeOf(statsFromMakeEffect).toEqualTypeOf>() -}) diff --git a/packages/effect-app/test/rpcMiddlewareContext.test.ts b/packages/effect-app/test/rpcMiddlewareContext.test.ts deleted file mode 100644 index 654a20c7a..000000000 --- a/packages/effect-app/test/rpcMiddlewareContext.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expectTypeOf, test } from "vitest" -import { makeRpcClient, NotLoggedInError, UnauthorizedError } from "../src/client.js" -import { RpcContextMap } from "../src/rpc.js" - -// A stand-in for an app service (e.g. `UserProfile`) that the `allowAnonymous` -// inverted middleware provides into the handler context whenever auth is -// required (i.e. `allowAnonymous` is not `true`). -interface UserProfile { - readonly _tag: "UserProfile" -} - -// Mirrors a real app request-context map: one inverted key providing a service -// by default (unless opted out), and one custom (role) key. -class RequestContextMap extends RpcContextMap.makeMap({ - allowAnonymous: RpcContextMap.makeInverted()(NotLoggedInError), - allowRoles: RpcContextMap.makeCustom()(UnauthorizedError, Array()) -}) {} - -type Map = typeof RequestContextMap.config - -// The context a handler receives for a given rpc config — exactly the -// `GetEffectContext<_RC, rpc.config>` the router computes for the handler -// (see `RpcMiddleware.ts`). Testing it against config literals pins down the -// middleware semantics directly. -type CtxFor = RpcContextMap.GetEffectContext - -test("`GetEffectContext`: the inverted `UserProfile` provision is gated by `allowAnonymous`, not `allowRoles`", () => { - // Empty config = nothing opted out → auth required → `UserProfile` provided. - expectTypeOf>().toEqualTypeOf() - - // `allowRoles` does not gate `UserProfile` — still provided. - expectTypeOf>().toEqualTypeOf() - - // Negative case: opting into anonymous removes the `UserProfile` provision. - expectTypeOf>().toBeNever() - - // Anonymous wins over roles for the inverted provision. - expectTypeOf>().toBeNever() - - // Regression documentation: a config that collapses to `Record` - // (what the no-config command overload used to produce) WRONGLY drops - // `UserProfile` — `keyof Record` is `string`, so `allowAnonymous` - // satisfies `key extends keyof T`, and `T["allowAnonymous"]` (`never`) `extends - // true`, taking the anonymous branch. This is exactly why the overload must - // yield `{}` (see the command-builder test below). - expectTypeOf>>().toBeNever() -}) - -// End-to-end guard for the actual fix: a command defined with NO config object -// must still provide `UserProfile` (auth required by default). This exercises the -// no-config `Command` overload, which now yields an empty `{}` config instead of -// `Record`. -const { TaggedRequestFor } = makeRpcClient({ - requestContextMap: RequestContextMap.config, - requestContext: undefined as never -}) -const Command = TaggedRequestFor("Test").Command - -class NoConfigCmd extends Command()("NoConfigCmd", {}) {} - -test("a no-config command still provides the auth-required `UserProfile` service", () => { - expectTypeOf>().toEqualTypeOf() -}) diff --git a/packages/effect-app/test/schema.test.ts b/packages/effect-app/test/schema.test.ts deleted file mode 100644 index bb9a15d32..000000000 --- a/packages/effect-app/test/schema.test.ts +++ /dev/null @@ -1,681 +0,0 @@ -// import { generateFromArbitrary } from "@effect-app/infra/test" -import * as Array from "effect-app/Array" -import * as S from "effect-app/Schema" -import { specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema" -import * as Effect from "effect/Effect" -import * as Struct from "effect/Struct" -import { describe, expect, expectTypeOf, test } from "vitest" - -const A = S.Struct({ a: S.NonEmptyString255, email: S.NullOr(S.Email) }) -test("works", () => { - console.log(S.StringId.make()) - // console.log(generateFromArbitrary(S.A.make(A)).value) - console.log(S.AST.resolveTitle(S.Email.ast)) - console.log(S.AST.resolveDescription(S.Email.ast)) - console.log(S.toJsonSchemaDocument(S.Email)) - console.log(S.toJsonSchemaDocument(S.Email)) - console.log(S.decodeExit(A)({ a: Array.range(1, 256).join(""), email: "hello" })) -}) - -test("literal default works", () => { - const l = S.Literals(["a", "b"]) - expect(l.Default).toBe("a") - expectTypeOf(l.Default).toEqualTypeOf<"a">() - const s = S.Struct({ l: l.withConstructorDefault }) - expect(s.make({}).l).toBe("a") - - const l2 = l.changeDefault("b") - const s2 = S.Struct({ l: l2.withConstructorDefault }) - expect(s2.make({}).l).toBe("b") -}) - -// Toy schema for `dropConstructorDefault`: a field with a constructor -// default that should NOT auto-populate when deriving a partial-update -// schema from a "create" schema. -const Person = S.Struct({ - name: S.String, - notes: S.NullOr(S.String).pipe(S.optionalKey, S.withConstructorDefault(Effect.succeed(null))) -}) - -test("plain Struct.omit leaks the inherited constructor default (the bug)", () => { - const UpdatePersonBuggy = S.Struct(Struct.omit(Person.fields, [] as const)) - expect(UpdatePersonBuggy.make({ name: "Mario" })).toEqual({ name: "Mario", notes: null }) -}) - -test("Struct.map(S.dropConstructorDefault) strips the inherited constructor default (the fix)", () => { - const UpdatePersonFixed = S.Struct(Struct.map(Struct.omit(Person.fields, [] as const), S.dropConstructorDefault)) - expect(UpdatePersonFixed.make({ name: "Mario" })).toEqual({ name: "Mario" }) -}) - -test("Struct.map(S.dropConstructorDefault) still omits the requested keys, like Struct.omit", () => { - const NameOnly = S.Struct(Struct.map(Struct.omit(Person.fields, ["notes"] as const), S.dropConstructorDefault)) - expect(NameOnly.make({ name: "Mario" })).toEqual({ name: "Mario" }) - expectTypeOf().toEqualTypeOf<{ readonly name: string }>() -}) - -test("makeExactOptional drops the inherited constructor default too", () => { - const UpdatePerson = S.Struct(S.makeExactOptional(Person.fields)) - expect(UpdatePerson.make({ name: "Mario" })).toEqual({ name: "Mario" }) -}) - -test("dropConstructorDefault stays 'no-default' after annotate/annotateKey (Rebuild fix)", () => { - const notesDropped = S.dropConstructorDefault(Person.fields.notes) - expectTypeOf<(typeof notesDropped)["~type.constructor.default"]>().toEqualTypeOf<"no-default">() - - const notesAnnotated = notesDropped.annotate({ description: "no longer defaulted" }) - expectTypeOf<(typeof notesAnnotated)["~type.constructor.default"]>().toEqualTypeOf<"no-default">() - - const notesKeyAnnotated = notesDropped.annotateKey({ description: "no longer defaulted (key)" }) - expectTypeOf<(typeof notesKeyAnnotated)["~type.constructor.default"]>().toEqualTypeOf<"no-default">() - - const UpdatePerson = S.Struct({ name: Person.fields.name, notes: notesAnnotated }) - expect(UpdatePerson.make({ name: "Mario" })).toEqual({ name: "Mario" }) -}) - -test("NonEmptyString255.Type uses the named brand alias", () => { - type A = typeof S.NonEmptyString255.Type - type B = string & S.NonEmptyString255Brand - expectTypeOf().toEqualTypeOf() -}) - -test("Opaque accepts an explicit Encoded type", () => { - interface User { - readonly id: string - readonly _tag: "User" - } - - interface UserEncoded { - readonly id: string - } - - const baseSchema = S.Struct({ id: S.String, name: S.String }) - const _UserSchema = S.Opaque()(baseSchema) - - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().not.toEqualTypeOf>() -}) - -test("Opaque with one generic keeps the base encoded shape", () => { - interface User { - readonly id: string - } - - const baseSchema = S.Struct({ id: S.String }) - const _UserSchema = S.Opaque()(baseSchema) - - expectTypeOf>().toEqualTypeOf<{ readonly id: string }>() - expectTypeOf>().toEqualTypeOf() -}) - -test("Opaque preserves optional Struct.make input", () => { - interface User { - readonly a?: string | undefined - readonly b?: number | undefined - } - - const schema = S.Opaque()(S.Struct({ - a: S.optional(S.String), - b: S.optional(S.Number) - })) - - const made = schema.make() - expect(made).toEqual({}) - expectTypeOf(made).toEqualTypeOf() -}) - -test("Opaque preserves optional TaggedStruct.make input", () => { - interface OnlyTag { - readonly _tag: "OnlyTag" - } - - const schema = S.Opaque()(S.TaggedStruct("OnlyTag", {})) - - const made = schema.make() - expect(made).toEqual({ _tag: "OnlyTag" }) - expectTypeOf(made).toEqualTypeOf() -}) - -test("S.Literals([\"A\", \"B\"]).Default is typed as \"A\"", () => { - const l = S.Literals(["A", "B"]) - expect(l.Default).toBe("A") - expectTypeOf(l.Default).toEqualTypeOf<"A">() -}) - -test("Struct.make accepts void when all fields are optional", () => { - const schema = S.Struct({ - a: S.optional(S.String), - b: S.optional(S.Number) - }) - - const made = schema.make() - expect(made).toEqual({}) - expectTypeOf(made).toEqualTypeOf<{ readonly a?: string | undefined; readonly b?: number | undefined }>() -}) - -test("StructNestedEncoded resolves Struct and from.Encoded", () => { - const plain = S.Struct({ a: S.String, b: S.NullOr(S.String) }) - const encodedKeys = plain.pipe(S.encodeKeys({ b: "b_encoded" })) - - expectTypeOf>().toEqualTypeOf<{ - readonly a: string - readonly b: string | null - }>() - - expectTypeOf>().toEqualTypeOf<{ - readonly a: string - readonly b_encoded: string | null - }>() -}) - -test("TaggedStruct.make accepts void when only constructor-default fields exist", () => { - const schema = S.TaggedStruct("OnlyTag", {}) - - const made = schema.make() - expect(made).toEqual({ _tag: "OnlyTag" }) - expectTypeOf(made).toEqualTypeOf<{ readonly _tag: "OnlyTag" }>() -}) - -test("tagged union derives tag map and tags from v4 literal ast", () => { - const schema = S.TaggedUnion([ - S.TaggedStruct("A", { a: S.String }), - S.TaggedStruct("B", { b: S.Finite }), - S.TaggedStruct("C", { c: S.Boolean }) - ]) - const caseA = schema.cases["A"] - const caseB = schema.cases["B"] - const caseC = schema.cases["C"] - const isAOrB = schema.isAnyOf(["A", "B"]) - - expect(caseA.fields._tag.ast.literal).toBe("A") - expect(caseB.fields._tag.ast.literal).toBe("B") - expect(caseC.fields._tag.ast.literal).toBe("C") - expect(S.decodeSync(schema.tags)("A")).toBe("A") - expect(S.decodeSync(schema.tags)("B")).toBe("B") - expect(S.decodeSync(schema.tags)("C")).toBe("C") - expect(() => S.decodeUnknownSync(schema.tags)("D")).toThrow() - - expect(schema.guards.A({ _tag: "A", a: "ok" })).toBe(true) - expect(schema.guards.A({ _tag: "B", b: 1 })).toBe(false) - expect(schema.guards.B({ _tag: "B", b: 1 })).toBe(true) - expect(schema.guards.B({ _tag: "A", a: "ok" })).toBe(false) - expect(schema.guards.C({ _tag: "C", c: true })).toBe(true) - expect(schema.guards.C({ _tag: "A", a: "ok" })).toBe(false) - - expect(isAOrB({ _tag: "A", a: "ok" })).toBe(true) - expect(isAOrB({ _tag: "B", b: 1 })).toBe(true) - expect(isAOrB({ _tag: "C", c: true })).toBe(false) -}) - -test("TaggedUnion tags returns a Literals schema with correct literal values", () => { - const schema = S.TaggedUnion([ - S.TaggedStruct("X", { x: S.String }), - S.TaggedStruct("Y", { y: S.Finite }) - ]) - - expect(schema.tags.literals).toEqual(["X", "Y"]) - expectTypeOf(schema.tags.literals).toMatchTypeOf() -}) - -test("TaggedUnion tags.pick returns a subset of the tag literals", () => { - const schema = S.TaggedUnion([ - S.TaggedStruct("A", { a: S.String }), - S.TaggedStruct("B", { b: S.Finite }), - S.TaggedStruct("C", { c: S.Boolean }) - ]) - - const subset = schema.tags.pick(["A", "C"]) - expect(subset.literals).toEqual(["A", "C"]) - expect(S.decodeSync(subset)("A")).toBe("A") - expect(S.decodeSync(subset)("C")).toBe("C") - expect(() => S.decodeUnknownSync(subset)("B")).toThrow() -}) - -test("tags standalone function extracts tags from member schemas", () => { - const members = [ - S.TaggedStruct("Foo", { foo: S.String }), - S.TaggedStruct("Bar", { bar: S.Finite }) - ] as const - - const tagSchema = S.tags(members) - expect(tagSchema.literals).toEqual(["Foo", "Bar"]) - expect(S.decodeSync(tagSchema)("Foo")).toBe("Foo") - expect(S.decodeSync(tagSchema)("Bar")).toBe("Bar") - expect(() => S.decodeUnknownSync(tagSchema)("Baz")).toThrow() -}) - -test("ExtendTaggedUnion adds tags to an existing Union", () => { - const union = S.Union([ - S.TaggedStruct("P", { p: S.String }), - S.TaggedStruct("Q", { q: S.Finite }) - ]) - const extended = S.ExtendTaggedUnion(union) - - expect(extended.tags.literals).toEqual(["P", "Q"]) - expect(S.decodeSync(extended.tags)("P")).toBe("P") - expect(S.decodeSync(extended.tags)("Q")).toBe("Q") - expect(() => S.decodeUnknownSync(extended.tags)("R")).toThrow() - expect(extended.cases["P"].fields._tag.ast.literal).toBe("P") - expect(extended.guards.P({ _tag: "P", p: "ok" })).toBe(true) - expect(extended.guards.P({ _tag: "Q", q: 1 })).toBe(false) -}) - -test("TaggedUnion match dispatches on _tag", () => { - const schema = S.TaggedUnion([ - S.TaggedStruct("A", { a: S.String }), - S.TaggedStruct("B", { b: S.Finite }) - ]) - - const matcher = schema.match({ - A: (v) => `got A: ${v.a}`, - B: (v) => `got B: ${v.b}` - }) - expect(matcher({ _tag: "A", a: "hello" })).toBe("got A: hello") - expect(matcher({ _tag: "B", b: 42 })).toBe("got B: 42") -}) - -test("TaggedUnion with single member", () => { - const schema = S.TaggedUnion([ - S.TaggedStruct("Only", { val: S.String }) - ]) - - expect(schema.tags.literals).toEqual(["Only"]) - expect(S.decodeSync(schema.tags)("Only")).toBe("Only") - expect(() => S.decodeUnknownSync(schema.tags)("Other")).toThrow() - expect(schema.guards.Only({ _tag: "Only", val: "x" })).toBe(true) -}) - -test("TaggedUnion tags type is narrowed to the exact tag literals", () => { - const schema = S.TaggedUnion([ - S.TaggedStruct("Alpha", { a: S.String }), - S.TaggedStruct("Beta", { b: S.Finite }), - S.TaggedStruct("Gamma", { c: S.Boolean }) - ]) - - type Tags = S.Schema.Type - expectTypeOf().toEqualTypeOf<"Alpha" | "Beta" | "Gamma">() -}) - -test("TaggedUnion with encodeKeys renaming a non-tag key", () => { - const MemberA = S.TaggedStruct("A", { firstName: S.String }).pipe( - S.encodeKeys({ firstName: "first_name" }) - ) - const MemberB = S.TaggedStruct("B", { lastName: S.Finite }).pipe( - S.encodeKeys({ lastName: "last_name" }) - ) - - const schema = S.TaggedUnion([MemberA, MemberB]) - - expect(schema.tags.literals).toEqual(["A", "B"]) - expect(S.decodeSync(schema.tags)("A")).toBe("A") - expect(S.decodeSync(schema.tags)("B")).toBe("B") - - // decode from encoded (snake_case) to decoded (camelCase) - const decoded = S.decodeUnknownSync(schema)({ _tag: "A", first_name: "Alice" }) - expect(decoded).toEqual({ _tag: "A", firstName: "Alice" }) - - const decoded2 = S.decodeUnknownSync(schema)({ _tag: "B", last_name: 42 }) - expect(decoded2).toEqual({ _tag: "B", lastName: 42 }) - - // encode back to snake_case - const encoded = S.encodeSync(schema)({ _tag: "A", firstName: "Alice" }) - expect(encoded).toEqual({ _tag: "A", first_name: "Alice" }) - - // guards work on decoded values - expect(schema.guards.A({ _tag: "A", firstName: "Alice" })).toBe(true) - expect(schema.guards.A({ _tag: "B", lastName: 42 })).toBe(false) - expect(schema.guards.B({ _tag: "B", lastName: 42 })).toBe(true) -}) - -test("TaggedUnion with TaggedClass members", () => { - class Foo extends S.TaggedClass()("Foo", { name: S.String }) {} - class Bar extends S.TaggedClass()("Bar", { count: S.Finite }) {} - - const schema = S.TaggedUnion([Foo, Bar]) - - expect(schema.tags.literals).toEqual(["Foo", "Bar"]) - expect(S.decodeSync(schema.tags)("Foo")).toBe("Foo") - expect(S.decodeSync(schema.tags)("Bar")).toBe("Bar") - expect(() => S.decodeUnknownSync(schema.tags)("Baz")).toThrow() - - const decoded = S.decodeUnknownSync(schema)({ _tag: "Foo", name: "Alice" }) - expect(decoded).toBeInstanceOf(Foo) - expect(decoded).toEqual(new Foo({ name: "Alice" })) - - const decoded2 = S.decodeUnknownSync(schema)({ _tag: "Bar", count: 3 }) - expect(decoded2).toBeInstanceOf(Bar) - expect(decoded2).toEqual(new Bar({ count: 3 })) - - expect(schema.guards.Foo(new Foo({ name: "Alice" }))).toBe(true) - expect(schema.guards.Foo(new Bar({ count: 3 }))).toBe(false) - expect(schema.guards.Bar(new Bar({ count: 3 }))).toBe(true) -}) - -describe("ReadonlySetFromArray", () => { - test("decodes an array of strings to a Set", () => { - const schema = S.ReadonlySetFromArray(S.String) - const decoded = S.decodeUnknownSync(schema)(["a", "b", "c"]) - expect(decoded).toEqual(new Set(["a", "b", "c"])) - }) - - test("encodes a Set back to an array", () => { - const schema = S.ReadonlySetFromArray(S.String) - const encoded = S.encodeSync(schema)(new Set(["a", "b"])) - expect(encoded).toEqual(["a", "b"]) - }) - - test("decodes with NumberFromString as value", () => { - const schema = S.ReadonlySetFromArray(S.NumberFromString) - const decoded = S.decodeUnknownSync(schema)(["1", "2", "3"]) - expect(decoded).toEqual(new Set([1, 2, 3])) - expectTypeOf(decoded).toEqualTypeOf>() - }) - - test("encodes with NumberFromString as value", () => { - const schema = S.ReadonlySetFromArray(S.NumberFromString) - const encoded = S.encodeSync(schema)(new Set([1, 2, 3])) - expect(encoded).toEqual(["1", "2", "3"]) - }) - - test("rejects invalid input", () => { - const schema = S.ReadonlySetFromArray(S.NumberFromString) - expect(() => S.decodeUnknownSync(schema)([1, 2])).toThrow() - }) -}) - -describe("ReadonlyMapFromArray", () => { - test("decodes an array of tuples to a Map", () => { - const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.Finite }) - const decoded = S.decodeUnknownSync(schema)([["a", 1], ["b", 2]]) - expect(decoded).toEqual(new Map([["a", 1], ["b", 2]])) - }) - - test("encodes a Map back to an array of tuples", () => { - const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.Finite }) - const encoded = S.encodeSync(schema)(new Map([["a", 1], ["b", 2]])) - expect(encoded).toEqual([["a", 1], ["b", 2]]) - }) - - test("decodes with NumberFromString as key", () => { - const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String }) - const decoded = S.decodeUnknownSync(schema)([["1", "one"], ["2", "two"]]) - expect(decoded).toEqual(new Map([[1, "one"], [2, "two"]])) - expectTypeOf(decoded).toEqualTypeOf>() - }) - - test("encodes with NumberFromString as key", () => { - const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String }) - const encoded = S.encodeSync(schema)(new Map([[1, "one"], [2, "two"]])) - expect(encoded).toEqual([["1", "one"], ["2", "two"]]) - }) - - test("decodes with NumberFromString as value", () => { - const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString }) - const decoded = S.decodeUnknownSync(schema)([["a", "10"], ["b", "20"]]) - expect(decoded).toEqual(new Map([["a", 10], ["b", 20]])) - expectTypeOf(decoded).toEqualTypeOf>() - }) - - test("encodes with NumberFromString as value", () => { - const schema = S.ReadonlyMapFromArray({ key: S.String, value: S.NumberFromString }) - const encoded = S.encodeSync(schema)(new Map([["a", 10], ["b", 20]])) - expect(encoded).toEqual([["a", "10"], ["b", "20"]]) - }) - - test("decodes with NumberFromString as both key and value", () => { - const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.NumberFromString }) - const decoded = S.decodeUnknownSync(schema)([["1", "10"], ["2", "20"]]) - expect(decoded).toEqual(new Map([[1, 10], [2, 20]])) - expectTypeOf(decoded).toEqualTypeOf>() - }) - - test("rejects invalid input", () => { - const schema = S.ReadonlyMapFromArray({ key: S.NumberFromString, value: S.String }) - expect(() => S.decodeUnknownSync(schema)([[1, "val"]])).toThrow() - }) -}) - -describe("ReadonlySet (with withConstructorDefault)", () => { - test("make provides withConstructorDefault", () => { - const schema = S.ReadonlySet(S.NumberFromString) - const struct = S.Struct({ items: schema.withConstructorDefault }) - const made = struct.make({}) - expect(made.items).toEqual(new Set()) - }) - - test("decodes a Set with NumberFromString values", () => { - const schema = S.ReadonlySet(S.NumberFromString) - const decoded = S.decodeUnknownSync(schema)(new Set(["1", "2"])) - expect(decoded).toEqual(new Set([1, 2])) - }) - - test("Encoded is a Set, not an array", () => { - const schema = S.ReadonlySet(S.String) - const encoded = S.encodeSync(schema)(new Set(["a"])) - expect(encoded).toEqual(new Set(["a"])) - expectTypeOf(encoded).toEqualTypeOf>() - }) -}) - -describe("ReadonlyMap (with withConstructorDefault)", () => { - test("make provides withConstructorDefault", () => { - const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String }) - const struct = S.Struct({ items: schema.withConstructorDefault }) - const made = struct.make({}) - expect(made.items).toEqual(new Map()) - }) - - test("decodes a Map with NumberFromString keys", () => { - const schema = S.ReadonlyMap({ key: S.NumberFromString, value: S.String }) - const decoded = S.decodeUnknownSync(schema)(new Map([["1", "one"]])) - expect(decoded).toEqual(new Map([[1, "one"]])) - }) - - test("Encoded is a Map, not an array of tuples", () => { - const schema = S.ReadonlyMap({ key: S.String, value: S.Finite }) - const encoded = S.encodeSync(schema)(new Map([["a", 1]])) - expect(encoded).toEqual(new Map([["a", 1]])) - expectTypeOf(encoded).toEqualTypeOf>() - }) -}) - -describe("JSON Schema", () => { - test("Email has format, minLength, maxLength", () => { - const doc = S.toJsonSchemaDocument(S.Email) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { "$ref": "#/$defs/Email" }, - definitions: { - Email: { - type: "string", - title: "Email", - description: "an email according to RFC 5322", - format: "email", - minLength: 2, - maxLength: 998 - } - } - }) - }) - - test("Email specialJsonSchemaDocument flattens allOf", () => { - const doc = specialJsonSchemaDocument(S.Email) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { "$ref": "#/$defs/Email" }, - definitions: { - Email: { - type: "string", - title: "Email", - description: "an email according to RFC 5322", - format: "email", - minLength: 2, - maxLength: 998 - } - } - }) - }) - - test("Date Encoded is Date; JSON codec encodes ISO strings", () => { - const d = new Date("2024-01-01T00:00:00.000Z") - expect(S.decodeUnknownSync(S.Date)(d)).toBe(d) - expect(S.encodeSync(S.Date)(d)).toBe(d) - expect(S.encodeSync(S.toCodecJson(S.Date))(d)).toBe("2024-01-01T00:00:00.000Z") - const doc = S.toJsonSchemaDocument(S.Date) - expect(doc.dialect).toBe("draft-2020-12") - expect(doc.schema).toEqual({ type: "string" }) - }) - - test("DateFromString keeps string Encoded", () => { - const doc = S.toJsonSchemaDocument(S.DateFromString) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { "$ref": "#/$defs/DateOrInvalid" }, - definitions: { - DateOrInvalid: { - type: "string", - description: "an ISO 8601 date string that will be decoded as a Date (may be invalid)", - format: "date-time" - } - } - }) - }) - - test("PhoneNumber has format phone", () => { - const doc = specialJsonSchemaDocument(S.PhoneNumber) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { "$ref": "#/$defs/PhoneNumber" }, - definitions: { - PhoneNumber: { - type: "string", - title: "PhoneNumber", - description: "a phone number with at least 7 digits", - format: "phone" - } - } - }) - }) - - test("Url has format uri", () => { - const doc = specialJsonSchemaDocument(S.Url) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { "$ref": "#/$defs/Url" }, - definitions: { - Url: { - type: "string", - title: "Url", - format: "uri" - } - } - }) - }) -}) - -describe("generateGuards", () => { - const StateSchema = S.TaggedUnion([ - S.TaggedStruct("Active", { since: S.String }), - S.TaggedStruct("Inactive", { reason: S.String }), - S.TaggedStruct("Pending", { eta: S.Finite }) - ]) - - type State = S.Schema.Type - type Entity = { readonly state: State; readonly name: string } - - const { isActive, isAnyOf, isInactive, isPending } = StateSchema.generateGuards("state") - - test("isActive narrows to Active member", () => { - const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" } - expect(isActive(entity)).toBe(true) - if (isActive(entity)) { - expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>() - } - }) - - test("isActive returns false for non-Active", () => { - const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" } - expect(isActive(entity)).toBe(false) - }) - - test("isInactive narrows to Inactive member", () => { - const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" } - expect(isInactive(entity)).toBe(true) - }) - - test("isPending narrows to Pending member", () => { - const entity: Entity = { state: { _tag: "Pending", eta: 42 }, name: "foo" } - expect(isPending(entity)).toBe(true) - }) - - test("isAnyOf narrows to union of specified members", () => { - const isActiveOrPending = isAnyOf(["Active", "Pending"]) - const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" } - const pending: Entity = { state: { _tag: "Pending", eta: 5 }, name: "bar" } - const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" } - - expect(isActiveOrPending(active)).toBe(true) - expect(isActiveOrPending(pending)).toBe(true) - expect(isActiveOrPending(inactive)).toBe(false) - - if (isActiveOrPending(active)) { - expectTypeOf(active.state).toEqualTypeOf< - { readonly _tag: "Active"; readonly since: string } | { readonly _tag: "Pending"; readonly eta: number } - >() - } - }) - - test("guards use schema-based validation (built-in guards)", () => { - expect(StateSchema.guards.Active({ _tag: "Active" })).toBe(false) - expect(StateSchema.guards.Active({ _tag: "Active", since: "2024-01-01" })).toBe(true) - }) -}) - -describe("generateGuardsFor", () => { - const StateSchema = S.TaggedUnion([ - S.TaggedStruct("Active", { since: S.String }), - S.TaggedStruct("Inactive", { reason: S.String }), - S.TaggedStruct("Pending", { eta: S.Finite }) - ]) - - type State = S.Schema.Type - type Entity = { readonly state: State; readonly name: string } - - const { isActive, isAnyOf } = StateSchema.generateGuardsFor()("state") - - test("isActive narrows to Active member", () => { - const entity: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" } - expect(isActive(entity)).toBe(true) - if (isActive(entity)) { - expectTypeOf(entity.state).toEqualTypeOf<{ readonly _tag: "Active"; readonly since: string }>() - } - }) - - test("isActive returns false for non-Active", () => { - const entity: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "foo" } - expect(isActive(entity)).toBe(false) - }) - - test("isAnyOf narrows to union of specified members", () => { - const isActiveOrPending = isAnyOf(["Active", "Pending"]) - const active: Entity = { state: { _tag: "Active", since: "2024-01-01" }, name: "foo" } - const inactive: Entity = { state: { _tag: "Inactive", reason: "expired" }, name: "baz" } - - expect(isActiveOrPending(active)).toBe(true) - expect(isActiveOrPending(inactive)).toBe(false) - }) - - test("ExtendTaggedUnion also exposes generateGuardsFor", () => { - const union = S.Union([ - S.TaggedStruct("X", { x: S.String }), - S.TaggedStruct("Y", { y: S.Finite }) - ]) - const extended = S.ExtendTaggedUnion(union) - type Obj = { readonly field: S.Schema.Type } - const { isX, isY } = extended.generateGuardsFor()("field") - - expect(isX({ field: { _tag: "X", x: "hi" } })).toBe(true) - expect(isX({ field: { _tag: "Y", y: 1 } })).toBe(false) - expect(isY({ field: { _tag: "Y", y: 1 } })).toBe(true) - }) -}) diff --git a/packages/effect-app/test/secretURL.test.ts b/packages/effect-app/test/secretURL.test.ts deleted file mode 100644 index af2ab186c..000000000 --- a/packages/effect-app/test/secretURL.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import * as Chunk from "effect/Chunk" -import * as Config from "effect/Config" -import * as ConfigProvider from "effect/ConfigProvider" -import * as Effect from "effect/Effect" -import { describe, expect, test } from "vitest" -import { fromChunk, fromString, isSecretURL, make, secretURL, unsafeWipe, value } from "../src/Config/SecretURL.js" - -const testUrls = [ - "https://example.com/path?key=secret123", - "http://user:password@host.com:8080/resource", - "postgres://admin:p4ssw0rd@db.internal:5432/mydb", - "redis://default:token@cache.example.com:6379", - "mongodb+srv://user:pass@cluster.mongodb.net/db", - "https://api.example.com/v2/endpoint?token=abc&format=json" -] - -describe("SecretURL", () => { - describe("fromString / value roundtrip", () => { - test.each(testUrls)("preserves %s", (url) => { - const secret = fromString(url) - expect(value(secret)).toBe(url) - }) - }) - - describe("make / value roundtrip", () => { - test.each(testUrls)("preserves %s via byte array", (url) => { - const bytes = url.split("").map((c) => c.charCodeAt(0)) - const secret = make(bytes) - expect(value(secret)).toBe(url) - }) - }) - - describe("fromChunk / value roundtrip", () => { - test.each(testUrls)("preserves %s via Chunk", (url) => { - const chunk = Chunk.fromIterable(url.split("")) - const secret = fromChunk(chunk) - expect(value(secret)).toBe(url) - }) - }) - - describe("toString", () => { - test("redacts the URL, showing only protocol", () => { - const secret = fromString("https://example.com/secret") - expect(String(secret)).toBe("SecretURL(https://)") - }) - - test("shows http protocol", () => { - const secret = fromString("http://example.com") - expect(String(secret)).toBe("SecretURL(http://)") - }) - - test("shows unknown for non-URL strings", () => { - const secret = fromString("not-a-url") - expect(String(secret)).toBe("SecretURL(unknown://)") - }) - }) - - describe("toJSON", () => { - test("returns tag and protocol only", () => { - const secret = fromString("https://example.com/secret") - expect(JSON.parse(JSON.stringify(secret))).toEqual({ _tag: "SecretURL", protocol: "https" }) - }) - - test("returns unknown protocol for non-URL", () => { - const secret = fromString("not-a-url") - expect(JSON.parse(JSON.stringify(secret))).toEqual({ _tag: "SecretURL", protocol: "unknown" }) - }) - }) - - describe("isSecretURL", () => { - test("returns true for SecretURL", () => { - const secret = fromString("https://example.com") - expect(isSecretURL(secret)).toBe(true) - }) - - test("returns false for plain string", () => { - expect(isSecretURL("https://example.com")).toBe(false) - }) - - test("returns false for plain object", () => { - expect(isSecretURL({ raw: [1, 2, 3] })).toBe(false) - }) - }) - - describe("unsafeWipe", () => { - test("zeroes out raw bytes", () => { - const secret = fromString("https://example.com") - unsafeWipe(secret) - expect(value(secret)).toBe("\0".repeat("https://example.com".length)) - }) - }) - - describe("non-URL strings", () => { - const nonUrls = [ - "just-a-secret-token", - "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc", - "sk-1234567890abcdef", - "some random string with spaces and symbols!@#$%" - ] - - test.each(nonUrls)("preserves non-URL value: %s", (input) => { - const secret = fromString(input) - expect(value(secret)).toBe(input) - expect(String(secret)).toBe("SecretURL(unknown://)") - expect(JSON.parse(JSON.stringify(secret))).toEqual({ _tag: "SecretURL", protocol: "unknown" }) - }) - }) - - describe("special characters", () => { - test("preserves URLs with encoded characters", () => { - const url = "https://example.com/path?q=hello%20world&name=%C3%A9" - expect(value(fromString(url))).toBe(url) - }) - - test("preserves URLs with unicode", () => { - const url = "https://example.com/café" - expect(value(fromString(url))).toBe(url) - }) - - test("preserves URLs with special query params", () => { - const url = "https://example.com?a=1&b=2&c=foo+bar" - expect(value(fromString(url))).toBe(url) - }) - }) - - describe("secretURL config", () => { - const run = (config: Config.Config, provider: ConfigProvider.ConfigProvider) => - Effect.runSync(config.parse(provider)) - - test("reads from env-style ConfigProvider (QUEUE_URL)", () => { - const url = "https://sqs.us-east-1.amazonaws.com/123456789/my-queue" - const provider = ConfigProvider.fromEnv({ env: { QUEUE_URL: url } }) - const result = run(secretURL("QUEUE_URL"), provider) - expect(value(result)).toBe(url) - expect(String(result)).toBe("SecretURL(https://)") - }) - - test("reads from fromUnknown with nested config ({ queue: { url } })", () => { - const url = "redis://default:token@cache.example.com:6379" - const provider = ConfigProvider.fromUnknown({ queue: { url } }) - const config = secretURL("url").pipe(Config.nested("queue")) - const result = run(config, provider) - expect(value(result)).toBe(url) - expect(String(result)).toBe("SecretURL(redis://)") - }) - - test("reads non-URL secret from env", () => { - const token = "sk-1234567890abcdef" - const provider = ConfigProvider.fromEnv({ env: { API_KEY: token } }) - const result = run(secretURL("API_KEY"), provider) - expect(value(result)).toBe(token) - expect(String(result)).toBe("SecretURL(unknown://)") - }) - - test("rejects empty string", () => { - const provider = ConfigProvider.fromEnv({ env: { QUEUE_URL: "" } }) - expect(() => run(secretURL("QUEUE_URL"), provider)).toThrow() - }) - }) -}) diff --git a/packages/effect-app/test/special.test.ts b/packages/effect-app/test/special.test.ts deleted file mode 100644 index a5a1315dc..000000000 --- a/packages/effect-app/test/special.test.ts +++ /dev/null @@ -1,1263 +0,0 @@ -import { InvalidStateError, LoginError, NotFoundError, NotLoggedInError, OptimisticConcurrencyException, ServiceUnavailableError, UnauthorizedError, ValidationError } from "effect-app/client/errors" -import { RequestContext } from "effect-app/RequestContext" -import * as AppSchema from "effect-app/Schema" -import { Class } from "effect-app/Schema/Class" -import { flattenNestedAnyOf, flattenSimpleAllOf, specialJsonSchemaDocument } from "effect-app/Schema/SpecialJsonSchema" -import { deduplicateOpenApiSchemas } from "effect-app/Schema/SpecialOpenApi" -import * as Effect from "effect/Effect" -import * as Option from "effect/Option" -import * as Predicate from "effect/Predicate" -import * as S from "effect/Schema" -import * as SchemaGetter from "effect/SchemaGetter" -import { describe, expect, it } from "vitest" - -class _FacadeModel extends AppSchema.Class<_FacadeModel>("FacadeModel")({ value: S.String }) { - get upperValue() { - return this.value.toUpperCase() - } - - static describe(self: { readonly value: string }) { - return `value:${self.value}` - } -} - -interface FacadeModelEncoded { - readonly value: string -} - -interface FacadeModelMake { - readonly value: string -} - -class FacadeModel extends AppSchema.OpaqueFacade< - FacadeModel, - FacadeModelEncoded, - FacadeModelMake, - never, - never ->()(_FacadeModel) { - declare readonly value: string - declare readonly upperValue: string - - get decoratedValue() { - return `facade:${this.value}` - } - - label() { - return this.decoratedValue - } -} - -class _RenamedFacadeModel extends AppSchema.Class<_RenamedFacadeModel>("RenamedFacadeModel")({ - internalName: S.String -}) { - get upperName() { - return this.internalName.toUpperCase() - } -} - -interface RenamedFacadeModelEncoded { - readonly external_name: string -} - -interface RenamedFacadeModelMake { - readonly internalName: string -} - -class RenamedFacadeModel extends AppSchema.OpaqueFacade< - RenamedFacadeModel, - RenamedFacadeModelEncoded, - RenamedFacadeModelMake, - never, - never ->()(_RenamedFacadeModel.pipe(S.encodeKeys({ internalName: "external_name" }))) { - declare readonly internalName: string - declare readonly upperName: string - - get decoratedName() { - return `facade:${this.internalName}` - } -} - -const _StructFacadeModel = AppSchema.Struct({ value: S.String }) - -interface StructFacadeModelEncoded { - readonly value: string -} - -interface StructFacadeModelMake { - readonly value: string -} - -class StructFacadeModel extends AppSchema.OpaqueFacade< - StructFacadeModel, - StructFacadeModelEncoded, - StructFacadeModelMake, - never, - never ->()(_StructFacadeModel) { - declare readonly value: string -} - -const _RenamedStructFacadeModel = AppSchema.Struct({ internalName: S.String }).pipe( - S.encodeKeys({ internalName: "external_name" }) -) - -interface RenamedStructFacadeModelEncoded { - readonly external_name: string -} - -interface RenamedStructFacadeModelMake { - readonly internalName: string -} - -class RenamedStructFacadeModel extends AppSchema.OpaqueFacade< - RenamedStructFacadeModel, - RenamedStructFacadeModelEncoded, - RenamedStructFacadeModelMake, - never, - never ->()(_RenamedStructFacadeModel) { - declare readonly internalName: string -} - -const _RenamedOpaqueStructFacadeModel = AppSchema - .Opaque< - { readonly internalName: string }, - RenamedStructFacadeModelEncoded - >()(AppSchema.Struct({ internalName: S.String })) - .pipe(S.encodeKeys({ internalName: "external_name" })) - -class RenamedOpaqueStructFacadeModel extends AppSchema.OpaqueFacade< - RenamedOpaqueStructFacadeModel, - RenamedStructFacadeModelEncoded, - RenamedStructFacadeModelMake, - never, - never ->()(_RenamedOpaqueStructFacadeModel) { - declare readonly internalName: string -} - -describe("Class", () => { - describe("strict", () => { - it("encoding doesnt accepts plain objects matching the struct (Fields argument)", () => { - class A extends Class("A")({ a: S.String }) {} - - expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" }) - expect(() => S.encodeUnknownSync(A)({ a: "world" })).toThrow() - expect(() => S.encodeUnknownSync(A)(null)).toThrow() - }) - - it("encoding rejects plain objects matching the struct (Struct argument)", () => { - class A extends Class("A")(S.Struct({ a: S.String })) {} - - expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" }) - expect(() => S.encodeUnknownSync(A)({ a: "world" })).toThrow() - expect(() => S.encodeUnknownSync(A)(null)).toThrow() - }) - - it("decoding still works normally", () => { - class A extends Class("A")({ a: S.String }) {} - - const decoded = S.decodeUnknownSync(A)({ a: "hello" }) - expect(decoded).toBeInstanceOf(A) - expect(decoded.a).toBe("hello") - - expect(() => S.decodeUnknownSync(A)(null)).toThrow() - expect(() => S.decodeUnknownSync(A)({ a: 1 })).toThrow() - }) - - it("S.is accepts class instances and not matching plain objects", () => { - class A extends Class("A")({ a: S.String }) {} - - expect(S.is(A)(new A({ a: "hello" }))).toBe(true) - expect(S.is(A)({ a: "world" })).toBe(false) - expect(S.is(A)({ a: 1 })).toBe(false) - expect(S.is(A)(null)).toBe(false) - }) - - it("rejects values that don't match the struct", () => { - class A extends Class("A")({ a: S.String }) {} - - expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow() - expect(() => S.encodeUnknownSync(A)("not an object")).toThrow() - }) - - it("returns a class constructor — new and instanceof work", () => { - class A extends Class("A")({ a: S.String }) {} - - const instance = new A({ a: "hello" }) - expect(instance).toBeInstanceOf(A) - expect(instance.a).toBe("hello") - }) - - it("preserves fields and identifier", () => { - class A extends Class("A")({ a: S.String, b: S.Number }) {} - - expect(A.identifier).toBe("A") - expect(Object.keys(A.fields)).toStrictEqual(["a", "b"]) - }) - }) - - describe("non strict", () => { - it("encoding accepts plain objects matching the struct (Fields argument)", () => { - class A extends Class("A")({ a: S.String }, undefined, { strict: false }) {} - - expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" }) - expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" }) - expect(() => S.encodeUnknownSync(A)(null)).toThrow() - }) - - it("encoding accepts plain objects matching the struct (Struct argument)", () => { - class A extends Class("A")(S.Struct({ a: S.String }), undefined, { strict: false }) {} - - expect(S.encodeUnknownSync(A)(new A({ a: "hello" }))).toStrictEqual({ a: "hello" }) - expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" }) - expect(() => S.encodeUnknownSync(A)(null)).toThrow() - }) - - it("S.is accepts class instances and matching plain objects", () => { - class A extends Class("A")({ a: S.String }, undefined, { strict: false }) {} - - expect(S.is(A)(new A({ a: "hello" }))).toBe(true) - expect(S.is(A)({ a: "world" })).toBe(true) - expect(S.is(A)({ a: 1 })).toBe(false) - expect(S.is(A)(null)).toBe(false) - }) - }) -}) - -describe("Class constructor", () => { - describe("strict", () => { - it("works as a base class — new, instanceof, encoding plain objects", () => { - class A extends Class("A")({ a: S.String }) {} - - const instance = new A({ a: "hello" }) - expect(instance).toBeInstanceOf(A) - expect(instance.a).toBe("hello") - - expect(S.encodeUnknownSync(A)(instance)).toStrictEqual({ a: "hello" }) - expect(() => S.encodeUnknownSync(A)({ a: "world" })).toThrow() - expect(() => S.encodeUnknownSync(A)(null)).toThrow() - expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow() - }) - - it("decoding works normally", () => { - class A extends Class("A")({ a: S.String }) {} - - const decoded = S.decodeUnknownSync(A)({ a: "hello" }) - expect(decoded).toBeInstanceOf(A) - expect(decoded.a).toBe("hello") - - expect(() => S.decodeUnknownSync(A)({ a: 1 })).toThrow() - }) - - it("exposes fields, identifier", () => { - class A extends Class("A")({ a: S.String, b: S.Number }) {} - - expect(A.identifier).toBe("A") - expect(Object.keys(A.fields)).toStrictEqual(["a", "b"]) - }) - }) - - describe("non strict", () => { - it("works as a base class — new, instanceof, encoding plain objects", () => { - class A extends Class("A")({ a: S.String }, undefined, { strict: false }) {} - - const instance = new A({ a: "hello" }) - expect(instance).toBeInstanceOf(A) - expect(instance.a).toBe("hello") - - expect(S.encodeUnknownSync(A)(instance)).toStrictEqual({ a: "hello" }) - expect(S.encodeUnknownSync(A)({ a: "world" })).toStrictEqual({ a: "world" }) - expect(() => S.encodeUnknownSync(A)(null)).toThrow() - expect(() => S.encodeUnknownSync(A)({ a: 123 })).toThrow() - }) - }) -}) - -describe("TaggedStruct constructor", () => { - it("works with _tag, make, and encoding plain objects", () => { - const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number }) - - const instance = Circle.make({ radius: 5 }) - expect(instance).toEqual({ _tag: "Circle", radius: 5 }) - - expect(S.encodeUnknownSync(Circle)(instance)).toStrictEqual({ _tag: "Circle", radius: 5 }) - expect(S.encodeUnknownSync(Circle)({ _tag: "Circle", radius: 10 })).toStrictEqual({ _tag: "Circle", radius: 10 }) - - expect(() => S.encodeUnknownSync(Circle)(null)).toThrow() - expect(() => S.encodeUnknownSync(Circle)({ _tag: "Circle", radius: "nope" })).toThrow() - }) - - it("decoding works normally", () => { - const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number }) - - const decoded = S.decodeUnknownSync(Circle)({ _tag: "Circle", radius: 5 }) - expect(decoded).toEqual({ _tag: "Circle", radius: 5 }) - }) - - it("S.decodeSync(S.toType(X)) should report n length schema error", () => { - class X extends S.Opaque()(S.TaggedStruct("X", { n: S.String.pipe(S.check(S.isMinLength(3))) })) {} - - try { - S.decodeSync(S.toType(X))({ _tag: "X", n: "a" /* not length 3 */ }) - expect.fail("expected decode to fail with a SchemaError") - } catch (error) { - expect(S.isSchemaError(error)).toBe(true) - if (S.isSchemaError(error)) { - expect(error.message).toContain("n") - expect(error.message.toLowerCase()).toContain("length") - } - } - }) - - it("exposes fields", () => { - const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number }) - - expect(Object.keys(Circle.fields)).toContain("_tag") - expect(Object.keys(Circle.fields)).toContain("radius") - }) -}) - -describe("strict declaration option", () => { - it("Class strict: true keeps class-level expected errors", () => { - class X extends Class("X")({ n: S.String.pipe(S.check(S.isMinLength(3))) }, undefined, { strict: true }) {} - - expect(() => S.decodeSync(S.toType(X))({ n: "a" })).toThrow("Expected X") - }) - - it("TaggedStruct preserves field-level expected errors", () => { - class X extends S.Opaque()(AppSchema.TaggedStruct("X", { n: S.String.pipe(S.check(S.isMinLength(3))) })) {} - - expect(() => S.decodeSync(S.toType(X))({ _tag: "X", n: "a" })).toThrow() - }) - - it("Class with encoded override strict: true keeps class-level expected errors", () => { - class X extends Class("X")({ n: S.String.pipe(S.check(S.isMinLength(3))) }, undefined, { - strict: true - }) {} - - expect(() => S.decodeSync(S.toType(X))({ n: "a" })).toThrow("Expected X") - }) - - it("TaggedStruct with opaque wrapper decodes tagged input", () => { - class X extends S.Opaque()(AppSchema.TaggedStruct("X", { n: S.String.pipe(S.check(S.isMinLength(3))) })) {} - - expect(() => S.decodeSync(S.toType(X))({ _tag: "X", n: "a" })).toThrow() - }) -}) - -describe("Class.copy", () => { - it("creates a new instance with updated fields", () => { - class A extends Class("A")({ a: S.String, b: S.Number }) {} - - const instance = new A({ a: "hello", b: 1 }) - const copied: A = A.copy(instance, { b: 2 }) - expect(copied).toBeInstanceOf(A) - expect(copied.a).toBe("hello") - expect(copied.b).toBe(2) - }) - - it("accepts a function for updates", () => { - class A extends Class("A")({ a: S.String, b: S.Number }) {} - - const instance = new A({ a: "hello", b: 1 }) - const copied: A = A.copy(instance, (a) => ({ b: a.b + 1 })) - expect(copied).toBeInstanceOf(A) - expect(copied.b).toBe(2) - }) - - it("is pipeable", () => { - class A extends Class("A")({ a: S.String, b: S.Number }) {} - - const instance = new A({ a: "hello", b: 1 }) - const copied: A = A.copy({ b: 2 })(instance) - expect(copied).toBeInstanceOf(A) - expect(copied.b).toBe(2) - }) -}) - -describe("TaggedStruct (opaque).copy", () => { - it("creates a new instance with updated fields", () => { - const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number }) - - const instance = Circle.make({ radius: 5 }) - const copied = Circle.copy(instance, { radius: 10 }) - expect(copied).toEqual({ _tag: "Circle", radius: 10 }) - }) - - it("accepts a function for updates", () => { - const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number }) - - const instance = Circle.make({ radius: 5 }) - const copied = Circle.copy(instance, (c) => ({ radius: c.radius * 2 })) - expect(copied).toEqual({ _tag: "Circle", radius: 10 }) - }) -}) - -describe("OpaqueFacade", () => { - it("keeps OpaqueFacade usable with struct schemas", () => { - const decoded = S.decodeSync(StructFacadeModel)({ value: "hello" }) - - expect(decoded).toStrictEqual({ value: "hello" }) - expect(S.encodeSync(StructFacadeModel)(decoded)).toStrictEqual({ value: "hello" }) - expect(S.is(StructFacadeModel)(decoded)).toBe(true) - expect(S.is(StructFacadeModel)({ value: 1 })).toBe(false) - }) - - it("keeps encoded-key struct facades on the struct path", () => { - const decoded = S.decodeSync(RenamedStructFacadeModel)({ external_name: "fran" }) - - expect(decoded).toStrictEqual({ internalName: "fran" }) - expect(decoded).not.toBeInstanceOf(RenamedStructFacadeModel) - expect(S.encodeSync(RenamedStructFacadeModel)(decoded)).toStrictEqual({ external_name: "fran" }) - expect(S.is(RenamedStructFacadeModel)(decoded)).toBe(true) - expect(S.is(RenamedStructFacadeModel)({ external_name: "fran" })).toBe(false) - }) - - it("keeps encoded-key opaque struct facades on the struct path", () => { - const decoded = S.decodeSync(RenamedOpaqueStructFacadeModel)({ external_name: "fran" }) - - expect(decoded).toStrictEqual({ internalName: "fran" }) - expect(decoded).not.toBeInstanceOf(RenamedOpaqueStructFacadeModel) - expect(S.encodeSync(RenamedOpaqueStructFacadeModel)(decoded)).toStrictEqual({ external_name: "fran" }) - expect(S.is(RenamedOpaqueStructFacadeModel)(decoded)).toBe(true) - expect(S.is(RenamedOpaqueStructFacadeModel)({ external_name: "fran" })).toBe(false) - }) - - it("decodes through the public facade class", () => { - const decoded = S.decodeSync(FacadeModel)({ value: "hello" }) - const decodedEffect = Effect.runSync(S.decodeUnknownEffect(FacadeModel)({ value: "effect" })) - - expect(decoded).toBeInstanceOf(FacadeModel) - expect(decodedEffect).toBeInstanceOf(FacadeModel) - expect(decoded).toBeInstanceOf(_FacadeModel) - expect(decoded.value).toBe("hello") - expect(decoded.upperValue).toBe("HELLO") - expect(decoded.decoratedValue).toBe("facade:hello") - expect(decoded.label()).toBe("facade:hello") - expect(FacadeModel.describe(decoded)).toBe("value:hello") - }) - - it("validates facade instances with S.is", () => { - const instance = new FacadeModel({ value: "hello" }) - - expect(instance).toBeInstanceOf(FacadeModel) - expect(instance.upperValue).toBe("HELLO") - expect(instance.label()).toBe("facade:hello") - expect(S.is(FacadeModel)(instance)).toBe(true) - expect(S.is(FacadeModel)(S.decodeSync(FacadeModel)({ value: "world" }))).toBe(true) - expect(S.is(FacadeModel)({ value: "plain" })).toBe(false) - expect(S.is(FacadeModel)({ value: 1 })).toBe(false) - }) - - it("decodes renamed keys through the public facade class", () => { - const decoded = S.decodeSync(RenamedFacadeModel)({ external_name: "fran" }) - - expect(decoded).toBeInstanceOf(RenamedFacadeModel) - expect(decoded).toBeInstanceOf(_RenamedFacadeModel) - expect(Object.getPrototypeOf(decoded)).toBe(RenamedFacadeModel.prototype) - expect(decoded.internalName).toBe("fran") - expect(decoded.upperName).toBe("FRAN") - expect(decoded.decoratedName).toBe("facade:fran") - expect(S.encodeSync(RenamedFacadeModel)(decoded)).toStrictEqual({ external_name: "fran" }) - expect(S.is(RenamedFacadeModel)(decoded)).toBe(true) - expect(S.is(RenamedFacadeModel)({ internalName: "plain" })).toBe(false) - expect(S.is(RenamedFacadeModel)({ external_name: "plain" })).toBe(false) - }) - - it("keeps generated struct facades decodable with public statics", () => { - const input: typeof RequestContext.Encoded = { - span: { traceId: "trace", spanId: "span", sampled: true }, - name: "operation", - locale: "en", - namespace: "tenant" - } - - const decoded = S.decodeSync(RequestContext)(input) - - expect(decoded).not.toBeInstanceOf(RequestContext) - expect(S.is(RequestContext)(decoded)).toBe(true) - expect(RequestContext.toMonitoring(decoded)).toStrictEqual({ operationName: "operation", locale: "en" }) - }) - - it("keeps error facades class-only", () => { - const makeStructErrorFacade = () => - AppSchema.OpaqueErrorFacadeClass< - { readonly value: string }, - { readonly value: string }, - { readonly value: string }, - never, - never - >()( - // Runtime guard: only class schemas are accepted (struct schemas throw). - AppSchema.Struct({ value: S.String }) as never - ) - - expect(makeStructErrorFacade).toThrow("OpaqueErrorFacadeClass requires a class schema") - }) -}) - -describe("Struct.copy", () => { - it("creates a new value with updated fields", () => { - const A = AppSchema.Struct({ a: S.String, b: S.Number }) - - const instance = A.make({ a: "hello", b: 1 }) - const copied = A.copy(instance, { b: 2 }) - - expect(copied).toEqual({ a: "hello", b: 2 }) - expect(copied).not.toBe(instance) - }) - - it("accepts a function for updates", () => { - const A = AppSchema.Struct({ a: S.String, b: S.Number }) - - const instance = A.make({ a: "hello", b: 1 }) - const copied = A.copy(instance, (a) => ({ b: a.b + 1 })) - - expect(copied).toEqual({ a: "hello", b: 2 }) - }) - - it("allows widening updates against the full struct type", () => { - const A = AppSchema.Struct({ - name: S.String, - state: S.Union([ - S.Struct({ _tag: S.tag("a"), a: S.String }), - S.Struct({ _tag: S.tag("b"), b: S.Number }) - ]) - }) - - const instance = A.make({ name: "x", state: { _tag: "a", a: "a" } }) - const copied = A.copy(instance, { state: { _tag: "b", b: 1 } }) - - expect(copied).toEqual({ name: "x", state: { _tag: "b", b: 1 } }) - }) - - it("copy function is preserved when using .annotate()", () => { - const A = AppSchema.Struct({ a: S.String, b: S.Number }).annotate({ title: "A" }) - - const instance = A.make({ a: "hello", b: 1 }) - const copied = A.copy(instance, { b: 2 }) - - expect(copied).toEqual({ a: "hello", b: 2 }) - expect(copied).not.toBe(instance) - }) - - it("copy function is preserved when using .annotateKey()", () => { - const A = AppSchema.Struct({ a: S.String, b: S.Number }).annotateKey({ title: "A" }) - - const instance = A.make({ a: "hello", b: 1 }) - const copied = A.copy(instance, { b: 2 }) - - expect(copied).toEqual({ a: "hello", b: 2 }) - expect(copied).not.toBe(instance) - }) - - it("copy function is preserved when using .mapFields()", () => { - const A = AppSchema.Struct({ a: S.String, b: S.Number }).mapFields((f) => ({ ...f })) - - const instance = A.make({ a: "hello", b: 1 }) - const copied = A.copy(instance, { b: 2 }) - - expect(copied).toEqual({ a: "hello", b: 2 }) - expect(copied).not.toBe(instance) - }) - - it("copy function is preserved through chained calls", () => { - const A = AppSchema - .Struct({ a: S.String, b: S.Number }) - .annotate({ title: "A" }) - .annotateKey({ description: "test" }) - - const instance = A.make({ a: "hello", b: 1 }) - const copied = A.copy(instance, { b: 2 }) - - expect(copied).toEqual({ a: "hello", b: 2 }) - expect(copied).not.toBe(instance) - }) -}) - -describe("TaggedStruct.copy", () => { - it("creates a new tagged value with updated fields", () => { - const Circle = AppSchema.TaggedStruct("Circle", { radius: S.Number }) - - const instance = Circle.make({ radius: 5 }) - const copied = Circle.copy(instance, { radius: 10 }) - - expect(copied).toEqual({ _tag: "Circle", radius: 10 }) - expect(copied).not.toBe(instance) - }) -}) - -describe("TaggedError", () => { - it("InvalidStateError toString includes the message", () => { - const error = new InvalidStateError("something went wrong") - expect(error.toString()).toContain("something went wrong") - }) - - it("NotFoundError toString includes the message", () => { - const error = new NotFoundError({ type: "User", id: "123" }) - expect(error.toString()).toContain("Didn't find User") - expect(error.toString()).toContain("123") - }) - - it("ServiceUnavailableError toString includes the message", () => { - const error = new ServiceUnavailableError("service down") - expect(error.toString()).toContain("service down") - }) - - it("ValidationError toString includes the message", () => { - const error = new ValidationError({ errors: ["field required"] }) - expect(error.toString()).toContain("Validation failed") - expect(error.toString()).toContain("field required") - }) - - it("NotLoggedInError toString includes the message", () => { - const error = new NotLoggedInError("not logged in") - expect(error.toString()).toContain("not logged in") - }) - - it("LoginError toString includes the message", () => { - const error = new LoginError("login failed") - expect(error.toString()).toContain("login failed") - }) - - it("UnauthorizedError toString includes the message", () => { - const error = new UnauthorizedError("forbidden") - expect(error.toString()).toContain("forbidden") - }) - - it("OptimisticConcurrencyException toString includes the message", () => { - const error = new OptimisticConcurrencyException({ message: "conflict" }) - expect(error.toString()).toContain("conflict") - }) - - it("OptimisticConcurrencyException from details toString includes the message", () => { - const error = new OptimisticConcurrencyException({ type: "User", id: "123", code: 409 }) - expect(error.toString()).toContain("Existing User 123 record changed") - }) -}) - -describe("SpecialJsonSchema", () => { - it("nullable to optional — from NullOr", () => { - const nullableDecodedUndefinedEncoded = (schema: S.Top) => { - const isNullableSchema = "members" in schema - && globalThis.Array.isArray((schema as any).members) - && (schema as any).members.length === 2 - && (schema as any).members.some((member: any) => member.ast._tag === "Null") - - const nullableMembers = isNullableSchema ? (schema as any).members as ReadonlyArray : undefined - const innerSchema = nullableMembers - ? nullableMembers.find((member: any) => member.ast._tag !== "Null")! - : schema - - const nullableSchema = isNullableSchema ? schema : S.NullOr(schema) - - return nullableSchema.pipe( - S.encodeTo(S.optionalKey(innerSchema), { - decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)), - encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)) - }) - ) - } - - const fromNullOr = nullableDecodedUndefinedEncoded(S.NullOr(S.String)) - const structFromNullOr = S.Struct({ status: fromNullOr }) - - const encode = S.encodeUnknownSync(structFromNullOr as any) - const encodedNull = encode({ status: null }) - expect("status" in encodedNull).toBe(false) - expect(encode({ status: "test" })).toStrictEqual({ status: "test" }) - - const decode = S.decodeUnknownSync(structFromNullOr as any) - expect(decode({})).toStrictEqual({ status: null }) - expect(decode({ status: "test" })).toStrictEqual({ status: "test" }) - - const doc = specialJsonSchemaDocument(structFromNullOr) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { - "type": "object", - "additionalProperties": true, - "properties": { - "status": { "type": "string" } - } - }, - definitions: {} - }) - }) - - it("identifies X universally — deduplicates same-fingerprint references", () => { - const X = S.String.annotate({ title: "X", identifier: "X" }) - - const s = S.Struct({ - a: S.NullOr(X).pipe( - S.encodeTo(S.optionalKey(X), { - decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)), - encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)) - }) - ), - b: S.NullOr(X).pipe( - S.encodeTo(S.optionalKey(X), { - decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)), - encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)) - }) - ), - c: S.NullOr(X), - d: X, - e: X.pipe(S.optionalKey) - }) - - const doc = specialJsonSchemaDocument(s) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { - "type": "object", - "additionalProperties": true, - "properties": { - "a": { "$ref": "#/$defs/X" }, - "b": { "$ref": "#/$defs/X" }, - "c": { - "anyOf": [ - { "$ref": "#/$defs/X" }, - { "type": "null" } - ] - }, - "d": { "$ref": "#/$defs/X" }, - "e": { "$ref": "#/$defs/X" } - }, - "required": ["c", "d"] - }, - definitions: { - X: { - "type": "string", - "title": "X" - } - } - }) - }) - - it("shared annotated schema via helper — deduplicates", () => { - const X = S.String.annotate({ title: "X", identifier: "X" }) - - const cache = new WeakMap() - const nullableDecodedUndefinedEncoded = (schema: S.Top) => { - const isNullableSchema = "members" in schema - && globalThis.Array.isArray((schema as any).members) - && (schema as any).members.length === 2 - && (schema as any).members.some((member: any) => member.ast._tag === "Null") - - const nullableMembers = isNullableSchema ? (schema as any).members as ReadonlyArray : undefined - const innerSchema = nullableMembers - ? nullableMembers.find((member: any) => member.ast._tag !== "Null")! - : schema - - const cached = cache.get(innerSchema.ast) - if (cached !== undefined) return cached - - const nullableSchema = isNullableSchema ? schema : S.NullOr(schema) - const out = nullableSchema.pipe( - S.encodeTo(S.optionalKey(innerSchema), { - decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)), - encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)) - }) - ) - - cache.set(innerSchema.ast, out) - return out - } - - const structWithShared = S.Struct({ - a: nullableDecodedUndefinedEncoded(X), - b: nullableDecodedUndefinedEncoded(S.NullOr(X)), - c: S.NullOr(X), - d: X, - e: X.pipe(S.optionalKey) - }) - - const doc = specialJsonSchemaDocument(structWithShared) - expect(doc).toStrictEqual({ - dialect: "draft-2020-12", - schema: { - "type": "object", - "additionalProperties": true, - "properties": { - "a": { "$ref": "#/$defs/X" }, - "b": { "$ref": "#/$defs/X" }, - "c": { - "anyOf": [ - { "$ref": "#/$defs/X" }, - { "type": "null" } - ] - }, - "d": { "$ref": "#/$defs/X" }, - "e": { "$ref": "#/$defs/X" } - }, - "required": ["c", "d"] - }, - definitions: { - X: { - "type": "string", - "title": "X" - } - } - }) - }) -}) - -describe("SpecialOpenApi", () => { - it("deduplicates identical components.schemas entries with same base identifier", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: { - "/foo": { - get: { - responses: { - 200: { - content: { - "application/json": { - schema: { $ref: "#/components/schemas/X" } - } - } - } - } - } - }, - "/bar": { - get: { - responses: { - 200: { - content: { - "application/json": { - schema: { $ref: "#/components/schemas/X1" } - } - } - } - } - } - } - }, - components: { - schemas: { - X: { type: "string", title: "X" }, - X1: { type: "string", title: "X" } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) as any - - // X1 should be removed, and $ref to X1 rewritten to X - expect(result.components.schemas).toStrictEqual({ - X: { type: "string", title: "X" } - }) - expect( - result.paths["/bar"].get.responses[200].content["application/json"].schema - ) - .toStrictEqual({ $ref: "#/components/schemas/X" }) - }) - - it("does not deduplicate entries with different representations", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: {}, - components: { - schemas: { - X: { type: "string", title: "X" }, - X1: { type: "number", title: "X" } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) as any - - // Both should remain since they have different representations - expect(result.components.schemas).toStrictEqual({ - X: { type: "string", title: "X" }, - X1: { type: "number", title: "X" } - }) - }) - - it("returns spec unchanged when no duplicates exist", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: {}, - components: { - schemas: { - Foo: { type: "string" }, - Bar: { type: "number" } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) - expect(result).toStrictEqual(spec) - }) - - it("rewrites nested $ref pointers in allOf/anyOf/oneOf", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: { - "/baz": { - post: { - requestBody: { - content: { - "application/json": { - schema: { - anyOf: [ - { $ref: "#/components/schemas/Y1" }, - { type: "null" } - ] - } - } - } - } - } - } - }, - components: { - schemas: { - Y: { type: "object", properties: { name: { type: "string" } } }, - Y1: { type: "object", properties: { name: { type: "string" } } } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) as any - - expect(result.components.schemas).toStrictEqual({ - Y: { type: "object", properties: { name: { type: "string" } } } - }) - expect( - result.paths["/baz"].post.requestBody.content["application/json"].schema.anyOf[0] - ) - .toStrictEqual({ $ref: "#/components/schemas/Y" }) - }) - - it("rewrites $ref pointers inside definitions themselves", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: {}, - components: { - schemas: { - Inner: { type: "string" }, - Inner1: { type: "string" }, - Outer: { - type: "object", - properties: { - field: { $ref: "#/components/schemas/Inner1" } - } - } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) as any - - expect(Object.keys(result.components.schemas)).toStrictEqual(["Inner", "Outer"]) - expect(result.components.schemas.Outer.properties.field).toStrictEqual({ - $ref: "#/components/schemas/Inner" - }) - }) - - it("handles spec without components gracefully", () => { - const spec = { openapi: "3.1.0", info: { title: "Test", version: "1.0" }, paths: {} } - const result = deduplicateOpenApiSchemas(spec) - expect(result).toStrictEqual(spec) - }) - - it("flattens allOf in components.schemas", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: {}, - components: { - schemas: { - PositiveInt: { - type: "integer", - allOf: [{ exclusiveMinimum: 0, title: "PositiveInt" }] - } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) as any - - expect(result.components.schemas.PositiveInt).toStrictEqual({ - type: "integer", - exclusiveMinimum: 0, - title: "PositiveInt" - }) - }) - - it("does not flatten allOf containing $ref entries", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: {}, - components: { - schemas: { - Composed: { - type: "object", - allOf: [{ $ref: "#/components/schemas/Base" }] - }, - Base: { type: "object", properties: { id: { type: "string" } } } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) as any - - expect(result.components.schemas.Composed).toStrictEqual({ - type: "object", - allOf: [{ $ref: "#/components/schemas/Base" }] - }) - }) - - it("does not flatten allOf entries that define their own type", () => { - const spec = { - openapi: "3.1.0", - info: { title: "Test", version: "1.0" }, - paths: {}, - components: { - schemas: { - Mixed: { - type: "object", - allOf: [{ type: "string", minLength: 1 }] - } - } - } - } - - const result = deduplicateOpenApiSchemas(spec) as any - - expect(result.components.schemas.Mixed).toStrictEqual({ - type: "object", - allOf: [{ type: "string", minLength: 1 }] - }) - }) -}) - -describe("flattenSimpleAllOf", () => { - it("flattens constraint-only allOf into parent with type", () => { - const input = { - type: "integer", - allOf: [{ exclusiveMinimum: 0, title: "PositiveInt" }] - } - expect(flattenSimpleAllOf(input)).toStrictEqual({ - type: "integer", - exclusiveMinimum: 0, - title: "PositiveInt" - }) - }) - - it("flattens string type with multiple constraints", () => { - const input = { - type: "string", - allOf: [ - { minLength: 1, maxLength: 255 }, - { title: "NonEmptyString255" } - ] - } - expect(flattenSimpleAllOf(input)).toStrictEqual({ - type: "string", - minLength: 1, - maxLength: 255, - title: "NonEmptyString255" - }) - }) - - it("does not flatten allOf with $ref", () => { - const input = { - type: "object", - allOf: [{ $ref: "#/components/schemas/Base" }] - } - expect(flattenSimpleAllOf(input)).toStrictEqual(input) - }) - - it("does not flatten allOf entries with their own type", () => { - const input = { - type: "object", - allOf: [{ type: "string", minLength: 1 }] - } - expect(flattenSimpleAllOf(input)).toStrictEqual(input) - }) - - it("allOf entry wins on property conflict", () => { - const input = { - type: "integer", - title: "OldTitle", - allOf: [{ title: "NewTitle", minimum: 0 }] - } - expect(flattenSimpleAllOf(input)).toStrictEqual({ - type: "integer", - title: "NewTitle", - minimum: 0 - }) - }) -}) - -describe("Post-processing integration — real Effect Schema types", () => { - it("PositiveInt — allOf flattened, no wrapping", () => { - const doc = specialJsonSchemaDocument(AppSchema.PositiveInt) - expect(doc.definitions["PositiveInt"]).toStrictEqual({ - type: "integer", - exclusiveMinimum: 0, - maximum: Number.MAX_SAFE_INTEGER - }) - }) - - it("NonEmptyString255 — multiple allOf constraints merged", () => { - const doc = specialJsonSchemaDocument(AppSchema.NonEmptyString255) - expect(doc.definitions["NonEmptyString255"]).toStrictEqual({ - type: "string", - minLength: 1, - maxLength: 255 - }) - }) - - it("NullOr(NonEmptyString64k) — null preserved in anyOf, allOf flattened in definition", () => { - const schema = S.Struct({ note: S.NullOr(AppSchema.NonEmptyString64k) }) - const doc = specialJsonSchemaDocument(schema) - - // null variant preserved (correct JSON Schema for NullOr) - expect(doc.schema).toStrictEqual({ - type: "object", - additionalProperties: true, - properties: { - note: { - anyOf: [ - { $ref: "#/$defs/NonEmptyString64k" }, - { type: "null" } - ] - } - }, - required: ["note"] - }) - - // allOf flattened in the referenced definition - expect(doc.definitions["NonEmptyString64k"]).toStrictEqual({ - type: "string", - minLength: 1, - maxLength: 65536 - }) - }) - - it("NonNegativeInt — allOf flattened", () => { - const doc = specialJsonSchemaDocument(AppSchema.NonNegativeInt) - expect(doc.definitions["NonNegativeInt"]).toStrictEqual({ - type: "integer", - minimum: 0, - maximum: Number.MAX_SAFE_INTEGER - }) - }) - - it("NullOr union flattens nested anyOf members", () => { - const A = S.String.annotate({ identifier: "A" }) - const B = S.Boolean.annotate({ identifier: "B" }) - const schema = S.Struct({ - value: S.NullOr(S.Union([A, B])) - }) - const doc = specialJsonSchemaDocument(schema) - const valueProp = (doc.schema as Record)["properties"]["value"] - - expect(valueProp).toStrictEqual({ - anyOf: [ - { $ref: "#/$defs/A" }, - { $ref: "#/$defs/B" }, - { type: "null" } - ] - }) - }) -}) - -describe("flattenNestedAnyOf", () => { - it("flattens nested anyOf with no sibling keys", () => { - const input = { - anyOf: [ - { anyOf: [{ type: "string" }, { type: "number" }] }, - { type: "null" } - ] - } - expect(flattenNestedAnyOf(input)).toStrictEqual({ - anyOf: [ - { type: "string" }, - { type: "number" }, - { type: "null" } - ] - }) - }) - - it("does not flatten anyOf entry with sibling keys", () => { - const input = { - anyOf: [ - { anyOf: [{ type: "string" }], title: "X" }, - { type: "null" } - ] - } - // The inner anyOf is not flattened into the outer (sibling "title" prevents it), - // but the single-element inner anyOf is unwrapped within the entry itself - expect(flattenNestedAnyOf(input)).toStrictEqual({ - anyOf: [ - { type: "string", title: "X" }, - { type: "null" } - ] - }) - }) - - it("unwraps anyOf with single item after flattening", () => { - const input = { - anyOf: [ - { anyOf: [{ type: "string" }] } - ] - } - expect(flattenNestedAnyOf(input)).toStrictEqual({ type: "string" }) - }) - - it("unwraps anyOf with single item, merging sibling properties", () => { - const input = { - title: "MyField", - anyOf: [{ type: "string" }] - } - expect(flattenNestedAnyOf(input)).toStrictEqual({ - title: "MyField", - type: "string" - }) - }) - - it("recurses into nested objects", () => { - const input = { - properties: { - field: { - anyOf: [ - { anyOf: [{ $ref: "#/defs/A" }, { $ref: "#/defs/B" }] }, - { type: "null" } - ] - } - } - } - expect(flattenNestedAnyOf(input)).toStrictEqual({ - properties: { - field: { - anyOf: [ - { $ref: "#/defs/A" }, - { $ref: "#/defs/B" }, - { type: "null" } - ] - } - } - }) - }) - - it("passes through non-objects unchanged", () => { - expect(flattenNestedAnyOf(null)).toBe(null) - expect(flattenNestedAnyOf(42)).toBe(42) - expect(flattenNestedAnyOf("hello")).toBe("hello") - }) -}) diff --git a/packages/effect-app/test/utils.test.ts b/packages/effect-app/test/utils.test.ts deleted file mode 100644 index 2fe525b99..000000000 --- a/packages/effect-app/test/utils.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import * as S from "effect-app/Schema" -import { test } from "vitest" -import { copyOrigin } from "../src/utils.js" - -test("works with class", () => { - class Banana { - name: string - state: { a: string; _tag: "a" } | { b: number; _tag: "b" } - - constructor(params: { name: string; state: { a: string; _tag: "a" } | { b: number; _tag: "b" } }) { - this.name = params.name - this.state = params.state - } - } - - const copyBanana = copyOrigin(Banana) - - const original = new Banana({ name: "banana", state: { a: "a", _tag: "a" } }) - - const res1 = copyBanana( - original, - (_) => ({ state: { b: 1, _tag: "b" } }) - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res1).toEqualTypeOf<{ - name: string - state: { - b: number - _tag: "b" - } - }>() - - expect(res1).toEqual({ - name: "banana", - state: { b: 1, _tag: "b" } - }) - - expect(Object.getPrototypeOf(res1)).toEqual(Banana.prototype) - - const res2 = copyBanana( - original, - { state: { b: 1, _tag: "b" } } - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res2).toEqualTypeOf<{ - name: string - state: { - b: number - _tag: "b" - } - }>() - - expect(res2).toEqual({ - name: "banana", - state: { b: 1, _tag: "b" } - }) - - expect(Object.getPrototypeOf(res2)).toEqual(Banana.prototype) - - const res3 = copyBanana( - res2, - { state: { a: "a", _tag: "a" } } - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res3).toEqualTypeOf<{ - name: string - state: { - a: string - _tag: "a" - } - }>() - - expect(res3).toEqual({ - name: "banana", - state: { a: "a", _tag: "a" } - }) - - expect(Object.getPrototypeOf(res3)).toEqual(Banana.prototype) - - copyBanana( - original, - // @ts-expect-error extraProp is not a valid property of Banana - { name: "string", extraProp: "whatever" } - ) - - copyBanana( - // @ts-expect-error extraProp is not a valid property of Banana - original, - { extraProp: "whatever" } - ) - - const res4 = copyBanana( - original, - (o) => o - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res4).toEqualTypeOf<{ - name: string - state: { - a: string - _tag: "a" - } | { - b: number - _tag: "b" - } - }>() -}) - -test("works with schema class", () => { - class Banana extends S.Class("Banana")({ - name: S.String, - state: S.Union([ - S.Struct({ a: S.String, _tag: S.tag("a") }), - S.Struct({ b: S.Finite, _tag: S.tag("b") }) - ]) - }) {} - - const copyBanana = copyOrigin(Banana) - - const original = new Banana({ name: "banana", state: { a: "a", _tag: "a" } }) - - const res1 = copyBanana( - original, - (_) => ({ state: { b: 1, _tag: "b" } }) - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res1).toEqualTypeOf<{ - name: string - state: { - b: number - _tag: "b" - } - }>() - - expect(res1).toEqual({ - name: "banana", - state: { b: 1, _tag: "b" } - }) - - expect(Object.getPrototypeOf(res1)).toEqual(Banana.prototype) - - const res2 = copyBanana( - original, - { state: { b: 1, _tag: "b" } } - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res2).toEqualTypeOf<{ - name: string - state: { - b: number - _tag: "b" - } - }>() - - expect(res2).toEqual({ - name: "banana", - state: { b: 1, _tag: "b" } - }) - - expect(Object.getPrototypeOf(res2)).toEqual(Banana.prototype) - - const res3 = copyBanana( - res2, - { state: { a: "a", _tag: "a" } } - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res3).toEqualTypeOf<{ - name: string - state: { - a: string - _tag: "a" - } - }>() - - expect(res3).toEqual({ - name: "banana", - state: { a: "a", _tag: "a" } - }) - - expect(Object.getPrototypeOf(res3)).toEqual(Banana.prototype) - - copyBanana( - original, - // @ts-expect-error extraProp is not a valid property of Banana - { name: "string", extraProp: "whatever" } - ) - - copyBanana( - // @ts-expect-error extraProp is not a valid property of Banana - original, - { extraProp: "whatever" } - ) - - const res4 = copyBanana( - original, - (o) => o - ) satisfies Banana // must be assignable to Banana - - expectTypeOf(res4).toEqualTypeOf<{ - name: string - state: { - readonly a: string - readonly _tag: "a" - } | { - readonly b: number - readonly _tag: "b" - } - }>() -}) diff --git a/packages/effect-app/test/waitingGate.test.ts b/packages/effect-app/test/waitingGate.test.ts deleted file mode 100644 index ad640aad3..000000000 --- a/packages/effect-app/test/waitingGate.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as Deferred from "effect/Deferred" -import * as Effect from "effect/Effect" -import * as Fiber from "effect/Fiber" -import { describe, expect, test } from "vitest" -import { makeWaitingGate, suspendWaiting, WaitingGate } from "../src/waitingGate.js" - -const run = (self: Effect.Effect) => Effect.runPromise(self) - -describe("suspendWaiting", () => { - test("brackets depth and tells the controller", async () => { - const depths: Array = [] - const gate = makeWaitingGate((depth) => depths.push(depth)) - gate.bind((depth) => Effect.sync(() => depths.push(depth))) - - await run( - suspendWaiting(Effect.sync(() => { - expect(gate.depth).toBe(1) - })) - .pipe(Effect.provideService(WaitingGate, gate)) - ) - - expect(gate.depth).toBe(0) - expect(depths).toEqual([1, 1, 0, 0]) - }) - - test("nests", async () => { - const gate = makeWaitingGate() - await run( - suspendWaiting(suspendWaiting(Effect.sync(() => { - expect(gate.depth).toBe(2) - }))) - .pipe(Effect.provideService(WaitingGate, gate)) - ) - expect(gate.depth).toBe(0) - }) - - test("restores depth when the effect is interrupted", async () => { - const gate = makeWaitingGate() - await run(Effect.gen(function*() { - const entered = yield* Deferred.make() - const fiber = yield* suspendWaiting(Effect.gen(function*() { - yield* Deferred.succeed(entered, undefined) - return yield* Effect.never - })) - .pipe( - Effect.provideService(WaitingGate, gate), - Effect.forkChild - ) - yield* Deferred.await(entered) - expect(gate.depth).toBe(1) - yield* Fiber.interrupt(fiber) - expect(gate.depth).toBe(0) - })) - }) - - test("does nothing on the default gate", async () => { - await run(Effect.gen(function*() { - const gate = yield* WaitingGate - expect(gate.active).toBe(false) - yield* suspendWaiting(Effect.void) - expect(gate.depth).toBe(0) - })) - }) -}) diff --git a/packages/effect-app/tsconfig.base.json b/packages/effect-app/tsconfig.base.json deleted file mode 100644 index 832135d45..000000000 --- a/packages/effect-app/tsconfig.base.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "extends": "@tsconfig/strictest/tsconfig.json", - "compilerOptions": { - "module": "Node16", - "lib": [ - "ES2023" - ], - "target": "ES2022", - // inline source maps stay better in sync with the source, which should aid with watchers. - // "sourceMap": true, - "inlineSourceMap": true, - "incremental": true, - "composite": true, - "declarationMap": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "noImplicitAny": true, - "useUnknownInCatchVariables": true, - // This is not desirable, as it's useful in generators. - "noImplicitReturns": false, - "noImplicitThis": true, - "resolveJsonModule": true, - "moduleResolution": "Node16", - "noErrorTruncation": true, - "forceConsistentCasingInFileNames": true, - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true - }, - "watchOptions": { - // Use native file system events for files and directories - // This actually detect changes in parent directories ... not great! - // however it seems that the response is very fast on the non-related projects figuring out their files are unchanged... - "watchFile": "useFsEvents", - "watchDirectory": "useFsEvents", - // // Poll files for updates more frequently - // // when they're updated a lot. - "fallbackPolling": "dynamicPriority", - "excludeDirectories": [ - "**/node_modules", - "**/dist", - "**/.build", - "**/.git", - "**/.data", - "**/.logs", - "**/.*" - ], - "excludeFiles": [ - "**/*.tmp", - "openapi.json", - "*.json" - ] - }, - "files": [], - "include": [], - "exclude": [ - "**/node_modules", - "**/build", - "**/dist", - "**/.*" - ] -} diff --git a/packages/effect-app/tsconfig.json b/packages/effect-app/tsconfig.json deleted file mode 100644 index 8565116ac..000000000 --- a/packages/effect-app/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": {}, - "extends": "../../tsconfig.base.json", - "references": [ - { - "path": "./tsconfig.src.json" - }, - { - "path": "./tsconfig.test.json" - } - ] -} diff --git a/packages/effect-app/tsconfig.src.json b/packages/effect-app/tsconfig.src.json deleted file mode 100644 index 872c35832..000000000 --- a/packages/effect-app/tsconfig.src.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "lib": [ - "esnext" - ], - "tsBuildInfoFile": "./dist/.tsbuildinfo", - "esModuleInterop": true, - "rootDir": "./src", - // keep in here, cause madge can't detect it from extended tsconfig - "moduleResolution": "Node16", - "outDir": "./dist" - }, - "include": [ - "./src/**/*.ts" - ], - "exclude": [ - "./dist", - "*.test.ts", - "node_modules", - "build", - "lib", - "dist", - "**/*.d.ts.map", - ".git", - ".data", - "**/.*", - "**/*.tmp" - ], - "references": [] -} diff --git a/packages/effect-app/tsconfig.test.json b/packages/effect-app/tsconfig.test.json deleted file mode 100644 index c071d42d0..000000000 --- a/packages/effect-app/tsconfig.test.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "rootDir": "./test", - "outDir": "./test/dist", - "tsBuildInfoFile": "./test/dist/.tsbuildinfo", - "types": [ - "node", - "vitest/globals" - ] - }, - "include": [ - "./test/**/*.ts" - ], - "exclude": [ - "./dist", - "./test/dist", - "node_modules", - "build", - "lib", - "dist", - "**/*.d.ts.map", - ".git", - ".data", - "**/.*", - "**/*.tmp" - ], - "references": [ - { - "path": "./tsconfig.src.json" - } - ] -} diff --git a/packages/effect-app/vitest.config.ts b/packages/effect-app/vitest.config.ts deleted file mode 100644 index 85903fa64..000000000 --- a/packages/effect-app/vitest.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -import { defineConfig } from "vitest/config" -import makeConfig from "../../vite.config.base" - -export default defineConfig(makeConfig(__dirname)) diff --git a/repos/effect/packages/effect/ARBITRARY.md b/packages/effect/ARBITRARY.md similarity index 100% rename from repos/effect/packages/effect/ARBITRARY.md rename to packages/effect/ARBITRARY.md diff --git a/repos/effect/packages/effect/CHANGELOG.md b/packages/effect/CHANGELOG.md similarity index 86% rename from repos/effect/packages/effect/CHANGELOG.md rename to packages/effect/CHANGELOG.md index fb6f29ad0..84c56cc33 100644 --- a/repos/effect/packages/effect/CHANGELOG.md +++ b/packages/effect/CHANGELOG.md @@ -1,5 +1,599 @@ # effect +## 4.0.6 + +### Patch Changes + +- [#9674](https://github.com/Effect-TS/effect/pull/9674) [`ce13a48`](https://github.com/Effect-TS/effect/commit/ce13a4872e516187851c5d9336d4df309cc8f0a4) Thanks @effect-bot! - Allow `Response.AllParts`, `Response.Part` and `Response.StreamPart` to be called without a toolkit, decoding tool call and tool result parts for any tool name with `unknown` parameters and results. + +- [#9696](https://github.com/Effect-TS/effect/pull/9696) [`f3bbbb4`](https://github.com/Effect-TS/effect/commit/f3bbbb448d431a9d1205ff09ba47cb81af0c7199) Thanks @gcanti! - Fix `Array.unionWith` calling custom equivalence callbacks with values from the wrong input or in the wrong order. + +- [#9511](https://github.com/Effect-TS/effect/pull/9511) [`5547315`](https://github.com/Effect-TS/effect/commit/554731500e43eef47ff68b569116b74006e91d87) Thanks @sledorze! - Speed up field copying in `Data.Class`, `Data.Error`, `Request.Class` and `Persistable` constructors. + +- [#9743](https://github.com/Effect-TS/effect/pull/9743) [`6093cfb`](https://github.com/Effect-TS/effect/commit/6093cfbbb673d36ecd18edea955763db003c8c26) Thanks @effect-bot! - Fix `AsyncResult` equality treating unrelated `Equal` values with a matching tag and hash as equal. + +- [#9607](https://github.com/Effect-TS/effect/pull/9607) [`30319d5`](https://github.com/Effect-TS/effect/commit/30319d506c9177d1cc1e0b10372e1ddb776aaf0a) Thanks @effect-bot! - Fix Effect atoms dropping the latest exposed success when an asynchronous run fails, so an `initialValue` or a success published with `get.setSelf` remains available as the failure's previous success. + +- [#9313](https://github.com/Effect-TS/effect/pull/9313) [`74a7596`](https://github.com/Effect-TS/effect/commit/74a759699a8582dbe660b21dcda21cc9db08f613) Thanks @effect-bot! - Preserve Reactivity and global layers when replacing an atom runtime layer through registry initial values, so injected test layers retain shared invalidation and global services. + +- [#9190](https://github.com/Effect-TS/effect/pull/9190) [`bfb32a3`](https://github.com/Effect-TS/effect/commit/bfb32a3cf5f8d0f02f0dfdb63dcf5237fde755b0) Thanks @danieljvdm! - Log Atom scope finalizer failures at `References.UnhandledLogLevel` when an atom is disposed, using the atom runtime's logger. Interruption-only cleanup is not logged. + +- [#9522](https://github.com/Effect-TS/effect/pull/9522) [`f8bf7f4`](https://github.com/Effect-TS/effect/commit/f8bf7f4bcc3ad543cd94cbeb010cb164e5dedcb0) Thanks @tim-smart! - Add a `skipInitialRun` option to `Atom.serializable` and `Atom.initialValue`. A seeded effect or stream atom then keeps its hydrated or initial value instead of running on mount, until it is refreshed or a dependency changes. Factory `get(...)` calls and `withReactivity` keys are still tracked; dependencies read inside the effect are tracked from its first real run. + +- [#9718](https://github.com/Effect-TS/effect/pull/9718) [`7f3d3df`](https://github.com/Effect-TS/effect/commit/7f3d3dfbb7b9e6e041c4ad9654c4532f72d2f16e) Thanks @fubhy! - Add `Base64Url.encodePadded` and a `padding` option to `SchemaGetter.encodeBase64Url` for consumers that require padded URL-safe Base64. + +- [#9698](https://github.com/Effect-TS/effect/pull/9698) [`db1046d`](https://github.com/Effect-TS/effect/commit/db1046d1fa0e4b5818c51d28017a89579b6261d6) Thanks @effect-bot! - Encode `HttpClientRequest.basicAuth` credentials as UTF-8 before base64 encoding, so non-ASCII usernames and passwords no longer throw or get mangled. + +- [#9326](https://github.com/Effect-TS/effect/pull/9326) [`3c4f88a`](https://github.com/Effect-TS/effect/commit/3c4f88aa4d1d4d958b42fe8d3f1195f0ca990f8a) Thanks @effect-bot! - Block imports of nested internal modules in the development and published package exports. These implementation details are not public APIs; public entry points remain available. + +- [#9441](https://github.com/Effect-TS/effect/pull/9441) [`916e709`](https://github.com/Effect-TS/effect/commit/916e70968acc4e443885c67012d855b7cc6f9bca) Thanks @fubhy! - Add the unstable `Bytes` module with `concat`, `Equivalence`, and `Order` for `Uint8Array` values. `Stream.mkUint8Array` now returns `Uint8Array`. Browser HTTP clients collect streamed request bodies without repeatedly copying accumulated bytes. + +- [#9470](https://github.com/Effect-TS/effect/pull/9470) [`916f843`](https://github.com/Effect-TS/effect/commit/916f8436225153a81a049bdc1feaedcafe4d1ded) Thanks @tim-smart! - Reduce EventLog remote replay overhead by reusing decoded conflict payloads within each batch. + +- [#9617](https://github.com/Effect-TS/effect/pull/9617) [`66ec757`](https://github.com/Effect-TS/effect/commit/66ec7574d23b7943584059b41136bc21ee9d4c53) Thanks @Marve10s! - Fix `Effect.cachedInvalidateWithTTL` caching a stale result when invalidated while the effect is running. + +- [#9499](https://github.com/Effect-TS/effect/pull/9499) [`daac06f`](https://github.com/Effect-TS/effect/commit/daac06f79a6ab8c7d3e1b66b427c06f7d64dcc8f) Thanks @schutzelaars! - Fix `Effect.callback` retaining completed suspension state, such as values received by `Deferred.await` or `PubSub.take`, until the next full GC. + +- [#9727](https://github.com/Effect-TS/effect/pull/9727) [`c2552ec`](https://github.com/Effect-TS/effect/commit/c2552ec2e031b409b2bef67309312b0168480a39) Thanks @Marve10s! - Fix `Cause.pretty`, `Cause.prettyErrors`, loggers and `runMain` overflowing the stack when an error's `cause` chain is cyclic. + +- [#9540](https://github.com/Effect-TS/effect/pull/9540) [`b02c1cd`](https://github.com/Effect-TS/effect/commit/b02c1cde36c890941a235224af8ecb035197582a) Thanks @Marve10s! - Preserve falsy nested error causes (`0`, `false`, `""`, `null`) in `Cause.prettyErrors` and `Cause.pretty`. + +- [#9418](https://github.com/Effect-TS/effect/pull/9418) [`1855f1d`](https://github.com/Effect-TS/effect/commit/1855f1d359535339bc7640b67e6bb7b12752ba19) Thanks @jpowersdev! - Fix `Channel.fromTransformStream` so an upstream failure is reported when a read is pending, instead of the error raised by aborting the transform stream. + +- [#9602](https://github.com/Effect-TS/effect/pull/9602) [`683ed2c`](https://github.com/Effect-TS/effect/commit/683ed2cb491f9529379c1d5c63933689ff57fd2b) Thanks @effect-bot! - Preserve `Prompt.FilePart` byte and URL data when chat history is exported or persisted as JSON. Encoding now produces tagged `{ _tag: "Uint8Array", base64 }` and `{ _tag: "URL", href }` objects that decode back to their original types, and `Chat.Persistence` reports undecodable stored history as a `PersistenceError` instead of a defect. + +- [#9238](https://github.com/Effect-TS/effect/pull/9238) [`ac68940`](https://github.com/Effect-TS/effect/commit/ac68940f7e4246b1beb43f9d451a67f41241ab82) Thanks @effect-bot! - Fix child-process cleanup without `forceKillAfter` to give remaining process-group members up to one second to exit after the leader exits. Clarify that waiting for the leader has no time limit and never escalates to `SIGKILL` when `forceKillAfter` is omitted. + +- [#9611](https://github.com/Effect-TS/effect/pull/9611) [`22a8401`](https://github.com/Effect-TS/effect/commit/22a84017e7222f332611c1bace05f9fa60c7cf67) Thanks @itisrmk! - Fix `Chunk.get` and `Chunk.getUnsafe` throwing on in-range fractional indexes of one-element chunks, and treat `NaN` and infinite indexes as out of bounds. + +- [#9287](https://github.com/Effect-TS/effect/pull/9287) [`c53c194`](https://github.com/Effect-TS/effect/commit/c53c19451171a00776b400a8548394ac514b628a) Thanks @devangpratap! - Fix the CLI lexer treating a short option cluster with an inline value, such as `-vn=3`, as one unknown flag instead of `-v -n=3`. A single dash followed by a multi-character name, such as `-num=3`, is now also split into separate flags; use `--num=3` instead. + +- [#9655](https://github.com/Effect-TS/effect/pull/9655) [`ca6d1cc`](https://github.com/Effect-TS/effect/commit/ca6d1cc5f803cd501e85431642b79b552322a76a) Thanks @effect-bot! - Add `Command.validate` and `CliError.ValidationError` to the unstable CLI. `validate` checks the parsed input before the handler runs, for rules such as mutually exclusive flags. A failure is shown with the command help, like any parse error. + +- [#9650](https://github.com/Effect-TS/effect/pull/9650) [`47940c5`](https://github.com/Effect-TS/effect/commit/47940c55c9e0eb855615094a2aca6e0812307bb2) Thanks @effect-bot! - Add `Flag.withNegation` and `Flag.withoutNegation` (with `Param` counterparts) to rename or remove the `--no-` form of boolean flags in parsing and shell completions. + +- [#9285](https://github.com/Effect-TS/effect/pull/9285) [`e8b24ef`](https://github.com/Effect-TS/effect/commit/e8b24ef9516706971db948efb2d3eab84ad85951) Thanks @okarin1511! - Fix the CLI treating `--=value` as a flag with an empty name. The argument is now passed through unchanged as a plain value, so it no longer fails with `Unrecognized flag: --`. + +- [#9666](https://github.com/Effect-TS/effect/pull/9666) [`2e8aae0`](https://github.com/Effect-TS/effect/commit/2e8aae0080a8d3e6d00c5ee0bc4f49f0b074acc2) Thanks @effect-bot! - Allow `Command.withExamples` to take a function that receives `{ commandPath }`, so help examples match wherever a command is mounted. + +- [#8350](https://github.com/Effect-TS/effect/pull/8350) [`ad7497b`](https://github.com/Effect-TS/effect/commit/ad7497bbc1bc70935f8f1ae395ff501532e2e770) Thanks @ArjunCodess! - Add a `TerminalCapabilities` reference and layer to `effect/cli`; built-in `Command` help, version, and error output now follows it, so `FORCE_COLOR` enables color off a TTY and `TERM=dumb` disables it. + +- [#9248](https://github.com/Effect-TS/effect/pull/9248) [`7f504a3`](https://github.com/Effect-TS/effect/commit/7f504a35edcf44952c55ecbb0916726bac4d3e52) Thanks @bezff! - Fix the ClickHouse migrator creating its ledger with SQLite syntax and wrapping the run in `BEGIN TRANSACTION`. + +- [#9395](https://github.com/Effect-TS/effect/pull/9395) [`99e365d`](https://github.com/Effect-TS/effect/commit/99e365d61a1fdaabeb31dc58e5752cade8a09e1f) Thanks @effect-bot! - Prevent `Effect.forkIn` and `Effect.forkScoped` from running work in an already-closed scope, even when `startImmediately` or `uninterruptible` is enabled. They now return an interrupted fiber without executing the supplied effect. + +- [#9407](https://github.com/Effect-TS/effect/pull/9407) [`954067d`](https://github.com/Effect-TS/effect/commit/954067da7fbd20a20c10d5d0de6ffe4690e6bdfd) Thanks @effect-bot! - Log and retry failures when scheduling the initial ClusterCron run so transient failures no longer leave the cron stopped. + +- [#9677](https://github.com/Effect-TS/effect/pull/9677) [`3e1a035`](https://github.com/Effect-TS/effect/commit/3e1a035d9c470ced858b7757f6d102b3aa9be008) Thanks @effect-bot! - Fix `ClusterCron` with `calculateNextRunFromPrevious: true` not resuming after its pending run was lost. + + In this mode every run is now processed by a single `executor` entity per cron job, so a restart no longer starts a second chain: runs never overlap and missed runs are caught up in order. Pending runs scheduled by earlier releases join the executor's chain. As a result, `Entity.CurrentAddress` inside `execute` no longer identifies the scheduled time; use the new `ClusterCron.CurrentScheduledTime` service instead, which is available in both modes. + + Failed runs are still retried until they succeed or are older than `skipIfOlderThan`, but in this mode a failing run now blocks later runs while it is retried, instead of later runs executing alongside it. + +- [#9577](https://github.com/Effect-TS/effect/pull/9577) [`41a1fe7`](https://github.com/Effect-TS/effect/commit/41a1fe74e6338e8d13d6d4f3daa03f71a826c2c4) Thanks @tim-smart! - `ClusterMetrics.entities` now reads the active entity count when metrics are snapshotted, instead of being updated by a loop once a second. The series is recorded in the `MetricRegistry` of the entity manager's context and removed when the entity manager shuts down. + +- [#9475](https://github.com/Effect-TS/effect/pull/9475) [`bcb3a86`](https://github.com/Effect-TS/effect/commit/bcb3a86335a8ed18da39300984c0fbae187fdc6b) Thanks @effect-bot! - Warn when a forced cluster shard release remains pending beyond `shardLockExpiration`, including the affected shards and pending entity types and singletons. Release failures now emit rate-limited warnings while preserving the requirement that teardown and lock release finish before reacquisition. + +- [#9336](https://github.com/Effect-TS/effect/pull/9336) [`739d3d1`](https://github.com/Effect-TS/effect/commit/739d3d18728cce0b6d907f0a3f24e04ab0b53560) Thanks @effect-bot! - Answer fresh, locally owned persisted cluster requests from a reply handler registered before the request is saved, instead of reading every local reply back from storage or polling for it. Duplicates and streams still read their replies from storage, and a dropped reply handler now falls back to storage immediately instead of waiting for the next poll. `MessageStorage.registerReplyHandler` must register its handler as soon as the effect starts. + +- [#9732](https://github.com/Effect-TS/effect/pull/9732) [`ed4da35`](https://github.com/Effect-TS/effect/commit/ed4da351fb9d29e4dfd6c60bd89ba4c007d264dc) Thanks @effect-bot! - Read persisted cluster messages from storage as soon as their entity type registers, instead of waiting for the next `entityMessagePollInterval`. + +- [#9716](https://github.com/Effect-TS/effect/pull/9716) [`3d84e19`](https://github.com/Effect-TS/effect/commit/3d84e19f8f14bccf0aa57b11825590c3bae162f4) Thanks @tim-smart! - Fix cluster shard releases waiting out `entityTerminationTimeout` when the shard moves while an entity is waiting to restart after a defect. + +- [#9256](https://github.com/Effect-TS/effect/pull/9256) [`cc00906`](https://github.com/Effect-TS/effect/commit/cc0090657eeb89debdd54af5fcb99d544f46c1bf) Thanks @Tanner-Scadden! - Add `ClusterMetrics.shardReleaseDuration`, a timer recording how long each completed shard release takes. + +- [#9244](https://github.com/Effect-TS/effect/pull/9244) [`09ba1f5`](https://github.com/Effect-TS/effect/commit/09ba1f59952c46cd22229d41236742c3e1f53a34) Thanks @effect-bot! - Keep the cluster shards gauge accurate when shards are released or shard locks are lost. + +- [#9513](https://github.com/Effect-TS/effect/pull/9513) [`9ff1a73`](https://github.com/Effect-TS/effect/commit/9ff1a73d995702f4c4c335888fe6a186f3f710c7) Thanks @effect-bot! - Repeat the stalled forced shard release warning every `max(shardLockExpiration, 30 seconds)` while the release stays pending, and add the `ClusterMetrics.shardsReleasing` gauge (`effect_cluster_shards_releasing`) counting the shards a runner is still releasing. + +- [#9443](https://github.com/Effect-TS/effect/pull/9443) [`06bc532`](https://github.com/Effect-TS/effect/commit/06bc532b71cd3684f010956774356864dd359372) Thanks @effect-bot! - Fix a Sharding startup race that could access singleton synchronization before initialization, causing a ReferenceError and delaying shutdown. + +- [#6441](https://github.com/Effect-TS/effect/pull/6441) [`97bfd84`](https://github.com/Effect-TS/effect/commit/97bfd84bb147e10ed562e4c1ffa589cdaf83a596) Thanks @sbking! - Fix SQL Server support in cluster `SqlMessageStorage`. Message claims and duplicate checks now use valid T-SQL. The SQL Server tables use filtered unique indexes instead of nullable `UNIQUE` constraints, and `NVARCHAR(MAX)` instead of `TEXT`, so keyless messages and chunked replies can be stored. + +- [#9645](https://github.com/Effect-TS/effect/pull/9645) [`25efaef`](https://github.com/Effect-TS/effect/commit/25efaef0d81d25b2651ef4de474e929d3180810a) Thanks @effect-bot! - Fix `ClusterWorkflowEngine` losing a `Workflow.interrupt` sent while a run was still saving its `Suspended` reply. + +- [#9394](https://github.com/Effect-TS/effect/pull/9394) [`c10335b`](https://github.com/Effect-TS/effect/commit/c10335be125c7846ae7b914eeb138244b660275b) Thanks @effect-bot! - Fix suspended cluster workflow scopes not being released when an execution completes. Earlier runs retain their resources through suspension and release them on completion or owner teardown, without running durable workflow finalizers on abandonment. + +- [#9490](https://github.com/Effect-TS/effect/pull/9490) [`a808972`](https://github.com/Effect-TS/effect/commit/a80897262648e4222ce48943e538f39a6b3f69d6) Thanks @tim-smart! - Remove regex lookbehind from ConfigProvider variable expansion to prevent module-loading syntax errors on Safari versions before 16.4, while preserving interpolation and escaping behavior. + +- [#9621](https://github.com/Effect-TS/effect/pull/9621) [`20e844c`](https://github.com/Effect-TS/effect/commit/20e844c7ef27b0e4b4f7911d603c45b1de2b0bc3) Thanks @Marve10s! - Fix `Config.schema` dropping record entries for numeric key schemas such as `Schema.Int` or `Schema.Number`. + +- [#9539](https://github.com/Effect-TS/effect/pull/9539) [`b9f73f3`](https://github.com/Effect-TS/effect/commit/b9f73f37c4ac538e6a327ed80ac5a0708858c540) Thanks @Marve10s! - Fix `Cron.prev` skipping an occurrence earlier in the same second as the input. + +- [#9719](https://github.com/Effect-TS/effect/pull/9719) [`5739878`](https://github.com/Effect-TS/effect/commit/5739878ded09d83d71d657e3b8d4264f4e4936d9) Thanks @Marve10s! - Fix `DateTime.formatIsoDate` and `DateTime.formatIsoDateUtc` dropping the day for years before 0000 or after 9999. + +- [#9654](https://github.com/Effect-TS/effect/pull/9654) [`28acdf3`](https://github.com/Effect-TS/effect/commit/28acdf3820b66056037bb7a24cd0e8d3de3fb06d) Thanks @okarin1511! - Fix DateTime.zoneFromString rejecting "GMT" and accept compact UTC offsets without colon + +- [#9582](https://github.com/Effect-TS/effect/pull/9582) [`59b6396`](https://github.com/Effect-TS/effect/commit/59b63967c1817ae5acb1811037f5aaa964219b7e) Thanks @itisrmk! - Accept lowercase `z` and `gmt` zone designators in `DateTime.make` and `DateTime.makeUnsafe`. These strings previously had a second `Z` appended and were rejected. + +- [#9725](https://github.com/Effect-TS/effect/pull/9725) [`0bccc9c`](https://github.com/Effect-TS/effect/commit/0bccc9c3dd5349b20f8d2e509ac5482d0c26a616) Thanks @Marve10s! - Fix `DateTime` values in named time zones turning year zero and BCE dates into CE years. + +- [#9437](https://github.com/Effect-TS/effect/pull/9437) [`fed70de`](https://github.com/Effect-TS/effect/commit/fed70de0dff8af24c5f814bd0d985ab70c70420b) Thanks @effect-bot! - Add `DecisionModel.decideSettled` to retain valid answers alongside per-decision `Result` failures (`Missing`, `Refused`, `NoDistribution`, or `Invalid`). Provider failures still fail the effect; `decide` remains all-or-nothing. + + OpenRouter now passes missing distributions to core validation. OpenAI reports refusals per decision and wrong-kind score answers as invalid rather than missing. + + Update hand-built services to implement `decideSettled`, handle optional provider `probabilities` and refusal answers, and use settled outcomes instead of the removed `OpenAiContentPolicyMetadata` and `metadata.openai.refusedDecisions`. + +- [#9422](https://github.com/Effect-TS/effect/pull/9422) [`82648a2`](https://github.com/Effect-TS/effect/commit/82648a21bf228145a14d2d1390006d67f74d89c7) Thanks @candrewlee14! - `DecisionModel.decide` now returns the response `id`, the `modelId` that answered and provider `metadata`. `OpenRouterDecisionModel` reports `cost` and the serving provider under `metadata.openrouter`. + +- [#9436](https://github.com/Effect-TS/effect/pull/9436) [`afb5f32`](https://github.com/Effect-TS/effect/commit/afb5f326cc14c9849d8d7ddefde1039ae036e017) Thanks @jpowersdev! - Fix `Channel.decodeText` and `Stream.decodeText` dropping incomplete trailing characters. Flush buffered bytes on normal completion, emitting U+FFFD by default or a defect with `fatal: true`. + +- [#9325](https://github.com/Effect-TS/effect/pull/9325) [`28813f5`](https://github.com/Effect-TS/effect/commit/28813f53a5026984fdc13d3971147ea5dc20104c) Thanks @effect-bot! - Rerun reactive queries only once per invalidation or batch flush when multiple keys match the same query, avoiding duplicate query results and atom refreshes. + +- [#9692](https://github.com/Effect-TS/effect/pull/9692) [`0fae934`](https://github.com/Effect-TS/effect/commit/0fae934a13aac12f9036b0733edd5dde5ce029f3) Thanks @effect-bot! - Destroy the `AsyncResource` a fiber captures when it yields once the fiber resumes, so tools like vitest's `detectAsyncLeaks` no longer report `effect/Fiber` leaks. + +- [#9410](https://github.com/Effect-TS/effect/pull/9410) [`f260830`](https://github.com/Effect-TS/effect/commit/f2608305861269fc2fbedcbbdcd71e4fc5f98ffc) Thanks @effect-bot! - Send an initial DevTools Ping immediately so a server that only responds to client messages can complete startup without waiting for the connection timeout. + +- [#9329](https://github.com/Effect-TS/effect/pull/9329) [`19b63eb`](https://github.com/Effect-TS/effect/commit/19b63eb81b2bccf033aadb565f52a6b0375d26a1) Thanks @effect-bot! - Include disposal library types in the Effect and ManagedRuntime declarations so non-Node consumers can type-check imports without explicitly enabling `ESNext.Disposable`. + +- [#9258](https://github.com/Effect-TS/effect/pull/9258) [`14db221`](https://github.com/Effect-TS/effect/commit/14db221f8f584100ff50fd1173b1a99a1caa1d06) Thanks @devangpratap! - Fix `Duration.fromInputUnsafe` losing nanosecond precision for large integer tuple and object inputs. + +- [#9506](https://github.com/Effect-TS/effect/pull/9506) [`679f956`](https://github.com/Effect-TS/effect/commit/679f956c4a7527854ff78db071dd7aa417211a48) Thanks @tim-smart! - Use `Effect.fnUntraced` for internal functions that returned `Effect.gen`. + +- [#9604](https://github.com/Effect-TS/effect/pull/9604) [`b72707c`](https://github.com/Effect-TS/effect/commit/b72707c2937328fd3bc70f29a6792d84c4256484) Thanks @effect-bot! - Document how `Effect.result` and `Effect.option` handle causes that mix typed errors with interruptions or defects, and the parameter-count rule that decides whether `Effect.promise`, `Effect.tryPromise` and `Effect.callback` provide an `AbortSignal`. + +- [#9552](https://github.com/Effect-TS/effect/pull/9552) [`44c8c1d`](https://github.com/Effect-TS/effect/commit/44c8c1d1c6f01ba9887a051b365e9fa89fcfc685) Thanks @wmaurer! - Add an optional `batchKey` to `EmbeddingModel.make` to group concurrent `embed` calls by provider-relevant context. OpenAI and OpenAI-compatible providers now separate batches with different configuration overrides or client transforms. Undeclared context, such as per-call `FetchHttpClient.RequestInit`, still uses the first caller's value. + +- [#9148](https://github.com/Effect-TS/effect/pull/9148) [`1e7909b`](https://github.com/Effect-TS/effect/commit/1e7909beb05bc740dbd330509e60bb185d7eeed1) Thanks @fubhy! - Add `effect/encoding/Tar` for streaming tar decoding and encoding. Decode v7, USTAR, PAX and non-sparse GNU archives; encode USTAR with PAX headers as needed. Entry constructors use reproducible defaults. + +- [#9420](https://github.com/Effect-TS/effect/pull/9420) [`0611a65`](https://github.com/Effect-TS/effect/commit/0611a657f76836f9ca275f64764b82270b4eda10) Thanks @jpowersdev! - Add the experimental `Xml` module to `effect/encoding` for streaming XML 1.0 decoding and encoding over text or UTF-8 bytes. Decoding checks well-formedness, enforces size and depth limits, and never reads external resources or expands DOCTYPE entities. `elements` collects repeated elements, such as feed items, into trees. + +- [#9419](https://github.com/Effect-TS/effect/pull/9419) [`2fadc65`](https://github.com/Effect-TS/effect/commit/2fadc654aa655fbd1912ce51eb1b764a757ac362) Thanks @jpowersdev! - Add experimental `effect/encoding/Zip` for streaming zip encoding and decoding. Encode and decode stored and deflated entries, with ZIP64 when needed; decoding reads entries as the archive arrives and checks each body's CRC-32 and sizes. This API may receive breaking changes in patch releases. + +- [#9578](https://github.com/Effect-TS/effect/pull/9578) [`feb6ce5`](https://github.com/Effect-TS/effect/commit/feb6ce59a38ab15fc6f89545bd5f0992dd58a06a) Thanks @tim-smart! - Add the always-on `ClusterMetrics.entityMailboxSizeMax` gauge, which reports the largest mailbox size among the resident entities of each type, read when metrics are snapshotted. + +- [#9572](https://github.com/Effect-TS/effect/pull/9572) [`5a2ed53`](https://github.com/Effect-TS/effect/commit/5a2ed5323884a435bfcfb5e3a6f2ae29547458ad) Thanks @tim-smart! - Add the unstable `Metric.remove`, `Metric.removeUnsafe` and `Metric.registerUnsafe` APIs for removing series and registering series whose value is read on snapshot. The OTLP and OpenTelemetry exporters now drop delta state for series that are no longer registered. Add the opt-in `ClusterMetrics.entityMailboxSize` gauge, enabled with `ShardingConfig.entityMailboxMetrics` or the `mailboxMetrics` entity option, which reports the mailbox size of each resident entity and removes the series when the entity is released. + +- [#9721](https://github.com/Effect-TS/effect/pull/9721) [`c8d26a4`](https://github.com/Effect-TS/effect/commit/c8d26a46731cb26a9b9612d9fde823d8813cfc04) Thanks @Marve10s! - Fix `Equivalence.combineAll` losing its criteria after the first comparison when given a one-shot iterable. + +- [#9482](https://github.com/Effect-TS/effect/pull/9482) [`0c8b446`](https://github.com/Effect-TS/effect/commit/0c8b446709c737c10d0e11af4313bb29fdff2588) Thanks @tim-smart! - Track the source remote of each imported entry in `SqlEventJournal` and the IndexedDB `EventJournal`, instead of one row per entry per remote. + + - Storage grows with entries rather than entries times remotes. An imported entry is still relayed to other remotes and never uploaded back to its source. + - Journals from earlier releases migrate on first open: SQL drops the remotes table and IndexedDB upgrades to version 2. SQL needs a writable connection. + - An entry acknowledged by a remote, or known to several remotes before migrating, may be uploaded to a remote once more. The server treats it as a duplicate. + +- [#9448](https://github.com/Effect-TS/effect/pull/9448) [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6) Thanks @tim-smart! - Fix the memory and IndexedDB `EventJournal` advancing a remote's sequence for a batch whose callback failed, and the memory journal missing conflicts while a clock-skewed local write leaves it unordered. + +- [#9479](https://github.com/Effect-TS/effect/pull/9479) [`b04191b`](https://github.com/Effect-TS/effect/commit/b04191b2941534754282c51ef2df2f965ac43a5c) Thanks @tim-smart! - Encrypted event log replication now encrypts entries in batches of up to 100 entries or about 256 KB of payload, one AES-GCM call per batch, and encrypted servers store and stream those batches. Each batch authenticates its store id and plaintext entry ids. This changes the encrypted protocol, APIs and server tables without migration: upgrade encrypted clients and servers together, and start encrypted servers on fresh storage because existing per-entry rows are not read. + + ### Breaking changes + + - `EncryptedEntry` and `EncryptedRemoteEntry` are replaced by `EncryptedBatch` and `EncryptedRemoteBatch`, and `WriteEntries.encryptedEntries` becomes `batches`. + - `EventLogEncryption.encrypt` and `decrypt` take the store id and work on batches, and the `decodeChanges` option of `EventLogRemote.makeWith` receives the store id. + - `EventLogServerEncrypted.Storage.write` and `changes` use batches, `PersistedEntry` is removed, and `SqlEventLogServerEncrypted` no longer accepts `insertBatchSize`. + + Event journals now keep the first copy of an entry that is repeated within one remote delivery, and `SqlEventJournal` advances an entry's remote sequence when it arrives again later, so `nextRemoteSequence` passes it. + +- [#9448](https://github.com/Effect-TS/effect/pull/9448) [`05c3409`](https://github.com/Effect-TS/effect/commit/05c340955944374e3ae47a1de0f7cb9b41a3b0e6) Thanks @tim-smart! - `EventJournal.writeFromRemote` now hands its `effect` the whole batch with conflicts read up front, and `EventLog.HandlerTransaction` runs a replayed batch's handlers in one transaction. + +- [#9445](https://github.com/Effect-TS/effect/pull/9445) [`4067e64`](https://github.com/Effect-TS/effect/commit/4067e6428776ae573c8cc72f7eaebd078a4ff8c7) Thanks @tim-smart! - Flush EventLog reactivity invalidations once per replayed remote batch instead of once per entry. + +- [#9471](https://github.com/Effect-TS/effect/pull/9471) [`e04563d`](https://github.com/Effect-TS/effect/commit/e04563dcd6ddeea74c088a12194c5a73e597ba2f) Thanks @tim-smart! - Speed up UUID formatting and memoize `EventJournal.Entry.idString`. + +- [#9491](https://github.com/Effect-TS/effect/pull/9491) [`02d7fc5`](https://github.com/Effect-TS/effect/commit/02d7fc583053c47dea3b5e1d9d6be164aeb321b6) Thanks @tim-smart! - Add `EventLog.groupBatchCompaction` for compaction across primary keys, with explicit forwarding or dropping of undecodable entries. Both compaction APIs now provide `forward(entry)` to preserve entries unchanged at their original batch positions. Align server compaction output ordering with client replay. + +- [#9521](https://github.com/Effect-TS/effect/pull/9521) [`561d2a1`](https://github.com/Effect-TS/effect/commit/561d2a1ddfa3c4340510f23da7adc50c89d97863) Thanks @tim-smart! - Generate EventLog entry ids from a hybrid logical clock, so a local write always sorts after every entry committed to its journal, even when the local clock is behind. + + - The memory, IndexedDB and SQL journals generate each local id after the latest id in their store, without adopting it into the process's id sequence. The IndexedDB and SQL journals read it from the store, because other tabs or processes sharing the store keep their own sequence. This adds one read per `writeMany`. Inside an enclosing SQL transaction, local ids also sort after its uncommitted rows, and a rollback leaves nothing behind. + - Clocks that run ahead are not clamped. A warning is logged when an imported id is more than 24 hours ahead of the local clock, read from the `Clock` service. + - If a store holds an id at the end of the UUID v7 timestamp range, local writes to that store fail with an `EventJournalError` instead of committing ids that sort before it. `makeEntryIdUnsafe` throws a `RangeError` once its own sequence passes the range. Ids created with an explicit `msecs` still clamp. + +- [#9520](https://github.com/Effect-TS/effect/pull/9520) [`8fe2dcc`](https://github.com/Effect-TS/effect/commit/8fe2dcc2137788a71177d42a9f67fb7003224868) Thanks @tim-smart! - Order EventLog conflicts by entry id in every journal and the unencrypted server, so replicas converge. A conflict is now a stored entry for the same event and primary key with a strictly greater id, remote batches replay in id order after compaction, and compaction replacements are ordered and conflict-checked by the source entry they are anchored to. + +- [#9507](https://github.com/Effect-TS/effect/pull/9507) [`a8086fa`](https://github.com/Effect-TS/effect/commit/a8086fa26a2c9a9f5f7f1d7590bbc494a0feb02a) Thanks @tim-smart! - Fix event log server compaction failing with an argument-limit error on large groups, and stop scanning the whole in-memory journal on replay. + +- [#9444](https://github.com/Effect-TS/effect/pull/9444) [`88a33b4`](https://github.com/Effect-TS/effect/commit/88a33b4122f7ffbbc1423add793a79abffd99c75) Thanks @tim-smart! - Decode `EventJournal` entry timestamps without allocating, and merge remote imports into the in-memory journal instead of re-sorting it on every batch. + +- [#9508](https://github.com/Effect-TS/effect/pull/9508) [`e77372a`](https://github.com/Effect-TS/effect/commit/e77372a8a2e7caec11999636e04c6e31c52e315f) Thanks @tim-smart! - Send chunked event-log remote writes with up to four parts in flight instead of one at a time. + +- [#9480](https://github.com/Effect-TS/effect/pull/9480) [`3dd4388`](https://github.com/Effect-TS/effect/commit/3dd438810df34ed3b3d2e3e35ccf84ccb00feb85) Thanks @tim-smart! - Reduce per-entry overhead in `EventLog` remote replay by caching payload decoders per handler and merging handler contexts once per batch. + +- [#9523](https://github.com/Effect-TS/effect/pull/9523) [`ba2b00c`](https://github.com/Effect-TS/effect/commit/ba2b00c0658b65775809e0052ec9612432600c31) Thanks @tim-smart! - Add the unstable `effect/eventlog/EventRow` module for convergent row updates with `set`, `patch` and `remove` events. It supports single-field, composite and derived keys, per-field conflict resolution through typed storage callbacks, and compaction that drops superseded entries. The constructors return `{ event, payload }`, which spreads into `EventLog.write` and works as a `writeMany` item or client argument. + + Breaking: the function returned by `EventLog.makeClient` now takes one `{ event, payload }` object instead of positional `event` and `payload` arguments. + +- [#9224](https://github.com/Effect-TS/effect/pull/9224) [`a049d4a`](https://github.com/Effect-TS/effect/commit/a049d4ad25f17cf4985284a8d15b07aea2a33242) Thanks @effect-bot! - Allow event-log authentication to retry failed session-auth binding lookups after storage recovers, while continuing to cache successful lookups. + +- [#9478](https://github.com/Effect-TS/effect/pull/9478) [`84aeede`](https://github.com/Effect-TS/effect/commit/84aeede040460a2b24bcf0db7a5d0a9b0e3c42d4) Thanks @tim-smart! - Skip decrypting remote changes the local event journal already stores, and pass entry bytes to WebCrypto without copying them. `EventJournal` gains a required `entriesById` method, which custom journal implementations must add. + +- [#9526](https://github.com/Effect-TS/effect/pull/9526) [`43e5e19`](https://github.com/Effect-TS/effect/commit/43e5e198d16df038b60764f5dd0218cdcf828f16) Thanks @tim-smart! - Fail and retry an EventLog remote replay batch when an entry fails to decode or its handler fails, instead of committing the entry without applying it. + +- [#9473](https://github.com/Effect-TS/effect/pull/9473) [`77bbf16`](https://github.com/Effect-TS/effect/commit/77bbf16e50d6b0ea090288cc5d3f11b20706a027) Thanks @tim-smart! - Speed up decoding of eventlog entries and unencrypted write frames by building instances from already validated fields. The wire format is unchanged. + +- [#9477](https://github.com/Effect-TS/effect/pull/9477) [`6af3a47`](https://github.com/Effect-TS/effect/commit/6af3a47d5b5d698c3ba896c37684266cd465bef1) Thanks @tim-smart! - Acknowledge event log writes instead of echoing them to the writing connection. Breaking protocol change: upgrade clients and servers together; mixed versions are unsupported. + + Custom implementations must add `EventJournal.writeRemoteAcks`, add encrypted `Storage.entrySequences` (the sequence each entry was first stored at, used to acknowledge skipped batches), and return `RemoteEntry` values from unencrypted `Storage.entriesAfter`. Set `firstSequence` when sequences start above 0. Custom remotes can still return `void` from `write` to confirm uploads through the change stream. + + `SqlEventLogServerEncrypted` adds a sequence column to its entry id tables, so encrypted servers still need fresh storage. + +- [#9452](https://github.com/Effect-TS/effect/pull/9452) [`aac8d21`](https://github.com/Effect-TS/effect/commit/aac8d21911333dedb649f4f9d302c28d14d46369) Thanks @tim-smart! - Add `EventLog.writeMany` and `EventJournal.writeMany`, which write a batch of local events under one lock, one handler transaction and one journal commit. Clock-based entry ids are now strictly ascending within a process. + +- [#9690](https://github.com/Effect-TS/effect/pull/9690) [`84c8dc4`](https://github.com/Effect-TS/effect/commit/84c8dc47b09067362749620f75068d6f28044660) Thanks @effect-bot! - Document that `HttpClient.followRedirects` needs `FetchHttpClient.RequestInit` with `{ redirect: "manual" }` when used with `FetchHttpClient`. + +- [#9603](https://github.com/Effect-TS/effect/pull/9603) [`78d1988`](https://github.com/Effect-TS/effect/commit/78d198818d4fd547ce2e9431302f66cb00dc7ad3) Thanks @effect-bot! - Wait for replaced `FiberMap` and `FiberHandle` fibers to finish their interruption cleanup before the owning scope finishes closing. + +- [#9729](https://github.com/Effect-TS/effect/pull/9729) [`5676a2a`](https://github.com/Effect-TS/effect/commit/5676a2ac5a918f8f91fc7838d7adf1b6f5e86a6d) Thanks @Marve10s! - Fix `Fiber.runIn` and `Effect.forkIn` hanging when the attached fiber closes its own scope. + +- [#9433](https://github.com/Effect-TS/effect/pull/9433) [`4b437bf`](https://github.com/Effect-TS/effect/commit/4b437bf4645fabc8aaeaa3b5a88e8c9048b9e4f5) Thanks @effect-bot! - Give defects precedence when the runtime combines a typed failure with a defect, so typed-error recovery can no longer swallow a crashing finalizer or a dying concurrent sibling. + +- [#9290](https://github.com/Effect-TS/effect/pull/9290) [`1af11fd`](https://github.com/Effect-TS/effect/commit/1af11fda1bccf823f74121b5125aa3befa084b18) Thanks @effect-bot! - Fix `AtomHttpApi` query response mode inference so literal and union response modes are accepted and produce the corresponding success types. + +- [#9524](https://github.com/Effect-TS/effect/pull/9524) [`716f8df`](https://github.com/Effect-TS/effect/commit/716f8dfa178f849e5b288464373bd132860c2e19) Thanks @tim-smart! - Fix `Atom.Serializable` encode and decode types to use `Schema.Json`, matching the JSON codec used at runtime. + +- [#9404](https://github.com/Effect-TS/effect/pull/9404) [`ee27718`](https://github.com/Effect-TS/effect/commit/ee27718a02125328a7478812b1b7d7c1d8532578) Thanks @effect-bot! - Fix `SchemaGetter.BigInt` to return `SchemaIssue.InvalidValue` for inputs that cannot be converted to a bigint. This also makes `SchemaTransformation.bigintFromString` and `Snowflake.SnowflakeFromString` reject invalid strings with typed schema errors instead of defects. + +- [#9411](https://github.com/Effect-TS/effect/pull/9411) [`4d86d1b`](https://github.com/Effect-TS/effect/commit/4d86d1b8887d48fa24acf6ad54f33ef3d55a7cd8) Thanks @effect-bot! - Start `Stream.callback` and `Channel.callback` registration before the first pull, so synchronous listener setup no longer misses early events. Browser HTTP response stream listeners now stay installed until the stream is finalized. + +- [#9323](https://github.com/Effect-TS/effect/pull/9323) [`5462ed7`](https://github.com/Effect-TS/effect/commit/5462ed7b3a4c37206c47e401d6be293dc540350f) Thanks @effect-bot! - Fix Cause runtime JSON and inspection output to use `reasons` instead of `failures`, including within failed Exits. Schema validation error paths now also use `reasons`; Cause and Exit schema encodings are unchanged. + +- [#9400](https://github.com/Effect-TS/effect/pull/9400) [`658837c`](https://github.com/Effect-TS/effect/commit/658837c3b249d982df56f2990247499746e36672) Thanks @effect-bot! - Preserve input order in concurrent `Effect.filter` results and record `Effect.all` keys, including `mode: "result"`. + +- [#9226](https://github.com/Effect-TS/effect/pull/9226) [`00261cc`](https://github.com/Effect-TS/effect/commit/00261cc12c31ecc7878f80670439731c905dc2bd) Thanks @effect-bot! - Reconnect the DevTools client after startup connection failures or dropped connections, and log connection failures at Debug level. Stop failed or pending connections on shutdown while preserving queued telemetry flushing for healthy connections. + +- [#9261](https://github.com/Effect-TS/effect/pull/9261) [`7cccb93`](https://github.com/Effect-TS/effect/commit/7cccb9385729bc0aed6630edea95d7db15439ac4) Thanks @effect-bot! - Preserve nanosecond precision when parsing large decimal Duration strings in units from milliseconds through weeks, and avoid intermediate overflow when converting large finite durations to numeric units. + +- [#9403](https://github.com/Effect-TS/effect/pull/9403) [`7c98228`](https://github.com/Effect-TS/effect/commit/7c982281cf73958833054175fc86db385e043cf6) Thanks @effect-bot! - Fix `Effect.effectify` discarding synchronous throws after a synchronous callback. These throws now become defects by default, or typed failures when `onSyncError` is provided. + +- [#9469](https://github.com/Effect-TS/effect/pull/9469) [`5a8e248`](https://github.com/Effect-TS/effect/commit/5a8e2481546352e2ad6233dad49420b14c59f624) Thanks @tim-smart! - Fix encrypted SQL event-log storage to return and publish only newly inserted entries, avoiding duplicate broadcasts when clients re-upload a batch. + +- [#9227](https://github.com/Effect-TS/effect/pull/9227) [`8037a65`](https://github.com/Effect-TS/effect/commit/8037a65f40cae9163ab9e0e687e289f717ae570b) Thanks @effect-bot! - Preserve undecodable event payloads unchanged during group compaction so they do not stall replication or prevent valid events from being compacted. + +- [#9393](https://github.com/Effect-TS/effect/pull/9393) [`25912db`](https://github.com/Effect-TS/effect/commit/25912db9d493227a2d6fa45cebcfc7507e638d8a) Thanks @effect-bot! - Preserve remote replay order for unaffected events when compaction is enabled, and replay replacements in compactor write order at their source position. + +- [#9457](https://github.com/Effect-TS/effect/pull/9457) [`7f32606`](https://github.com/Effect-TS/effect/commit/7f32606baac43a67d171ae93b77c7b29791c9692) Thanks @tim-smart! - Fix IndexedDB event journal uploads resending entries the remote has not echoed back. Upload progress is now checkpointed by insertion order, so entries imported later with older ids are still forwarded. Existing databases upgrade automatically; entries the remote has not confirmed may be uploaded once more after the upgrade. + +- [#9239](https://github.com/Effect-TS/effect/pull/9239) [`0386f88`](https://github.com/Effect-TS/effect/commit/0386f88295ac70cbc1f90ab8cceb0981d848c628) Thanks @effect-bot! - Prevent `WorkflowEngine.layerMemory` from running duplicate compensations after workflow replays. Each replay now starts with a fresh workflow scope, as with the cluster engine, so only the final run's compensations run. + + Finalizers registered in runs that suspended still run once the execution finishes, in the same order, but they now observe `Exit.void` instead of the execution's final exit. + +- [#9237](https://github.com/Effect-TS/effect/pull/9237) [`403298a`](https://github.com/Effect-TS/effect/commit/403298ac457fb634eca2c013fed26d37fc80445f) Thanks @effect-bot! - Align safe interruption in `WorkflowEngine.layerMemory` with the cluster engine: workflows that finish without suspending now complete normally instead of being interrupted and running compensation. Preserve unsafe interruption across suspended replays. + +- [#9203](https://github.com/Effect-TS/effect/pull/9203) [`5a3ebc0`](https://github.com/Effect-TS/effect/commit/5a3ebc082124f9e10106b2347736e66fc74ebc36) Thanks @effect-bot! - Allow absolute HTTP URLs in opaque-origin documents, such as sandboxed srcdoc iframes, by ignoring an invalid inferred base URL. Preserve relative URL resolution when the document provides a valid base. + +- [#9390](https://github.com/Effect-TS/effect/pull/9390) [`fac2191`](https://github.com/Effect-TS/effect/commit/fac21911964b742a38e1d4e73bd4f95aec4911dc) Thanks @effect-bot! - Fix rate limiter delay mode to charge oversized token requests against the shared store, preserving queue ordering and delay/reset timing for fixed-window and token-bucket limits. + +- [#9600](https://github.com/Effect-TS/effect/pull/9600) [`fdb12dc`](https://github.com/Effect-TS/effect/commit/fdb12dcd61b6505eed02e6f7a3c88a85d4c48e8d) Thanks @effect-bot! - Fix `RequestResolver.persisted` to persist only refresh results when using `staleWhileRevalidate`. Whole-batch refresh failures now replace stale values, while refreshes that complete no entries leave the original expiry unchanged. + +- [#9519](https://github.com/Effect-TS/effect/pull/9519) [`97909b8`](https://github.com/Effect-TS/effect/commit/97909b89b167da54a56e6f82f1c4db8bfedae04f) Thanks @tim-smart! - Fix dependency tracking for hydrated plain serializable atoms. They now build on first read or mount like wrapped atoms, so hydrated effect and stream atoms run on mount after SSR instead of skipping that initial run. + +- [#9241](https://github.com/Effect-TS/effect/pull/9241) [`c3983f4`](https://github.com/Effect-TS/effect/commit/c3983f4db2b3052a58e8045cce270fd7ccc4e2b2) Thanks @effect-bot! - Preserve fractional token counts in the Redis fixed-window rate limiter so remaining capacity, excess-request rejection, and delay reservations are calculated correctly. + +- [#9568](https://github.com/Effect-TS/effect/pull/9568) [`3a5e65e`](https://github.com/Effect-TS/effect/commit/3a5e65e409c9569e23bb7af61df401347e9ec420) Thanks @tim-smart! - Run each `RequestResolver.fromEffect` entry with its own caller context, without inheriting another caller's services. + +- [#9386](https://github.com/Effect-TS/effect/pull/9386) [`8b40fbc`](https://github.com/Effect-TS/effect/commit/8b40fbc93caf52f3a4c69020849f0051f100be65) Thanks @effect-bot! - Fix `Resource.auto` to wait for the first scheduled run before refreshing, avoiding an extra acquisition immediately after construction. + +- [#9379](https://github.com/Effect-TS/effect/pull/9379) [`3f3e119`](https://github.com/Effect-TS/effect/commit/3f3e119ad61c2167bc6c7170c3fb68cd1eb3421e) Thanks @boozedog! - Fix TestSchema round-trip assertions to use schema equivalence, avoiding false failures for object prototypes and signed zeros. + +- [#9317](https://github.com/Effect-TS/effect/pull/9317) [`7c7b8ed`](https://github.com/Effect-TS/effect/commit/7c7b8eda54e44f9cddc1188f994f7fdddf901055) Thanks @effect-bot! - Fix hooks and parse options being ignored or reused across calls to `Schema.toStandardSchemaV1`. Both `Schema.toStandardSchemaV1` and `Schema.toStandardJSONSchemaV1` now return fresh shallow copies and compose in either order. Access statics that use `this.#field` through the original class. + +- [#9383](https://github.com/Effect-TS/effect/pull/9383) [`d4f71bc`](https://github.com/Effect-TS/effect/commit/d4f71bc42507b638d149fb8806d26ea626d17956) Thanks @effect-bot! - Fix unordered `Stream.mapEffect`, `Stream.flattenEffect`, and `Stream.bindEffect` dropping in-flight results when concurrency is `Infinity` or an oversized positive number. These concurrency values now use the same safe bound as `"unbounded"`, so successful completion waits for pending effects. + +- [#9397](https://github.com/Effect-TS/effect/pull/9397) [`c66241e`](https://github.com/Effect-TS/effect/commit/c66241e791c4b51d8a65051142d01682590f9bf8) Thanks @effect-bot! - Invalidate atom Stream mutation reactivity keys only after successful completion, rather than on failure, defects, interruption, or replacement by a newer write. + +- [#9225](https://github.com/Effect-TS/effect/pull/9225) [`f0e941e`](https://github.com/Effect-TS/effect/commit/f0e941ee2f90f0a891d1228a90da91386f0258cb) Thanks @effect-bot! - Fix unencrypted event-log replication to acknowledge already committed entries without re-running their handlers, even when storage order differs from timestamp order or entries share a timestamp. Exclude older entries from handler conflicts regardless of storage order. + +- [#9466](https://github.com/Effect-TS/effect/pull/9466) [`5f8a6e5`](https://github.com/Effect-TS/effect/commit/5f8a6e5108aa6c6cd4043a3c9f4dc93d43369623) Thanks @effect-bot! - Prevent unhandled errors from Node ws WebSockets when a pending handshake is closed after an open timeout or fiber interruption. + +- [#9549](https://github.com/Effect-TS/effect/pull/9549) [`2800a09`](https://github.com/Effect-TS/effect/commit/2800a0910f7f4ad7cfa21dfc840edf8fe6e67ca7) Thanks @effect-bot! - Flatten redundant nested `anyOf` alternatives in `Schema.toJsonSchemaDocument` output while preserving annotations, references, and `oneOf` boundaries. + +- [#9319](https://github.com/Effect-TS/effect/pull/9319) [`e9749b0`](https://github.com/Effect-TS/effect/commit/e9749b04c10badabcdf71e6958de5ac56a5316e9) Thanks @effect-bot! - Prevent atom hydration from replacing newer AsyncResult values with older dehydrated state. + +- [#9468](https://github.com/Effect-TS/effect/pull/9468) [`eaef09c`](https://github.com/Effect-TS/effect/commit/eaef09c11b6ed2029c513eea0aa3256dcc6f517f) Thanks @tim-smart! - Fix `EventLog` remote reconnects resuming from the sequence captured at startup instead of the journal's current sequence, which replayed already-written entries and re-triggered reactivity invalidations. + +- [#9569](https://github.com/Effect-TS/effect/pull/9569) [`a3c56e5`](https://github.com/Effect-TS/effect/commit/a3c56e5b5e756e879f1cfb90230e8eae620e33df) Thanks @tim-smart! - Pre-response handlers and `HttpMiddleware.withLoggerDisabled` now apply to exactly one server request. `HttpApiTest` no longer accumulates handlers across retry attempts that reuse the same client request. `HttpEffect.appendPreResponseHandlerUnsafe` now takes the fiber context instead of the request. + +- [#9385](https://github.com/Effect-TS/effect/pull/9385) [`bb61152`](https://github.com/Effect-TS/effect/commit/bb611522fcfb4d317b33acdf5ee24a480d042435) Thanks @effect-bot! - Restart the random sequence from the seed on each evaluation of an effect wrapped with `Random.withSeed`. This also ensures repeated evaluations of `TestCrypto.make` and independent builds of `TestCrypto.layer` start fresh deterministic sequences. + +- [#9240](https://github.com/Effect-TS/effect/pull/9240) [`e9c842b`](https://github.com/Effect-TS/effect/commit/e9c842bc294abf658d698bb17732ae13465b3817) Thanks @effect-bot! - Fix SQL persistence reads to check expiry on every execution, so reused `get` and `getMany` effects stop returning expired entries in both SQL layouts. + +- [#9401](https://github.com/Effect-TS/effect/pull/9401) [`6d8c8e2`](https://github.com/Effect-TS/effect/commit/6d8c8e2075b15ae84ae96e93a0d295cb9221b496) Thanks @effect-bot! - Apply GraphQL resolver batch limits independently per protocol URL and `batchKey`, so interleaved lookups for different tenants or servers do not fragment each other's batches. `GraphQLResolver.request` resolves each lookup's URL in the calling fiber, so lookups that rely on a `Context.Reference` default are no longer routed with another lookup's value. + +- [#9536](https://github.com/Effect-TS/effect/pull/9536) [`1f28187`](https://github.com/Effect-TS/effect/commit/1f281878ef559ef356af2d0e11b381aba857cae1) Thanks @schutzelaars! - Fix `HashMap.set` discarding equal-but-distinct replacement values. + +- [#9548](https://github.com/Effect-TS/effect/pull/9548) [`0b9842b`](https://github.com/Effect-TS/effect/commit/0b9842b76bbc1b016c8abc46432052e1f60b2fa3) Thanks @effect-bot! - Load the bundled Scalar and Swagger UI assets when `HttpApiScalar.layer` or `HttpApiSwagger.layer` is built instead of when `effect/http-api` is imported. Importing the module no longer evaluates about 5 MB of embedded JavaScript and CSS. + +- [#9575](https://github.com/Effect-TS/effect/pull/9575) [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799) Thanks @tim-smart! - Add streaming request payloads to `HttpApi`. An endpoint can declare `HttpApiSchema.StreamUint8Array` or the new `HttpApiSchema.StreamNdjson({ schema })` as its `payload`, and the handler receives a lazy `Stream` that reads the body as it pulls. Bad NDJSON lines fail the stream with `HttpApiSchemaError` (400). The new `maxBytes` and `maxElementBytes` limits fail it with the new `HttpApiPayloadTooLarge` (413), and a `content-length` over `maxBytes` is rejected before the handler runs. `StreamNdjson` also works as a success response. + + `HttpApiClient` sends a `Stream` payload without buffering it and infers the stream's requirements. `HttpApiTest` passes stream payloads through unbuffered. `OpenApi.fromApi` marks streaming request bodies with `x-effect-stream`, and `openapi-generator` maps them, and NDJSON or JSON Lines bodies with an `itemSchema`, back to `StreamUint8Array` and `StreamNdjson`. + + The Node server drains up to 256 KiB of a body the handler left unread, then closes the connection, instead of reading the whole upload or leaving the socket stuck. + +- [#9563](https://github.com/Effect-TS/effect/pull/9563) [`84c3405`](https://github.com/Effect-TS/effect/commit/84c34057ceab9fe6782b73f4a04a31304e0d14de) Thanks @tim-smart! - Allow undeclared `HttpServerRespondable` errors in `HttpApiBuilder` to render their own responses when error schema encoding fails. Oversized multipart streaming uploads now return 413 instead of 500, while declared error schemas retain precedence. + +- [`3934c0c`](https://github.com/Effect-TS/effect/commit/3934c0c61857c03468c08c2477f49a6b977d28ff) Thanks @tim-smart! - Return 413 instead of 400 when buffered or decompressed request bodies exceed their size limit, while preserving 400 for other parse failures. Node HTTP servers drain oversized bodies so the response can reach the client without destroying the socket. Node stream readers expose `MaxBytesError` and an optional `destroyOnOverflow` setting; their default destruction behavior is unchanged. + +- [#9728](https://github.com/Effect-TS/effect/pull/9728) [`048e7c8`](https://github.com/Effect-TS/effect/commit/048e7c87125c4ace5a61895feac3d049a317bbfc) Thanks @Marve10s! - Fix `HttpClient.followRedirects` dying on an invalid `Location` header; it now returns the redirect response unchanged. + +- [#9673](https://github.com/Effect-TS/effect/pull/9673) [`1121334`](https://github.com/Effect-TS/effect/commit/1121334ce68c379051011a2ffea37b0a30ea9a47) Thanks @effect-bot! - Add `HttpClientResponse.toWeb`, which returns the original Web `Response` of a fetch-backed response as `Some`, and `None` for other backends. The original body keeps runtime capabilities such as workerd's known length, so it can be written to Cloudflare R2 without copying. + +- [#9339](https://github.com/Effect-TS/effect/pull/9339) [`9a680d6`](https://github.com/Effect-TS/effect/commit/9a680d6985c6a27d7ebb29cd9eb674f3fe524505) Thanks @fubhy! - Add raw HTTP/1.1 upgrades through `HttpClientRequest.upgrade`, `HttpClientResponse.upgrade`, the Node HTTP server, and `HttpApiSchema.Upgrade`. Node clients expose the upgraded connection as a `Socket`; HttpApi clients return `{ headers, socket }` and OpenAPI documents the 101 response. + + Custom `HttpClientResponse` implementations must add `upgrade`. Fetch and XMLHttpRequest reject upgrade requests with a `TransportError` before anything is sent; Bun and Deno servers still support only WebSocket upgrades. + +- [#9709](https://github.com/Effect-TS/effect/pull/9709) [`85e2aee`](https://github.com/Effect-TS/effect/commit/85e2aeee686963c26ca40d204f982deebb9d6117) Thanks @tim-smart! - Add typed HTTP date and `Retry-After` header values to `Headers`, and parse them consistently across the HTTP client, OTLP exporter, GraphQL client, AI clients and static server. + + - `Headers.parseHttpDate` / `Headers.formatHttpDate` read all three RFC 9110 date formats as UTC, whatever the process time zone, and emit IMF-fixdate. + - `Headers.parseRetryAfter`, `Headers.formatRetryAfter` and `Headers.retryAfterDelay` accept whole non-negative seconds of any size, read exactly, or an HTTP date, and the delay is never negative. HTTP dates accept the `23:59:60` leap second, read as the following second. + - `Headers.RetryAfter` holds either a `Duration` (`RetryAfterDuration`) or a `DateTime.Utc` (`RetryAfterDateTime`). `Headers.retryAfterDuration` builds the first from any `Duration.Input`: infinite durations throw, finite negative ones become zero, and fractional seconds are rounded up when formatted. + - Parse failures are reported as `Headers.HeaderParseError`. + - `Schema.DateTimeUtcFromHttpDate` and `Schema.RetryAfterFromString` expose the same parsing as string codecs for HTTP API headers. + - Invalid `Retry-After` values such as `-5`, `1.5`, `1e3` or `Infinity` are now ignored everywhere. `HttpClient.retryTransient` and `HttpClient.withRateLimiter` no longer honour fractional seconds, and the AI clients no longer produce negative or infinite `retryAfter` durations. + - `HttpStaticServer` reads `If-Modified-Since` and `Last-Modified` as UTC; asctime dates were previously shifted by the process time zone. + +- [#9293](https://github.com/Effect-TS/effect/pull/9293) [`34ec440`](https://github.com/Effect-TS/effect/commit/34ec4407a14ce045ca14cc76c77582884a7e1ebe) Thanks @bezff! - Keep HTTP message bodies unread during inspection. + +- [#8691](https://github.com/Effect-TS/effect/pull/8691) [`343a149`](https://github.com/Effect-TS/effect/commit/343a149fa7d5a9a0a45841b1b6800b0ecfa16a32) Thanks @Marve10s! - Document how to define `HttpMiddleware.make` middleware that preserves app error and service types, including with `Effect.fn`. + +- [#9447](https://github.com/Effect-TS/effect/pull/9447) [`915117b`](https://github.com/Effect-TS/effect/commit/915117b523cb6e5cbc98b5ff1c119a8d0d1f6b9a) Thanks @bezff! - Describe `HttpRouter` route misses caused by a path parameter longer than `maxParamLength`. + +- [#9417](https://github.com/Effect-TS/effect/pull/9417) [`7f823c1`](https://github.com/Effect-TS/effect/commit/7f823c10d20ef53a1081e0d0e2008a4116140799) Thanks @fubhy! - Fix HTTP response framing: omit bodies for 204, 205 and 304, and remove `content-length` for 204 and 205. Node now aborts responses whose bodies do not match `content-length`. Deno and Bun preserve raw Web `Response` objects when merging headers, and Deno preserves their status and headers for HEAD requests. + +- [#9415](https://github.com/Effect-TS/effect/pull/9415) [`6e614e6`](https://github.com/Effect-TS/effect/commit/6e614e6feb4281a9e81f5ec3be48e7905ff4292c) Thanks @fubhy! - - Fix HTTPS detection in `HttpServerRequest.toURL` and tracing. An exact `http` or `https` in `x-forwarded-proto` takes precedence; otherwise, infer HTTPS from `originalUrl`. Paths starting with `//` preserve the request host. + - Node server requests now expose an absolute `originalUrl` when a Host header is present, fixing `HttpMiddleware.searchParamsParser`. Use `url` instead of `originalUrl` to read the path. Hostless requests are unchanged. + - For absolute-form Node requests, derive `url` and `headers.host` from the request target, ignoring a conflicting Host header. + +- [#9726](https://github.com/Effect-TS/effect/pull/9726) [`9990b39`](https://github.com/Effect-TS/effect/commit/9990b3977a544247cd4123edde8353a55bf77192) Thanks @Marve10s! - Remove path parameter regex constraints from `HttpApiClient` URLs and `OpenApi` paths. + +- [#6837](https://github.com/Effect-TS/effect/pull/6837) [`22bf781`](https://github.com/Effect-TS/effect/commit/22bf7812aa8a20c676efade943c342cde4ef1cbd) Thanks @aryasaatvik! - Fix `HttpApiSchema.status` forking named schemas, so a schema reused across response statuses emits a single OpenAPI component. + +- [#9518](https://github.com/Effect-TS/effect/pull/9518) [`41f08b7`](https://github.com/Effect-TS/effect/commit/41f08b75aba40e95cd6c743ec6fcf1f5022f7b5b) Thanks @pujitha24! - Fix `Hydration.hydrate` to notify existing atom subscribers immediately instead of on the next read. + +- [#9476](https://github.com/Effect-TS/effect/pull/9476) [`e8482ca`](https://github.com/Effect-TS/effect/commit/e8482cad9651fa8fe8036fe6292a286994897d0d) Thanks @tim-smart! - Index duplicate and conflict detection in the unencrypted event-log server once per incoming batch instead of rescanning stored history for every entry. `EventLogServerUnencrypted.Storage.entriesAfter` now receives the batch as an optional third argument. The built-in storages use it to read only matching entries, and SQL storage creates a `_event_key_idx` index on startup for SQLite, Postgres and MySQL. Custom storages that ignore the argument keep working. + +- [#9474](https://github.com/Effect-TS/effect/pull/9474) [`5ae85fa`](https://github.com/Effect-TS/effect/commit/5ae85fa8ac1fc5e12d5fe8fe7388ac504788ae3b) Thanks @tim-smart! - Speed up conflict detection in the IndexedDB event journal. Remote writes now read only the stored entries that share an event and primary key with the batch, through a new `[event, primaryKey, id]` index, and no longer re-validate stored rows. Existing databases upgrade to version 3, which only adds the index. + +- [#9609](https://github.com/Effect-TS/effect/pull/9609) [`c477a0a`](https://github.com/Effect-TS/effect/commit/c477a0a8010bd0a988221bc00c446848314e1c3c) Thanks @itisrmk! - Fix `Ini.parse` dropping a trailing backslash from unquoted keys, values and section names. + +- [#9267](https://github.com/Effect-TS/effect/pull/9267) [`8fbb39e`](https://github.com/Effect-TS/effect/commit/8fbb39eb92577915831f19a83b51be376e844613) Thanks @effect-bot! - Isolate `AtomHttpApi` and `AtomRpc` mutations by service ID so services sharing endpoint names or RPC tags invoke their own clients with their own arguments. + +- [#9493](https://github.com/Effect-TS/effect/pull/9493) [`1c70828`](https://github.com/Effect-TS/effect/commit/1c7082847453e9c2be0cc267a39ca554c52f4013) Thanks @Marve10s! - Fix `Iterable.dedupeAdjacent` and `Iterable.dedupeAdjacentWith` overflowing the stack on long runs of equal elements. + +- [#9431](https://github.com/Effect-TS/effect/pull/9431) [`8207d1e`](https://github.com/Effect-TS/effect/commit/8207d1e91ead8aac8f01d9e13b49624b64237c8b) Thanks @jpowersdev! - Add `JsonArray` to `effect/encoding`. Its decoders stream the elements of one JSON array, at the root or selected by a JSON Pointer, without buffering the whole document; its encoders write a stream of values as one JSON array. + +- [#9247](https://github.com/Effect-TS/effect/pull/9247) [`7886b84`](https://github.com/Effect-TS/effect/commit/7886b847658664f64392a14efda86f44b3d1e170) Thanks @gcanti! - Add `includeAnnotationKey` to `SchemaRepresentation.fromJsonSchemaDocument` and `fromJsonSchemaMultiDocument` to preserve selected custom JSON metadata as schema annotations. Selected annotations survive normalization of retained values and can be re-exported with the exporter's matching option. Discarded annotated references no longer force resolution, while surviving annotated circular aliases are consistently rejected, including annotations introduced through `allOf`. + +- [#9252](https://github.com/Effect-TS/effect/pull/9252) [`4839c60`](https://github.com/Effect-TS/effect/commit/4839c601bdda845d9a54d77f578f64372475ab04) Thanks @nikelborm! - Made catchIf not put Refinement function arg as generic Err param + +- [#9324](https://github.com/Effect-TS/effect/pull/9324) [`2595d83`](https://github.com/Effect-TS/effect/commit/2595d831f702d4a748f97e33e71fd5250f58af56) Thanks @effect-bot! - Fix `Effect.fromOption` to evaluate a custom `onNone` error factory on each execution rather than during construction. Exceptions thrown by the factory now become runtime defects. + +- [#9723](https://github.com/Effect-TS/effect/pull/9723) [`f5ae278`](https://github.com/Effect-TS/effect/commit/f5ae27827a82db3f8973b70745fc6f14e7d8fa80) Thanks @Marve10s! - Fix `ManagedRuntime` interrupting requests that dispose it before their first async step. + +- [#8862](https://github.com/Effect-TS/effect/pull/8862) [`7a8ce00`](https://github.com/Effect-TS/effect/commit/7a8ce0098a5f22cbb6c110b16e850fb3fcbcadf2) Thanks @lloydrichards! - Add the experimental `McpClient` and `McpClientError` modules, a scoped MCP client for the `2025-11-25` and `2026-07-28` protocols over stdio or Streamable HTTP. It discovers tools, prompts and resources, calls tools with optional schema decoding, and exposes discovered tools to a language model through `McpClient.toolkit`. + + MCP schemas now check `_meta`, `structuredContent`, capability records and other JSON-valued fields only one level deep when decoding, and type them as `unknown` or `Readonly>`. `McpServer` still rejects non-JSON tool results when it serializes 2026-07-28 responses. + +- [#8553](https://github.com/Effect-TS/effect/pull/8553) [`688d302`](https://github.com/Effect-TS/effect/commit/688d30275ff114e50b4f0d343c6227ee33d67348) Thanks @Floriferous! - Stop publishing a non-recursive MCP tool schema root in `$defs` as well as inline, so `tools/list` no longer sends it twice. A custom `toJsonSchema` `$ref` into the root's own definition, such as `#/$defs/Note/properties/title` when `Note` is the tool schema, no longer resolves. + +- [#9634](https://github.com/Effect-TS/effect/pull/9634) [`c7072e4`](https://github.com/Effect-TS/effect/commit/c7072e46356a19615625ae0fa569b1c3a79d50be) Thanks @tim-smart! - Memoize composed layers by identity, as in v3. `Layer.provide`, `Layer.provideMerge` and `Layer.mergeAll` (and the operators built on them) now check the `MemoMap`, so a composed layer referenced from several parents is built once per memo map instead of once per path. Deeply shared `.Default`-style layers no longer rebuild their whole chain on every path, which could leave millions of scopes open at startup. + + A composed layer that is shared now shares everything inside it, including dependencies wrapped in `Layer.fresh`. Wrap the composed layer itself in `Layer.fresh` to get a separate copy. As with leaf layers, a shared build runs on the first requester's fiber, so if that fiber is interrupted, every requester waiting on the shared build sees the interruption. + +- [#9234](https://github.com/Effect-TS/effect/pull/9234) [`425cedb`](https://github.com/Effect-TS/effect/commit/425cedb5801c9527c116a64d280d9e7c2820f922) Thanks @effect-bot! - Fix `WorkflowEngine.layerMemory` so `interruptUnsafe` completes running and suspended workflows with an interrupted exit without requiring an external wake. + +- [#9598](https://github.com/Effect-TS/effect/pull/9598) [`88bc680`](https://github.com/Effect-TS/effect/commit/88bc68001c6c70984b03a5500232e9b5aab14085) Thanks @effect-bot! - Fail SQL migrations with a typed `MigrationError` instead of a defect, so `Effect.catchTag("MigrationError")` and retries can handle them. + +- [#9668](https://github.com/Effect-TS/effect/pull/9668) [`268e09c`](https://github.com/Effect-TS/effect/commit/268e09c94c9e8f46142b5c9f29ce0a22bfbbc3aa) Thanks @effect-bot! - Fix `Model.Class` and `VariantSchema` class `.extend` to accept variant fields and regenerate every variant schema from the merged fields. + +- [#9715](https://github.com/Effect-TS/effect/pull/9715) [`7e94505`](https://github.com/Effect-TS/effect/commit/7e94505edfc9d81ea08cb96a7d935806495bf61e) Thanks @tim-smart! - Fix SQL Server deadlocks in the SQL `PersistedQueue` store. Offers no longer use a serializable `MERGE`, and claims skip rows locked by other claims or settlements. + +- [#9699](https://github.com/Effect-TS/effect/pull/9699) [`afa5da3`](https://github.com/Effect-TS/effect/commit/afa5da3c8a9848215e49535dd3d39179bfd67715) Thanks @effect-bot! - Fix `MultipartParser` handling of `Content-Disposition`: decode RFC 8187 `filename*` values with an optional language tag in UTF-8 or ISO-8859-1, and report malformed disposition headers as `InvalidDisposition` instead of silently producing a nameless field. + +- [#9575](https://github.com/Effect-TS/effect/pull/9575) [`c29ddd8`](https://github.com/Effect-TS/effect/commit/c29ddd899c53c5d8194c4a0dfef2134304dda799) Thanks @tim-smart! - `Ndjson` byte decoders split lines on raw bytes and decode each line as strict UTF-8. They accept `\n`, `\r\n` and a bare `\r`, skip a leading BOM, and take a `maxLineBytes` option. `NdjsonError` now has a `reason` (`"InvalidJson"`, `"InvalidUtf8"` or `"LineTooLarge"`) and an `unterminated` flag. Byte and string decoders emit the lines before the first bad line, then fail. + +- [#9430](https://github.com/Effect-TS/effect/pull/9430) [`48102ac`](https://github.com/Effect-TS/effect/commit/48102ac592ef89fed9d5d10afd2daece0a0ee310) Thanks @effect-bot! - Add non-retryable `AiError.InvalidResponseError` for provider response schema and SSE decoding failures, with optional HTTP context; model-generated output validation remains retryable. Update exhaustive matches on `AiErrorReason` to handle the new tag, and update serialized-error consumers to accept it. + +- [#9270](https://github.com/Effect-TS/effect/pull/9270) [`463eca3`](https://github.com/Effect-TS/effect/commit/463eca3609a6a9681247abbcc71a6f10b261a1d9) Thanks @Marve10s! - Fix `Number.nextPow2` returning the wrong power of two for some large inputs, such as `2 ** 29` and `2 ** 52 + 1`. + +- [#9694](https://github.com/Effect-TS/effect/pull/9694) [`e7162ea`](https://github.com/Effect-TS/effect/commit/e7162eae7f04af1d583b1f31fdd767f91ec1c82e) Thanks @effect-bot! - Render bare numbers with a standard leading integer check as `Schema.Int`, preserving custom annotations and other checks. Only emit the `decodeError` helper in generated HTTP clients when an operation has error responses. + +- [#7340](https://github.com/Effect-TS/effect/pull/7340) [`95752db`](https://github.com/Effect-TS/effect/commit/95752db5eb3b6e59fb8749e9792704f8b167f36c) Thanks @astahmer! - Add an `includeAnnotationKey` option to `OpenApi.fromApi` that selects custom schema annotation keys, such as `x-*` vendor extensions, to include in the generated OpenAPI schemas. It mirrors the `Schema.toJsonSchemaDocument` option of the same name, which `fromApi` previously did not forward. + +- [#9462](https://github.com/Effect-TS/effect/pull/9462) [`4577a28`](https://github.com/Effect-TS/effect/commit/4577a28bcdb50b0d184775f8c1b7ad890d86e255) Thanks @tobimori! - Support the nullable single-member `allOf` tooling convention in `JsonSchema.fromSchemaOpenApi3_0` when no string `type` or `anyOf` is present. Sibling constraints are preserved and may still reject null. + +- [#9199](https://github.com/Effect-TS/effect/pull/9199) [`973d599`](https://github.com/Effect-TS/effect/commit/973d599a3e8611626764a0e3e8f79edbcf30dc7c) Thanks @jayhemnani9910! - Fix OTLP integer attributes outside the signed 64-bit range causing collectors to reject the export. They are now sent as `doubleValue` for numbers and `stringValue` for bigints, and large in-range numbers keep their exact value in JSON. + +- [#9198](https://github.com/Effect-TS/effect/pull/9198) [`2f066a5`](https://github.com/Effect-TS/effect/commit/2f066a58e90316dd88a219fcea94200beda54219) Thanks @jayhemnani9910! - Fix `OtlpSerialization.layerJson` writing `NaN` and `Infinity` as `null`, which made OTLP collectors reject the whole export. + +- [#9334](https://github.com/Effect-TS/effect/pull/9334) [`f0ccbb6`](https://github.com/Effect-TS/effect/commit/f0ccbb6917be5c08364b0b8adbd0c4c19e7f2de1) Thanks @effect-bot! - Make overlapping `ManagedRuntime.dispose` and `ManagedRuntime.disposeEffect` calls wait for the disposal in progress, and make the `dispose` function returned by `HttpEffect.toWebHandlerLayerWith` (and `HttpRouter.toWebHandler`) return the same promise on every call. Fibers started by the runtime, their child fibers and its finalizers still return immediately when they dispose it. + +- [#9615](https://github.com/Effect-TS/effect/pull/9615) [`2dda6ce`](https://github.com/Effect-TS/effect/commit/2dda6cefc641dbea2ea3279a9b96c0332b277a40) Thanks @Marve10s! - Fix `Path.fromFileUrl` throwing a `URIError` instead of failing with `BadArgument` when the URL contains a malformed percent-encoding. + +- [#8676](https://github.com/Effect-TS/effect/pull/8676) [`3da7c0a`](https://github.com/Effect-TS/effect/commit/3da7c0aabab07c8fc682aea7559536bf31705853) Thanks @rchasman! - With `SpanPropagationEnabled`, statement spans record `db.client.connection.wait_time_ms`, and in `@effect/sql-pg` a statement that waited for a new session gets a `db.connect` child span covering the part of the connect it waited for. + +- [#9222](https://github.com/Effect-TS/effect/pull/9222) [`5a67e0b`](https://github.com/Effect-TS/effect/commit/5a67e0b3a8118dd7be709a56d6d7af4f10e9c4f3) Thanks @effect-bot! - Fix `SqlRunnerStorage` hanging on PGlite. + + `SqlClient` now has a `driver` field identifying the driver that created it, such as `"pg"`, `"pglite"` or `"sqlite-node"`. Every Effect SQL driver sets it. On PGlite, runner storage no longer reserves the only connection and uses lock table rows instead of advisory locks, so runner and application queries are no longer blocked. + + Third-party drivers must pass a `driver` name to `SqlClient.make`, and hand-written `SqlClient` implementations must provide a `driver` field: + + ```ts + const make = Effect.gen(function*() { + return yield* SqlClient.make({ + driver: "my-driver", + acquirer, + compiler, + spanAttributes: [] + }) + }) + ``` + +- [#9498](https://github.com/Effect-TS/effect/pull/9498) [`73b3a6e`](https://github.com/Effect-TS/effect/commit/73b3a6e3c38795400251d9c5d73f05c88acd90cd) Thanks @schutzelaars! - Fix unnecessary value retention in `PubSub` replay buffers and `SubscriptionRef`. + +- [#9446](https://github.com/Effect-TS/effect/pull/9446) [`c6f9e4c`](https://github.com/Effect-TS/effect/commit/c6f9e4cbe6f9c8f0a03fefde18df4d35e98a24df) Thanks @jayhemnani9910! - Fix `PubSub.takeBetween` throwing `RangeError` on large backlogs. + +- [#9495](https://github.com/Effect-TS/effect/pull/9495) [`be02676`](https://github.com/Effect-TS/effect/commit/be026768ee29a1b1a860c1fd17f2a0896c01f61e) Thanks @Marve10s! - Fix reused `PubSub.takeUpTo` effects returning fewer messages after consuming replayed messages. + +- [#9458](https://github.com/Effect-TS/effect/pull/9458) [`26de861`](https://github.com/Effect-TS/effect/commit/26de86100cda8617e077ce66f0a6bb8dfe738fb6) Thanks @tim-smart! - Avoid loading Node's internal undici on cold start when importing `effect/Schema` or starting a Node HTTP server before any web API is used. `Schema.FormData` resolves the global constructor during validation, and `@effect/platform-node` loads `node:http` and `node:https` without evaluating their lazy web API exports. + +- [#9322](https://github.com/Effect-TS/effect/pull/9322) [`9313799`](https://github.com/Effect-TS/effect/commit/931379911a74d5e2207859c295b1acc6a5e2917e) Thanks @effect-bot! - Stop failed `Reactivity.query` and `SqlClient.reactiveMailbox` queries from rerunning on later invalidations, and discard pending reruns when a query fails. + +- [#9328](https://github.com/Effect-TS/effect/pull/9328) [`d5c1e5b`](https://github.com/Effect-TS/effect/commit/d5c1e5bd425dfe983dff1f8c853c0b7eaa772f87) Thanks @effect-bot! - Propagate all entry release defects when an `RcMap` owner scope closes, after attempting every release and clearing the map. This also applies to resources managed through `LayerMap`. + +- [#9191](https://github.com/Effect-TS/effect/pull/9191) [`11bb31f`](https://github.com/Effect-TS/effect/commit/11bb31fec0bf2fdfc025009a04de4c69bffa433a) Thanks @danieljvdm! - Fix `Channel.fromReadableStream`, `Channel.fromTransformStream` and `Stream.fromReadableStream` discarding rejected cancel hooks and leaving the reader locked after finalization. A rejected cancel hook is now a defect, and an already errored stream is not reported twice. + +- [#9396](https://github.com/Effect-TS/effect/pull/9396) [`8c23e46`](https://github.com/Effect-TS/effect/commit/8c23e46a8280e3f2c831d1c0ca3f106c92f8e071) Thanks @effect-bot! - Fix `Stream.toReadableStream` to wait for Web Stream demand before pulling the next upstream chunk, avoiding premature upstream side effects. + +- [#9235](https://github.com/Effect-TS/effect/pull/9235) [`9c96011`](https://github.com/Effect-TS/effect/commit/9c960111de202c0f1dd6989e5b8836150e0ca694) Thanks @effect-bot! - Fix the Redis PersistedQueue store dropping entries when a payload or custom id contains a lone UTF-16 surrogate. + +- [#9618](https://github.com/Effect-TS/effect/pull/9618) [`0d50570`](https://github.com/Effect-TS/effect/commit/0d50570bbf078804092fcd4a47ccd2d0019f989c) Thanks @Marve10s! - Fix `Effect.repeat`, `repeatOrElse`, `schedule` and `scheduleFrom` treating a `Cause.Done` failure from the repeated effect as schedule completion. + +- [#9542](https://github.com/Effect-TS/effect/pull/9542) [`9ed2bf1`](https://github.com/Effect-TS/effect/commit/9ed2bf18da0e1ff4fecc31b7cbac2976ec999e00) Thanks @Marve10s! - Fix `RequestResolver.withCache` to call the wrapped resolver's `preCheck` on cache misses, preserving hook completion and nested cache reuse. + +- [#9541](https://github.com/Effect-TS/effect/pull/9541) [`76ec912`](https://github.com/Effect-TS/effect/commit/76ec9120f43f4fdb08b9c5170ff9e3fd82f22457) Thanks @Marve10s! - Fix `RequestResolver.withSpan` dropping every span option except `attributes` and `links` when the options are returned by a function. + +- [#9439](https://github.com/Effect-TS/effect/pull/9439) [`af2756d`](https://github.com/Effect-TS/effect/commit/af2756dd1d196aa3f199ce21c6d2dc3ada3b67b5) Thanks @tim-smart! - Fix `Effect.request` failing when a resolver's batch delay completes synchronously, such as `RequestResolver.setDelay(0)` under `TestClock`, by registering the request before the delay starts. + +- [#9230](https://github.com/Effect-TS/effect/pull/9230) [`3ec8100`](https://github.com/Effect-TS/effect/commit/3ec8100c793b8a22381ad115106b93824d840f64) Thanks @effect-bot! - Keep `Resource.auto` refreshing according to its schedule after a failed refresh, while preserving the previously cached value. + +- [#9460](https://github.com/Effect-TS/effect/pull/9460) [`93abf02`](https://github.com/Effect-TS/effect/commit/93abf020876af0a6b6b8b25621c80760b7996762) Thanks @schutzelaars! - Fix `RpcClient` retaining request state when a request fails without a reply. + +- [#9232](https://github.com/Effect-TS/effect/pull/9232) [`1e94993`](https://github.com/Effect-TS/effect/commit/1e949935a1065e5140245a514ce3a799061143e3) Thanks @effect-bot! - Fix RPC connection defects failing new requests started synchronously by pending callers. Only requests pending when defect delivery begins are failed; new requests await their own responses. Ensure all pending calls are released even if defect delivery is interrupted. + +- [#9656](https://github.com/Effect-TS/effect/pull/9656) [`99a4616`](https://github.com/Effect-TS/effect/commit/99a4616264e31de65fa8d3dc19576309b1a3f84a) Thanks @effect-bot! - Add an opt-in `batch` option to `RpcClient.layerProtocolHttp` and `RpcClient.makeProtocolHttp` that sends concurrent RPC requests from the same client in one HTTP request. + +- [#9297](https://github.com/Effect-TS/effect/pull/9297) [`0a7978e`](https://github.com/Effect-TS/effect/commit/0a7978ea8e70e51700345b12f8c19952dde05255) Thanks @tim-smart! - Fix HTTP RPC cancellation before the first response chunk and during buffered response collection so client aborts interrupt server-side requests and release their resources. + +- [#9562](https://github.com/Effect-TS/effect/pull/9562) [`b4a060b`](https://github.com/Effect-TS/effect/commit/b4a060b95a6473d54e277dc005172b54aab9bddf) Thanks @tim-smart! - Change RPC NDJSON's content type from `application/ndjson` to `application/x-ndjson`; update any exact media-type checks in custom integrations. Reject malformed non-empty lines with `RpcSerialization.NdjsonDecodeError` (preserving the parse error as `cause`) instead of silently dropping messages, while explicitly skipping empty LF and CRLF lines. Existing RPC HTTP clients and servers remain compatible across the header change. + +- [#9665](https://github.com/Effect-TS/effect/pull/9665) [`274f4a4`](https://github.com/Effect-TS/effect/commit/274f4a4dac5ee2d9dbb6358cc3dd3afe345d4705) Thanks @effect-bot! - Add the `Rpc.ParseOptions` annotation, so RPC payloads and responses can be decoded and encoded with schema parse options such as `concurrency`. + +- [#9601](https://github.com/Effect-TS/effect/pull/9601) [`25f29be`](https://github.com/Effect-TS/effect/commit/25f29be77bdcd5c2a7be8e2710e88f5a10c4cb8d) Thanks @effect-bot! - Fix `runMain` skipping finalizers when `SIGINT` or `SIGTERM` arrives during synchronous startup. + +- [#9547](https://github.com/Effect-TS/effect/pull/9547) [`9036440`](https://github.com/Effect-TS/effect/commit/9036440edca01c24371d0c2c1044be5e5d3cb65e) Thanks @effect-bot! - Prevent uncaught errors when unread HTTP responses are garbage-collected in runtimes such as Cloudflare Workers that restrict aborting request I/O outside its request context. + +- [#9593](https://github.com/Effect-TS/effect/pull/9593) [`415f005`](https://github.com/Effect-TS/effect/commit/415f0053eb848d562febcb5b011e15f31a99bead) Thanks @magoz! - Fix `Schema.isBase64` and `Schema.isBase64Url` throwing `RangeError: Maximum call stack size exceeded` on large inputs. + +- [#9245](https://github.com/Effect-TS/effect/pull/9245) [`191308b`](https://github.com/Effect-TS/effect/commit/191308bd0f5ee04557570f2f960c6e06c429bcfd) Thanks @fubhy! - Add the unstable `Schema.toCodecGoJson` codec for Go JSON responses. It normalizes optional-key `null` values to missing keys and required array or record `null` values to empty collections, while preserving schemas that already accept `null`. Encoding is unchanged from `Schema.toCodecJson`. + +- [#9170](https://github.com/Effect-TS/effect/pull/9170) [`4b96570`](https://github.com/Effect-TS/effect/commit/4b96570ed28d93b14e958742933d227dc5e74b15) Thanks @effect-bot! - Preserve literal member inference in Schema.Enum for inline object literals without requiring `as const`. + +- [#9409](https://github.com/Effect-TS/effect/pull/9409) [`d84ec3f`](https://github.com/Effect-TS/effect/commit/d84ec3fab0346c01bc59f6624d1c046ca4140087) Thanks @effect-bot! - Preserve filter-reported error paths when message annotations override schema check messages in Standard Schema and default error formatting. Filter message annotations now also override directly returned messages, including when the annotation is an empty string. Avoid applying struct messages to multiple filter failures. + +- [#9720](https://github.com/Effect-TS/effect/pull/9720) [`392aa1c`](https://github.com/Effect-TS/effect/commit/392aa1cb275ac9fdb9b4b1831d8c06196bba54b9) Thanks @Marve10s! - Fix `Schema.toFormatter` throwing on `ReadonlySet`, `ReadonlyMap`, `HashSet`, `HashMap` and `Chunk` values that contain symbols or null-prototype objects, and keep `Chunk` elements in iteration order. + + Map and set entries are now sorted by their formatted output instead of by their raw values, so they can print in a different order than before, including for primitive keys and values. + +- [#9556](https://github.com/Effect-TS/effect/pull/9556) [`99e9bd9`](https://github.com/Effect-TS/effect/commit/99e9bd98a354d83202e2522f3ade9ee4bf4a862c) Thanks @gcanti! - Remove the type-level uniqueness check from `Schema.toTaggedUnion` so generic member lists compile again. Duplicate discriminants still throw at runtime. + +- [`6f6f45f`](https://github.com/Effect-TS/effect/commit/6f6f45f480a3f191aed6a82ca5c301f60ef2cf84) Thanks @gcanti! - Fix `Schema.toJsonSchemaDocument` to support numeric record keys and BigInt template literals, and approximate tuples with post-rest elements. + +- [#9318](https://github.com/Effect-TS/effect/pull/9318) [`13fc07e`](https://github.com/Effect-TS/effect/commit/13fc07edeb939ca6229b858c9d9a8c5243ac21b5) Thanks @effect-bot! - Preserve synchronous throws from schema transformations, filters, middleware, and custom declaration parsers as defects when decoding or encoding, including at the root schema. Exit, Effect, and Promise parsers now report these defects through their result instead of throwing before returning. + +- [#9596](https://github.com/Effect-TS/effect/pull/9596) [`a4b38dd`](https://github.com/Effect-TS/effect/commit/a4b38dde7f3fdc1b54a49bffdaca912b4c47c76e) Thanks @magoz! - Match finite number strings in linear time. `Schema.isStringFinite`, `Number` record keys, `Number` template literal parts and `Schema.toCodecStringTree` number fields no longer take quadratic time to reject long digit runs such as `"111…x"`. The JSON Schema `pattern` for these schemas changes to the same linear regular expression and accepts exactly the same strings. + +- [#9724](https://github.com/Effect-TS/effect/pull/9724) [`8b18580`](https://github.com/Effect-TS/effect/commit/8b185807bb99f6de3d9564447e2f4e08d2f8863d) Thanks @Marve10s! - Fix `Schema.Tuple` encoding leaving an empty slot when a trailing optional element such as `Schema.OptionFromOptionalKey` is omitted. + +- [#9391](https://github.com/Effect-TS/effect/pull/9391) [`0c477fe`](https://github.com/Effect-TS/effect/commit/0c477fe417d3ecab86bb8d9afd0c392e9cb8a3da) Thanks @effect-bot! - Propagate all release defects from `ScopedCache.invalidateAll` and cache owner-scope shutdown after attempting every entry release. Successful bulk invalidation now returns `undefined` as declared. + +- [#9243](https://github.com/Effect-TS/effect/pull/9243) [`9a5331d`](https://github.com/Effect-TS/effect/commit/9a5331db94bfdca4f48317d062fbeb96054a9049) Thanks @fubhy! - Add `effect/SemVer` for strict Semantic Versioning 2.0.0 parsing, formatting, comparison, and version bumps, and `effect/SemVerRange` for npm-style range matching and version selection. Add schemas and string codecs for both types. + +- [#9272](https://github.com/Effect-TS/effect/pull/9272) [`a706d73`](https://github.com/Effect-TS/effect/commit/a706d73db8a8d460877c5d7e8a793a52d45be307) Thanks @Marve10s! - Fix `Sink.collect` and `Sink.take` throwing a `RangeError` on large input chunks. + +- [#9320](https://github.com/Effect-TS/effect/pull/9320) [`c9a1e72`](https://github.com/Effect-TS/effect/commit/c9a1e72c5965aa9fe01a669ebfd523f803f5a189) Thanks @effect-bot! - Fall back to client computation when atom hydration data fails schema decoding or a streamed initial result cannot be encoded, instead of throwing or leaving the atom waiting indefinitely. + +- [#9487](https://github.com/Effect-TS/effect/pull/9487) [`89eaaf8`](https://github.com/Effect-TS/effect/commit/89eaaf8581908e248c0b1e47baa7906e0c541635) Thanks @jayhemnani9910! - Fix `Socket.readerString` and `Socket.toChannelString` corrupting characters split across binary frames. Incomplete characters are emitted as U+FFFD before string frames or on close code 1000. + +- [#9292](https://github.com/Effect-TS/effect/pull/9292) [`8215811`](https://github.com/Effect-TS/effect/commit/8215811d51d28d37b4732a6ab03de4047ada62ea) Thanks @tim-smart! - `SqlEventLogServerUnencrypted` publication and `Reactivity` invalidations inside a SQL transaction now wait for the outermost commit and are dropped on rollback. + +- [#9289](https://github.com/Effect-TS/effect/pull/9289) [`ad69018`](https://github.com/Effect-TS/effect/commit/ad6901857e05e7b436a0149b5ece7f3a36d9ff33) Thanks @tim-smart! - Add `SqlClient.afterCommit`, which runs an effect once the outermost transaction commits, or immediately outside a transaction. + +- [#9576](https://github.com/Effect-TS/effect/pull/9576) [`a733eba`](https://github.com/Effect-TS/effect/commit/a733eba9f699737a80a9e7276521980bd929075a) Thanks @effect-bot! - Add `SqlClient.BeginTransaction`, a reference that replaces the statement starting a top-level transaction, such as `BEGIN ISOLATION LEVEL SERIALIZABLE`, saving the round trip of a separate `SET TRANSACTION`. Nested transactions keep their savepoints. `SqlClient.makeWithTransaction` passes the statement to `begin` as a second argument. + +- [#9472](https://github.com/Effect-TS/effect/pull/9472) [`7282d5f`](https://github.com/Effect-TS/effect/commit/7282d5f1bcc06d7065429ef81a3d17c64043fde4) Thanks @tim-smart! - Improve `SqlEventJournal` uploads and query performance. + + - Checkpoint successful uploads per remote without waiting for an echo, while forwarding older entries imported later. + - Index conflict, entry-ordering and remote-sequence queries. + - Migrate existing databases on first open using a writable connection. The first upload after migration may resend unconfirmed entries. + - Run `write` and `writeMany` callbacks in the journal transaction, rolling back their SQL on the same client if the write fails or is interrupted. + +- [#9233](https://github.com/Effect-TS/effect/pull/9233) [`da33572`](https://github.com/Effect-TS/effect/commit/da33572776763e45802ce8c2fe14ec101f9a5c34) Thanks @effect-bot! - Fix SQL event journals reporting incoming remote entries as conflicts. Remote writes now report only previously stored newer entries and atomically persist entries and remote sequences after all callbacks succeed, allowing failed writes to be retried. + +- [#9606](https://github.com/Effect-TS/effect/pull/9606) [`d9e2d12`](https://github.com/Effect-TS/effect/commit/d9e2d1221768e9a3e5e00165cb95b86e0c126cc0) Thanks @effect-bot! - Fence SQL `PersistedQueue` settlements and lock refreshes by acquisition, so a handler whose lock lapsed can no longer complete, fail, retry or release a row after the same store re-claims it. + +- [#9517](https://github.com/Effect-TS/effect/pull/9517) [`dc923b8`](https://github.com/Effect-TS/effect/commit/dc923b847ac7d9673b4f0aa7c136557287b2e5e3) Thanks @effect-bot! - Allow queued SQL transactions to be interrupted while waiting for a connection, respecting the caller's interruptibility. Interrupted or failed acquisition releases the connection scope and its permits, and a libSQL transaction interrupted during `BEGIN` is rolled back. + + `SqlClient.makeWithTransaction` (unstable) now owns the connection scope: `acquireConnection` receives the mask's `restore` function and returns a scoped connection effect instead of a `[scope, connection]` tuple. + +- [#9599](https://github.com/Effect-TS/effect/pull/9599) [`1b74086`](https://github.com/Effect-TS/effect/commit/1b740862e3d25ec5d4b6f86b9fe792b6dabbf84c) Thanks @effect-bot! - Preserve typed SQL errors without cleanup defects after SQLite automatically rolls back a transaction in sqlite-node, sqlite-bun and in-memory sqlite-wasm. OPFS and React Native clients are unchanged. + +- [#9485](https://github.com/Effect-TS/effect/pull/9485) [`8a5507b`](https://github.com/Effect-TS/effect/commit/8a5507ba00f641edf71227d79b9f90efdb634fe2) Thanks @itisrmk! - Fix `SqlMessageStorage` ID precision loss on SQLite drivers that return numbers, including Cloudflare Durable Objects. + +- [#9398](https://github.com/Effect-TS/effect/pull/9398) [`23460fa`](https://github.com/Effect-TS/effect/commit/23460fad704c730926bffe3cc6874268ef6680ec) Thanks @effect-bot! - Treat persisted cache entries that no longer decode as cache misses, recomputing and replacing them after success or error schema changes. + +- [#9264](https://github.com/Effect-TS/effect/pull/9264) [`0dbbea6`](https://github.com/Effect-TS/effect/commit/0dbbea65a93c59147a4bc407cfcda38ea26341b2) Thanks @effect-bot! - Redirect static directory index URLs without a trailing slash to their canonical form with a 301 response, preserving router prefixes and query strings so relative links resolve correctly. Clients that do not follow redirects must request the trailing-slash URL directly. + +- [#9622](https://github.com/Effect-TS/effect/pull/9622) [`70aad3f`](https://github.com/Effect-TS/effect/commit/70aad3ffe617984f5b599aeed1bc7f04899aca55) Thanks @Marve10s! - Keep recursive `Stream.concat` chains through `Stream.suspend` linear in their length. + +- [#9274](https://github.com/Effect-TS/effect/pull/9274) [`0909236`](https://github.com/Effect-TS/effect/commit/090923611b209a19f3281a7b1d0c15a9b653a0c7) Thanks @Marve10s! - Fix `Stream.mapAccum` throwing a `RangeError` when its callback returns a large array. + +- [#9623](https://github.com/Effect-TS/effect/pull/9623) [`a9e66d8`](https://github.com/Effect-TS/effect/commit/a9e66d889017b302355ba5b8bc2b2050ca1ea5aa) Thanks @Marve10s! - Keep `Stream.retry` and `Channel.retry` work constant per attempt instead of growing with every retry. + +- [#9624](https://github.com/Effect-TS/effect/pull/9624) [`ee5c0d5`](https://github.com/Effect-TS/effect/commit/ee5c0d5f43348747b04b5f7bf1b39355614c2f2c) Thanks @Marve10s! - Fix `Stream.zip` and related zip operators taking quadratic time when the two streams emit chunks of different sizes. + +- [#9492](https://github.com/Effect-TS/effect/pull/9492) [`631c5d6`](https://github.com/Effect-TS/effect/commit/631c5d6d9a81e6389964b878764fb9aea1538247) Thanks @Marve10s! - Fix `Stream.zipWithArray` discarding right-side leftovers when the callback returns leftovers for both streams. + +- [#9750](https://github.com/Effect-TS/effect/pull/9750) [`7e60c1a`](https://github.com/Effect-TS/effect/commit/7e60c1ae62a96c768f9c67d706f494213947f22f) Thanks @armancharan! - Fix `String.capitalize` and `String.uncapitalize` for strings that start with an astral letter such as `"𐐨"`. + +- [#9494](https://github.com/Effect-TS/effect/pull/9494) [`4cddd3c`](https://github.com/Effect-TS/effect/commit/4cddd3c41e71fc4990d37c42b1bcf2ce3780b41d) Thanks @Marve10s! - Fix suspended `Schema.Struct` parsers overwriting earlier results when the same parsing Effect is rerun. + +- [#9722](https://github.com/Effect-TS/effect/pull/9722) [`8727e5d`](https://github.com/Effect-TS/effect/commit/8727e5d2bec8e2d729b7aeafb725a4ab21d533d2) Thanks @Marve10s! - Fix `Stream.switchMap` and `Channel.switchMap` not finalizing inner streams that were replaced before they started. + +- [#9428](https://github.com/Effect-TS/effect/pull/9428) [`2bc97fa`](https://github.com/Effect-TS/effect/commit/2bc97faa0e06b3973780dae26029e2c59f5dba38) Thanks @jpowersdev! - Fix `Tar.decode` dropping a leading byte order mark from entry names, link targets and PAX keys, so a name such as `"\uFEFF/etc/passwd"` no longer decodes as `/etc/passwd`. + +- [#9392](https://github.com/Effect-TS/effect/pull/9392) [`f9a7f05`](https://github.com/Effect-TS/effect/commit/f9a7f056ba31aef435933acf082ec8c01ac7decc) Thanks @effect-bot! - Keep TestClock diagnostics visible when TestConsole captures test output, including warnings about time not advancing and busy fibers. Ordinary test console output remains captured. + +- [#9434](https://github.com/Effect-TS/effect/pull/9434) [`d978cca`](https://github.com/Effect-TS/effect/commit/d978cca02914bec0ddf66bb2d68d90b220096fb6) Thanks @jpowersdev! - Add the experimental `TextDiff` module for array, line and Unicode code point edit scripts, replay, inversion and unified diff formatting. Use `maxEdits` to bound search work on large inputs, trading minimality for a valid script when the cap is exceeded. + +- [#6428](https://github.com/Effect-TS/effect/pull/6428) [`b334ccf`](https://github.com/Effect-TS/effect/commit/b334ccf361a0f11362e7aabdbf70b35f53a39d61) Thanks @coyaSONG! - Add `FileSystem.lstat`, which returns the `File.Info` of a path without following symbolic links. Custom `FileSystem` implementations, including those passed to `FileSystem.make`, must now provide `lstat`. + +- [#9538](https://github.com/Effect-TS/effect/pull/9538) [`0b50cfc`](https://github.com/Effect-TS/effect/commit/0b50cfc644313afe4c318073893231ec1946eb5f) Thanks @jayhemnani9910! - Allow empty quoted keys such as `"" = 1` in `Toml.parse`, as permitted by TOML 1.0. + +- [#9741](https://github.com/Effect-TS/effect/pull/9741) [`5b7d2b5`](https://github.com/Effect-TS/effect/commit/5b7d2b5c90bc77eb9f8be1b91166c53f89aa1d0c) Thanks @danieljvdm! - Fix `Toolkit.handle` dropping additional failures and their annotations when a tool handler fails in `"error"` failure mode. + +- [#8065](https://github.com/Effect-TS/effect/pull/8065) [`6ee26b1`](https://github.com/Effect-TS/effect/commit/6ee26b16b51b380c41dbfa84d612aa525b420fce) Thanks @danieljvdm! - Interrupt Toolkit handlers when their result stream closes, including early termination. Add `Toolkit.PreliminaryBuffer` so callers can opt in to bounding buffered preliminary results. + +- [#9271](https://github.com/Effect-TS/effect/pull/9271) [`eab7e6a`](https://github.com/Effect-TS/effect/commit/eab7e6acfc7e422857565b98b9eddd3a98de1b92) Thanks @Marve10s! - Fix Trie iterators restarting from the first entry when iterated again. They now continue from their current position. + +- [#9701](https://github.com/Effect-TS/effect/pull/9701) [`e618f10`](https://github.com/Effect-TS/effect/commit/e618f1050f41424439eb82478fcf1c5da5a3244d) Thanks @effect-bot! - Require an explicit trusted proxy policy before applying forwarded headers. + + `HttpMiddleware.xForwardedHeaders` now takes `{ trustedProxies }` and only applies `X-Forwarded-*` headers when the connecting peer is trusted. `X-Forwarded-For` is read from the right, skipping trusted proxies, and `X-Forwarded-Host` / `X-Forwarded-Proto` are only used when they line up with it. + + `HttpServerRequest.toURL` no longer reads `x-forwarded-proto` directly. It uses the new `request.protocol`, set by the trusted proxy middleware, before the transport scheme. The connecting peer stays available as `request.peerAddress`. + + ```ts + import { HttpMiddleware } from "effect/http" + import { IpNetwork } from "effect/net" + + // before + HttpMiddleware.xForwardedHeaders + + // after + HttpMiddleware.xForwardedHeaders({ + trustedProxies: [IpNetwork.fromStringUnsafe("10.0.0.0/8")] + }) + ``` + +- [#9560](https://github.com/Effect-TS/effect/pull/9560) [`da14b1f`](https://github.com/Effect-TS/effect/commit/da14b1f18d71a18fc431b56d82cffe09aeb92708) Thanks @effect-bot! - Add opt-in typed reactivity keys. Augment `Reactivity.Keys` to map key names to ID types, and the public reactivity APIs (`Reactivity`, `Atom`, `AtomHttpApi`, `AtomRpc`, `SqlClient.reactive` and `EventLog.groupReactivity`) reject undeclared key names and mismatched IDs. With no keys declared, the accepted types are unchanged. + +- [#9516](https://github.com/Effect-TS/effect/pull/9516) [`3314fc3`](https://github.com/Effect-TS/effect/commit/3314fc30c22d5c9d737f3df7201f4a41424f701b) Thanks @tim-smart! - Preserve defects in mixed failure causes across Effect, Stream and Channel. + + - Typed recovery skips causes containing a defect and converts every typed error to a defect: `Fail(e) + Die(d)` becomes `Die(e) + Die(d)`, preserving reason order and interrupts. Use `catchCause` or `catchDefect` to recover from these causes. + - An interrupt alongside a typed error does not block recovery, including Effect retry and repeat error paths. Retrying or re-raising only the typed error can discard that interrupt. + - The same rule applies to failing `repeat` / `repeatOrElse` policies. Retry-policy failures still propagate unchanged. + - Error mapping in Effect (including eager variants), Exit, Stream and Channel now maps every typed error in place, preserving defects, interrupts and reason order instead of keeping only the first mapped failure. Mapping does not convert typed errors to defects. + +- [#8351](https://github.com/Effect-TS/effect/pull/8351) [`66db624`](https://github.com/Effect-TS/effect/commit/66db624670032a02144d42b6e64b83b324c813cd) Thanks @ArjunCodess! - Add `UserDirectories` to `effect/persistence`, a service that resolves the home, config, data, cache and state directories for an application following XDG and platform conventions. + +- [#9531](https://github.com/Effect-TS/effect/pull/9531) [`4b8a50d`](https://github.com/Effect-TS/effect/commit/4b8a50db9649603f382f33640698020ca79547d0) Thanks @jayhemnani9910! - Report an initial UTF-16 BOM in `Xml.decode` as `Unsupported` instead of `Syntax`. + +- [#9581](https://github.com/Effect-TS/effect/pull/9581) [`8d52668`](https://github.com/Effect-TS/effect/commit/8d52668533e9d4a6d6b3be9fe58bd5e69fa80b65) Thanks @jayhemnani9910! - Fix `Yaml.parse` block scalars including less indented comments in their value. + +- [#9608](https://github.com/Effect-TS/effect/pull/9608) [`1de1055`](https://github.com/Effect-TS/effect/commit/1de10559e7263747751bfb032fd4b0fd8aa28724) Thanks @jayhemnani9910! - Fix `Yaml.parse` rejecting tabs in block scalar content and discarding the whitespace of whitespace-only block scalar lines. Leading empty lines more indented than the block scalar content are now rejected. + +- [#9652](https://github.com/Effect-TS/effect/pull/9652) [`db130e6`](https://github.com/Effect-TS/effect/commit/db130e64557fafe370819d19f78e3b19a21275d6) Thanks @okarin1511! - Fix `Yaml.parse` rejecting anchors defined inside flow collections, such as `{a: &ref 1, b: *ref}`. + +- [#9700](https://github.com/Effect-TS/effect/pull/9700) [`5fa5fa5`](https://github.com/Effect-TS/effect/commit/5fa5fa5b9bff77352fb9ff2d424bf46bd6309986) Thanks @fubhy! - Fix `Yaml.parse` rejecting flow collections that span multiple lines, such as a JSON-style mapping with its entries and closing bracket on separate lines. + ## 4.0.5 ### Patch Changes diff --git a/repos/effect/packages/effect/CONFIG.md b/packages/effect/CONFIG.md similarity index 100% rename from repos/effect/packages/effect/CONFIG.md rename to packages/effect/CONFIG.md diff --git a/repos/effect/packages/effect/HTTPAPI.md b/packages/effect/HTTPAPI.md similarity index 81% rename from repos/effect/packages/effect/HTTPAPI.md rename to packages/effect/HTTPAPI.md index 2864513db..9205f770f 100644 --- a/repos/effect/packages/effect/HTTPAPI.md +++ b/packages/effect/HTTPAPI.md @@ -1362,11 +1362,334 @@ Layer.launch(ApiLayer).pipe(NodeRuntime.runMain) ## Streaming Requests -To receive large or continuous data from the client, register the endpoint with `.handleRaw`, which opts out of automatic payload decoding and exposes the raw `HttpServerRequest`. The request body is then available as a `Stream` of `Uint8Array` chunks through `request.stream`, so the handler can consume it incrementally instead of buffering it in memory. +To receive large or continuous uploads, declare the payload with a streaming schema. These are the same constructors used for [streaming responses](#streaming-responses), and the slot decides the direction: -The payload schema still describes the endpoint in the generated documentation, but with `.handleRaw` the handler decides how the body is consumed. Note that the declared content type is no longer enforced at runtime: requests with a different `Content-Type` header are not rejected with `415`, so any such validation is up to the handler. +| Declaration | Handler receives | Default content type | +| ----------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------- | +| `HttpApiSchema.StreamUint8Array({ contentType?, maxBytes? })` | `Stream` | `application/octet-stream` | +| `HttpApiSchema.StreamNdjson({ schema, contentType?, maxBytes?, maxElementBytes? })` | `Stream` | `application/x-ndjson` | -**Example** (Handling Streaming Requests) +The handler gets a lazy `Stream`. The server reads the socket only as the handler pulls, with no read-ahead, so not pulling is the backpressure. `HttpApiSchema.StreamSse` is response-only and is rejected in `payload`. + +### Streaming Raw Bytes + +**Example** (Receiving a Binary Upload) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema, Stream } from "effect" +import { HttpRouter } from "effect/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" +import { createServer } from "node:http" + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("upload", "/upload", { + // Stream the body as raw bytes and stop after 100 MiB + // (default content type: application/octet-stream) + payload: HttpApiSchema.StreamUint8Array({ maxBytes: "100 MiB" }), + success: Schema.String + }) + ) +) + +const GroupLayer = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("upload", ({ payload }) => + // payload: Stream + payload.pipe( + // The body is read as the fold pulls, never buffered as a whole + Stream.runFold(() => 0, (total, chunk) => total + chunk.length), + Effect.map((total) => `received ${total} bytes`) + )) +) + +const ApiLayer = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLayer), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLayer).pipe(NodeRuntime.runMain) + +// printf 'abc' | curl -X POST 'http://localhost:3000/upload' --data-binary @- -H "Content-Type: application/octet-stream" +// "received 3 bytes" +``` + +### Streaming NDJSON + +`HttpApiSchema.StreamNdjson({ schema })` declares a body of newline-delimited JSON, one encoded element per line. The handler receives decoded elements, and the element schema's decoding services are added to the handler's requirements. + +**Example** (Importing Records Line by Line) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema, Stream } from "effect" +import { HttpRouter } from "effect/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" +import { createServer } from "node:http" + +const Event = Schema.Struct({ + id: Schema.Number, + name: Schema.String +}) + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("importEvents", "/events", { + // One JSON-encoded Event per line + // (default content type: application/x-ndjson) + payload: HttpApiSchema.StreamNdjson({ schema: Event }), + success: Schema.String + }) + ) +) + +const GroupLayer = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("importEvents", ({ payload }) => { + // payload: Stream + let accepted = 0 + return payload.pipe( + Stream.runForEach((_event) => + Effect.sync(() => { + // store the event... + accepted++ + }) + ), + Effect.map(() => `accepted ${accepted} records`), + // A bad record ends the stream. Every record before it has already + // been handled, so report how far the import got + Effect.catchTag("HttpApiSchemaError", () => + Effect.succeed(`accepted ${accepted} records, record ${accepted + 1} is invalid`)) + // HttpApiPayloadTooLarge is not caught, so it becomes an empty 413 + ) + }) +) + +const ApiLayer = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLayer), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLayer).pipe(NodeRuntime.runMain) + +// printf '{"id":1,"name":"a"}\n{"id":2,"name":"b"}\nnope\n' | curl -X POST 'http://localhost:3000/events' --data-binary @- -H "Content-Type: application/x-ndjson" +// "accepted 2 records, record 3 is invalid" +``` + +The wire format: + +- The server accepts only the declared content type, compared after normalization, and answers `415` otherwise. There are no aliases: for JSON Lines, pass `contentType: "application/jsonl"`. +- `\n`, `\r\n` and a bare `\r` each end a line. Empty lines are skipped, so a client can send `\n` as a keep-alive. A line containing only whitespace is malformed. +- A missing final newline is fine if the last line parses. If it doesn't, the issue message says "unterminated final line", so a truncated body is recognizable. +- A leading UTF-8 BOM is stripped. Invalid UTF-8 is an error, never replaced with U+FFFD. +- The first malformed line, or the first element that fails to decode, fails the stream with an `HttpApiSchemaError` (`kind: "Payload"`), after every element before it has been delivered. A failure ends the stream, so a handler can't skip a bad record and keep reading. To skip bad records, declare a lenient element schema and filter. +- NDJSON can't detect a body that was cut cleanly at a line boundary. It looks like a shorter, complete body. + +### Limits and Errors + +Limits are set on the declaration only: + +- `maxBytes` caps the whole body. It has no default, and it does not fall back to `MaxBodySize`, which only applies to buffered reads. +- `maxElementBytes` (NDJSON only) caps the UTF-8 bytes of one line, without its terminator. It defaults to 1 MiB, and a longer line fails before the rest of it is buffered. +- Both count the bytes the handler sees, after the `HttpMiddleware.decompression` middleware. +- Both count only bytes that are actually read. After `Stream.take(1)`, `maxBytes` never fires. + +If the request's `content-length` exceeds `maxBytes`, the server answers an empty `413` with `Connection: close` as soon as params, headers and query have decoded. The handler doesn't run and no body byte is read. + +The payload stream's errors are added to the handler's error channel automatically. A handler can catch them after a partial read, as in the example above, or let them through. The server maps the ones it lets through before responding: + +| Error | Response | +| ---------------------------- | ------------------------------------ | +| `HttpApiSchemaError` | `400`, the same as buffered payloads | +| `HttpApiPayloadTooLarge` | empty `413` | +| Body read failure (a defect) | `500`, reported | + +`HttpApiPayloadTooLarge` carries `limit` (`"maxBytes"` or `"maxElementBytes"`) and its configured `size`. + +`HttpMiddleware.decompression` also caps the decompressed body at `maxDecompressedSize`, which defaults to `MaxBodySize` and then to 10 MiB. That cap applies to streaming payloads too, and breaching it answers `413`. Raise `maxDecompressedSize` on servers that accept large compressed uploads. + +### Body Lifetime + +- Middleware runs first, then params, headers and query, then the payload. A params, headers or query failure answers `400` without reading a body byte, and the handler always gets the body unread. +- Read the payload stream once. The body is never buffered for a replay, so a repeated read keeps the adapter's native behavior: an empty body, a typed `RequestParseError`, or a replay of the caller's stream under `HttpApiTest`. Don't rely on any of them. +- A handler can respond without reading the whole body, for example to reject an upload early. Over HTTP/1.1 the Node server then drains up to 256 KiB of what is left. Past that it sends `Connection: close` and closes the socket. + +### Mixing Buffered and Streaming Payloads + +One streaming payload can share the `payload` array with buffered payloads that use other content types. The server picks one by the request's content type, and the handler tells them apart with `Stream.isStream`. + +**Example** (Accepting a JSON Array or an NDJSON Stream) + +```ts +import { Effect, Schema, Stream } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" + +const Event = Schema.Struct({ + id: Schema.Number, + name: Schema.String +}) + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("importEvents", "/events", { + payload: [ + // application/json: the whole batch in one body + Schema.Array(Event), + // application/x-ndjson: one event per line, streamed + HttpApiSchema.StreamNdjson({ schema: Event }) + ], + success: Schema.Number + }) + ) +) + +const GroupLayer = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("importEvents", ({ payload }) => + Stream.isStream(payload) + ? Stream.runCount(payload) + : Effect.succeed(payload.length)) +) +``` + +Endpoint construction throws when: + +- two streaming payloads are declared, because the client could not choose an encoding for a `Stream` +- a streaming and a buffered payload share a media type, compared after normalization, so `; charset=utf-8` does not make them distinct +- the method is `GET`, `HEAD`, `OPTIONS` or `TRACE`, which is also a type error. Every other method, including `DELETE` and `QUERY`, accepts a streaming payload + +### Sending Streams with the Derived Client + +The derived client takes a `Stream` as the payload: `Stream` for `StreamUint8Array`, and `Stream` for `StreamNdjson`. It sends the stream as it is pulled, without buffering, with the declared content type and no `content-length`. NDJSON elements are encoded lazily, each as one line of JSON followed by `\n`. + +The method infers only `R` from the stream, plus the services the element schema needs to encode. The stream's error isn't one of the endpoint's declared errors, so it's not in the method's type. A failing source aborts the upload, and the method fails with the `HttpClientError` the adapter reports. If the source fails while a streaming success response is being read, the response stream fails with that `HttpClientError` instead. An element that fails to encode fails the upload the same way. + +**Example** (Uploading a Stream of Records) + +```ts +import { Effect, Schema, Stream } from "effect" +import { FetchHttpClient } from "effect/http" +import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" + +const Event = Schema.Struct({ + id: Schema.Number, + name: Schema.String +}) + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("importEvents", "/events", { + payload: HttpApiSchema.StreamNdjson({ schema: Event }), + success: Schema.String + }) + ) +) + +const program = Effect.gen(function*() { + const client = yield* HttpApiClient.make(Api, { + baseUrl: "http://localhost:3000" + }) + + // Any Stream of Events works + const events = Stream.range(1, 1000).pipe( + Stream.map((id) => ({ id, name: `event ${id}` })) + ) + + const result = yield* client.group.importEvents({ payload: events }) + console.log(result) +}) +// program: Effect + +Effect.runFork(program.pipe(Effect.provide(FetchHttpClient.layer))) +``` + +Things to know: + +- A `400` or `413` from the server is an `HttpClientError` with a `DecodeError` reason, as a buffered `400` is today. There's no client-side `HttpApiPayloadTooLarge`, because the `413` body is empty. +- The client doesn't check `maxBytes` or `maxElementBytes` before sending. A `HttpClient.mapRequest` that sets `content-length` lets the server answer an early `413` on Node clients. `FetchHttpClient` strips `content-length`. +- With a streaming success response, the upload runs until either side ends. Otherwise it's interrupted once the method has decoded the response, whether that's an error status or a buffered success. +- Retries and redirects replay the stream. `HttpClient.retry` and 307/308 redirects run the payload stream again. That's right for a source that can be re-read, like a file, and wrong for a one-shot source like `Stream.fromQueue`: a retry would send only what is left in the queue. Don't retry uploads from such sources, or buffer them first. +- On a mixed payload, a `Stream` value is always sent with the streaming member, even when a buffered member's `Type` would also accept it (`Schema.Unknown`, `Schema.Any`). A `Uint8Array` or an array is never wrapped into a stream. +- `FetchHttpClient` needs a runtime that can send a `ReadableStream` request body. Node, Bun and Deno can. In browsers, Chrome sends streaming uploads only over HTTP/2 or later, and over HTTP/1.1 the request fails with a `TransportError`. + +### Streaming Both Ways + +A streaming payload can be combined with a streaming success response. Neither direction is buffered, and response headers go out as soon as the handler returns its stream. + +**Example** (Transforming an Upload as It Arrives) + +```ts +import { Effect, Schema, Stream } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" + +const Event = Schema.Struct({ + id: Schema.Number, + name: Schema.String +}) + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.post("enrich", "/enrich", { + payload: HttpApiSchema.StreamNdjson({ schema: Event }), + success: HttpApiSchema.StreamNdjson({ schema: Event }) + }), + HttpApiEndpoint.post("enrichBatch", "/enrich-batch", { + payload: HttpApiSchema.StreamNdjson({ schema: Event }), + success: HttpApiSchema.StreamNdjson({ schema: Event }) + }) + ) +) + +const enrich = (event: typeof Event.Type) => ({ ...event, name: event.name.toUpperCase() }) + +const GroupLayer = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers + // Interleaved: each output line is written as its input line arrives. + // Needs a full-duplex client + .handle("enrich", ({ payload }) => Effect.succeed(Stream.map(payload, enrich))) + // Portable: read the whole upload, then respond + .handle("enrichBatch", ({ payload }) => + Stream.runCollect(payload).pipe( + Effect.map((events) => Stream.fromIterable(events.map(enrich))) + )) +) +``` + +Interleaving is best effort, and both ends must be full duplex. Node's `node:http` and undici, and Bun's and Deno's `fetch`, are. Browsers send uploads with `duplex: "half"` and don't read the response until the upload has ended. A handler that pipes its input to its output, like `enrich` above, then deadlocks once the data outgrows the flow-control windows. The portable pattern is the one `enrichBatch` uses: consume the upload, then respond. + +Once the response headers are sent, the payload stream is pulled inside the response stream. A payload failure is then a response failure: the connection is aborted and the client sees a body read error. For a `StreamSse` response, map the payload error into the declared SSE `error` schema to send it as a typed failure. + +On Deno, an HTTP/1.1 request body read can stall while the handler returns a streamed response (denoland/deno#36966), which affects this pattern until the fix ships. + +### Proxying a Streaming Upload + +`HttpServerRequest.toClientRequest` turns the incoming request into an outbound `HttpClientRequest` that streams the same body. The body is not buffered, so if the outbound client retries the proxied request or follows a 307/308 redirect, the retry resends whatever the adapter gives a second read, usually an empty body. A proxy that retries has to buffer the body itself, or turn retries off. + +### Testing with HttpApiTest + +`HttpApiTest` passes a `Stream` payload to the handler unbuffered, pulling each element from the caller's stream as the handler reads it. Limits, the `400` and both `413` paths work in memory. The early `413` needs a `content-length`, which the client doesn't send for streams, so set one through `HttpClient.mapRequest`. When the caller's stream fails, the handler's read of the payload fails as a defect and the test call dies with it, because `HttpApiTest` doesn't turn defects into responses. Inside a streaming response, the response body fails instead. + +Socket behavior and client aborts are not emulated: no draining of unread bodies, no `Connection: close`, no interrupted handler, and no `499`. + +### Reading the Raw Request Body + +To take over body handling completely, register the endpoint with `.handleRaw`. It opts out of automatic payload decoding and exposes the raw `HttpServerRequest`, whose body is available as a `Stream` of `Uint8Array` chunks through `request.stream`. + +The payload schema still describes the endpoint in the generated documentation, but with `.handleRaw` the handler decides how the body is consumed. The declared content type is no longer enforced at runtime: requests with a different `Content-Type` header are not rejected with `415`, so any such validation is up to the handler. + +**Example** (Reading the Raw Request Body) ```ts import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" @@ -1845,6 +2168,7 @@ Layer.launch(ApiLayer).pipe(NodeRuntime.runMain) To stream data to the client over time, declare the success schema with one of the streaming constructors: - `HttpApiSchema.StreamUint8Array()` streams raw bytes (default content type: `application/octet-stream`) +- `HttpApiSchema.StreamNdjson({ schema })` streams typed values as newline-delimited JSON (default content type: `application/x-ndjson`) - `HttpApiSchema.StreamSse(options)` streams typed Server-Sent Events (default content type: `text/event-stream`) With a streaming success schema, the handler returns a `Stream` directly, and the derived client resolves to a typed `Stream` on the consuming side. The stream declaration is also rendered in the OpenAPI documentation. @@ -1963,9 +2287,64 @@ Layer.launch(ApiLayer).pipe(NodeRuntime.runMain) To control the full event shape instead, pass an `events` schema whose Encoded side matches the SSE wire format: a required `data` string plus optional `id` and `event` fields. The handler then emits whole events. +### Streaming NDJSON + +`HttpApiSchema.StreamNdjson({ schema })` declares a response of newline-delimited JSON. Each stream element is encoded with `schema` and written as one line of JSON followed by `\n`, including the last. The wire format is the same as for [NDJSON request payloads](#streaming-ndjson). + +**Example** (Implementing an NDJSON Endpoint) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schedule, Schema, Stream } from "effect" +import { HttpRouter } from "effect/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" +import { createServer } from "node:http" + +const Message = Schema.Struct({ + text: Schema.String +}) + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("group").add( + HttpApiEndpoint.get("getMessages", "/messages", { + // Each stream element is written as one line of JSON + // (default content type: application/x-ndjson) + success: HttpApiSchema.StreamNdjson({ schema: Message }) + }) + ) +) + +const GroupLayer = HttpApiBuilder.group( + Api, + "group", + (handlers) => + handlers.handle("getMessages", () => + Effect.succeed( + Stream.make({ text: "one" }, { text: "two" }, { text: "three" }).pipe( + Stream.schedule(Schedule.spaced("500 millis")) + ) + )) +) + +const ApiLayer = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLayer), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLayer).pipe(NodeRuntime.runMain) + +// curl 'http://localhost:3000/messages' --no-buffer +// {"text":"one"} +// {"text":"two"} +// {"text":"three"} +``` + +`StreamNdjson` has no `error` schema. If the handler's stream fails after the response has started, the connection is aborted, so the client sees a body read error and never a clean end. Typed failures mid-stream need `StreamSse` (see [Typed Stream Failures](#typed-stream-failures)). + ### Consuming Streams with the Derived Client -For both constructors, calling the endpoint on the derived client resolves to a typed `Stream`: +For all three constructors, calling the endpoint on the derived client resolves to a typed `Stream`: ```ts const events = yield * client.group.getEvents() @@ -1974,6 +2353,18 @@ const events = yield * client.group.getEvents() yield * Stream.runForEach(events, (message) => Console.log(message.text)) ``` +The element types and errors are: + +| Success schema | Client stream | +| -------------------------- | ------------------------------------------------------------------------------------------------------ | +| `StreamUint8Array()` | `Stream` | +| `StreamNdjson({ schema })` | `Stream` | +| `StreamSse(options)` | `Stream`, where `E` is the `error` schema | + +An NDJSON response is decoded with the same rules as an NDJSON request payload. A malformed line or an element that fails to decode is a `SchemaError`. The client also enforces the declared `maxElementBytes` (1 MiB by default), so a server that never sends `\n` can't make it buffer forever. A breach is an `HttpClientError` with a `DecodeError` reason. A server failure mid-stream is the `HttpClientError` from reading the body. + +If the endpoint also has a [streaming payload](#streaming-requests), the upload can still be running while the response is read, and a late failure of the payload stream fails the response stream with its error too. + ### Typed Stream Failures `HttpApiSchema.StreamSse` accepts an `error` schema describing typed stream failures. Since the HTTP status is already sent when the stream fails, the failure cannot travel as a normal error response. Instead, the server encodes the full `Cause` into a reserved `effect/http-api/stream/failure` event, and the derived client decodes that event and fails the stream with the original cause. @@ -2003,6 +2394,65 @@ Things to know: - An endpoint may declare at most one streaming success schema, `HEAD` endpoints cannot declare one, and streaming schemas are not supported in error responses. - The `effect/http-api/stream/failure` event name is reserved: `events` schemas may not declare it, and the client treats such an event as a stream failure only when its `data` decodes to a `Cause`. +## Upgrading the Connection + +`HttpApiSchema.Upgrade({ protocol })` declares an endpoint that switches the connection to another protocol with an HTTP/1.1 upgrade, like Docker's `Upgrade: tcp` for container attach. After `101 Switching Protocols` the connection carries raw bytes in both directions as a `Socket`. + +The handler receives the connection as `socket` and returns when it is done with it. The `101` is sent when the handler acquires the socket's reader, so the handler can still fail with a declared error before then. Socket operations fail with `SocketError`, which the handler handles itself, since the response has already switched protocols. + +**Example** (Implementing an Attach Endpoint) + +```ts +import { Effect, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api" + +class NotFound extends Schema.TaggedError()("NotFound", {}, { httpApiStatus: 404 }) {} + +const Api = HttpApi.make("myApi").add( + HttpApiGroup.make("containers").add( + HttpApiEndpoint.post("attach", "/containers/:id/attach", { + params: { id: Schema.String }, + success: HttpApiSchema.Upgrade({ protocol: "tcp" }), + error: NotFound + }) + ) +) + +const GroupLayer = HttpApiBuilder.group( + Api, + "containers", + (handlers) => + handlers.handle("attach", ({ params, socket }) => + Effect.gen(function*() { + if (params.id !== "abc") return yield* new NotFound() + const reader = yield* socket.reader // sends the 101 + const writer = yield* socket.writer + yield* writer.write("attached\n") + const chunks = yield* reader.pull + yield* writer.writeAll(chunks) + }).pipe( + // the connection is already upgraded, so socket failures end the handler + Effect.catchTag("SocketError", () => Effect.void), + Effect.scoped + )) +) +``` + +The derived client sends `Connection: Upgrade` and `Upgrade: tcp`, and resolves to the `101` response headers and the connection. Any other response is decoded as one of the endpoint's errors. + +```ts +const { headers, socket } = yield * client.containers.attach({ params: { id: "abc" } }) +const reader = yield * socket.reader +``` + +Things to know: + +- The client needs a backend that can hand over the connection: `NodeHttpClient.layerNodeHttp` (Node, Bun 1.4+, Deno) or `NodeHttpClient.layerUndici` (Node, Deno). Fetch and `XMLHttpRequest` cannot send upgrade requests, so the call fails with a `TransportError` before anything is sent. +- Serve upgrade endpoints with `NodeHttpServer`, which also works on Bun 1.4+ and Deno. `Bun.serve` and `Deno.serve` only upgrade to WebSocket, so `BunHttpServer` and `DenoHttpServer` fail these upgrades. +- Requests that do not ask for the declared protocol get `426 Upgrade Required`. +- An upgrade endpoint cannot declare a payload or other success schemas, and upgrades are not supported in error responses. For WebSocket, use `HttpServerRequest.upgrade` and `Socket.makeWebSocket` instead. +- Releasing the writer's scope half-closes the connection, and releasing the reader's scope closes it. + # Error Handling ## Adding Custom Error Responses diff --git a/repos/effect/packages/effect/LICENSE b/packages/effect/LICENSE similarity index 100% rename from repos/effect/packages/effect/LICENSE rename to packages/effect/LICENSE diff --git a/repos/effect/packages/effect/MCP.md b/packages/effect/MCP.md similarity index 55% rename from repos/effect/packages/effect/MCP.md rename to packages/effect/MCP.md index 546002295..49559c6f3 100644 --- a/repos/effect/packages/effect/MCP.md +++ b/packages/effect/MCP.md @@ -80,13 +80,16 @@ The part layers are merged into one layer that has a MCP server implementation a `McpServer.layerStdio` is used to create a standard I/O–based MCP server identified by its name and version. Its ordered, non-empty `protocols` declaration names implemented protocol adapters rather than arbitrary version strings. This release supports `McpProtocol.v2024_11_05`, -`McpProtocol.v2025_03_26`, and `McpProtocol.v2025_06_18`. The `v2024_11_05` adapter implements that -revision's RPC schemas and stdio framing, including its batch policy. It does not implement the -historical two-endpoint HTTP+SSE transport. `McpServer.layerHttp` instead offers the 2024 RPC schema -through the same single-endpoint HTTP compatibility transport used by the 2025 adapters. Because of +`McpProtocol.v2025_03_26`, `McpProtocol.v2025_06_18`, `McpProtocol.v2025_11_25`, and +`McpProtocol.v2026_07_28`. Both `layerStdio` and `layerHttp` support all five adapters; +`layerHttpRequestScoped` supports only the stateless `v2026_07_28` adapter. +The `v2024_11_05` adapter implements that revision's RPC schemas and stdio framing, including its +batch policy. It does not implement the historical two-endpoint HTTP+SSE transport. +`McpServer.layerHttp` instead offers the 2024 RPC schema through the same single-endpoint HTTP +compatibility transport used by the newer adapters. Because of the layer architecture the server implementation can be easily exchanged with this HTTP-based implementation. Finally, a logging layer is added with -`Logger.layer([Logger.consolePretty({ stderr: true })])`, ensuring logs are written to `stderr`. +`Layer.succeed(Logger.LogToStderr, true)`, ensuring logs are written to `stderr`. This is essential when using stdio, as any output to `stdout` would interfere with the protocol communication. @@ -370,3 +373,199 @@ const ServerLayer = Layer.mergeAll( // Run the server Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) ``` + +## Connecting to an MCP server + +`McpClient` connects to an MCP server, discovers its tools, prompts and resources, and calls them. +The client and its `McpClientError` module are experimental, so their API may change in minor +releases. It supports the `2025-11-25` and `2026-07-28` protocol revisions over stdio and Streamable HTTP. +The transport selects the revision explicitly; the client does not negotiate one. + +The client covers discovery, tool calls, prompt retrieval, resource reads and Toolkit adaptation. +Resource templates, completion, logging, subscriptions, roots, sampling, elicitation, OAuth and +automatic retries are not implemented. Server requests other than `ping` receive a method-not-found +response, and modern `input_required` results fail with reason `UnsupportedError`. + +### Connecting over HTTP + +`McpClient.layerHttp` provides a connected client from an `HttpClient`. The layer scope owns the +connection: legacy sessions are closed with a `DELETE` when the scope ends. + +```typescript +import { Effect, Layer } from "effect" +import { McpClient, McpProtocol } from "effect/ai" +import { FetchHttpClient } from "effect/http" + +const ClientLayer = McpClient.layerHttp({ + name: "report-workflow", + version: "1.0.0", + protocol: McpProtocol.v2026_07_28, + url: "http://localhost:3000/mcp" +}).pipe(Layer.provide(FetchHttpClient.layer)) + +const program = Effect.gen(function*() { + const client = yield* McpClient.McpClient + const tools = yield* client.listTools() + yield* Effect.log(tools.map((tool) => tool.name)) +}) + +await Effect.runPromise(program.pipe(Effect.provide(ClientLayer))) +``` + +Transport `headers` can carry static credentials. For rotating tokens, wrap the `HttpClient` with +`HttpClient.mapRequestEffect` and read the current token there. + +### Connecting to a subprocess + +`McpClient.layerStdio` spawns the server with the `ChildProcessSpawner` service and exchanges +newline-delimited JSON-RPC over its stdin and stdout. Server logs belong on stderr. + +```typescript +import { NodeServices } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { McpClient, McpProtocol } from "effect/ai" +import { ChildProcess } from "effect/process" + +const ClientLayer = McpClient.layerStdio({ + name: "report-workflow", + version: "1.0.0", + protocol: McpProtocol.v2025_11_25, + command: ChildProcess.make("node", ["./mcp-server.mjs"]) +}).pipe(Layer.provide(NodeServices.layer)) + +const program = Effect.gen(function*() { + const client = yield* McpClient.McpClient + const tools = yield* client.listTools() + yield* Effect.log(tools.map((tool) => tool.name)) +}) + +await Effect.runPromise(program.pipe(Effect.provide(ClientLayer))) +``` + +When the scope closes, the client closes stdin and waits five seconds for the process to exit, then +sends `SIGTERM`, and `SIGKILL` five seconds later. + +To build a client without a layer, provide a `McpClient.Transport` from `McpClient.http` or +`McpClient.stdio` to `McpClient.make` inside an `Effect.scoped` region, and keep every use of the +client inside that scope. + +### Calling tools + +`callTool` takes the descriptor returned by `listTools`, not just a name: on modern HTTP the +descriptor's `x-mcp-header` annotations decide which arguments are mirrored into request headers. +Without a `schema` the full `McpSchema.CallToolResult` is returned, including `isError`, so the +caller decides how to treat tool-reported failures. With a `schema`, `structuredContent` is decoded +and a tool-reported failure fails with reason `ToolError` carrying the original result. + +```typescript +import { Effect, Schema } from "effect" +import { McpClient, McpClientError } from "effect/ai" + +const Report = Schema.Struct({ count: Schema.Number }) + +export const readReport = Effect.fn("readReport")(function*(reportId: string) { + const client = yield* McpClient.McpClient + const tools = yield* client.listTools() + const tool = tools.find((tool) => tool.name === "read_report") + if (tool === undefined) { + return yield* new McpClientError.McpClientError({ + reason: new McpClientError.UnsupportedError({ description: "Server does not expose read_report" }) + }) + } + return yield* client.callTool({ tool, arguments: { reportId }, schema: Report }, { timeout: "2 seconds" }) +}) +``` + +Remote input schemas are returned as JSON Schema metadata only. The client does not validate +arguments against them; define the argument contract in your application or in a `Toolkit`. + +### Handling failures + +Every failure is an `McpClientError.McpClientError` whose `reason` is one of the reason classes +exported by `McpClientError`: `TransportError`, `ProtocolError` (with the JSON-RPC `code` and +`data`), `HttpError` (with `status` and the `www-authenticate` and `retry-after` headers), +`ClosedError` (with `sessionExpired`), `TimeoutError` (with `operation`), `LimitError`, +`UnsupportedError`, `ValidationError`, `ConfigurationError` and `ToolError` (with the tool +`result`). Each reason derives its `message` from these fields and an optional `description`. +`McpClientError.isMcpClientError` and `McpClientError.isMcpClientErrorReason` identify them. + +```typescript +import { Effect, Schema } from "effect" +import type { McpClient, McpSchema } from "effect/ai" + +const Report = Schema.Struct({ count: Schema.Number }) + +export const readWithDeadline = (client: McpClient.McpClient, tool: McpSchema.Tool, reportId: string) => + client.callTool({ tool, arguments: { reportId }, schema: Report }, { timeout: "2 seconds" }).pipe( + Effect.map((report) => ({ status: "ready" as const, report })), + Effect.catchReason("McpClientError", "TimeoutError", () => Effect.succeed({ status: "deferred" as const })) + ) +``` + +`Effect.catchReason` and `Effect.catchReasons` recover from selected reasons; +`Effect.catchTag("McpClientError", ...)` handles them all. The client never retries, so a timeout +says nothing about whether the server completed the call. + +### Limits and deadlines + +Initialization and each complete discovery run under a single deadline, 60 seconds by default and +configurable per client with `timeout` or per call with the `timeout` option. Tool calls, prompts and +resource reads have no deadline unless you pass their `timeout` option or compose `Effect.timeout`. +Discovery follows pagination until the server stops returning a cursor and fails with `LimitError` +after 100 pages, 10,000 items or a repeated cursor. A missing server capability makes the list +operations return an empty array and the retrieval operations fail with `UnsupportedError`. + +Discovery omits tools that require task-augmented execution. Modern HTTP discovery also omits tools +whose `x-mcp-header` annotations are invalid. Messages larger than `maxMessageBytes` (16 MiB by +default) are rejected in both directions. + +The client creates no spans; wrap calls with `Effect.withSpan` or `Effect.fn` as needed. + +### Exposing discovered tools to a language model + +`McpClient.toolkit` discovers every tool and returns a `Toolkit.WithHandler` that `LanguageModel` +accepts. An optional prefix qualifies the model-facing names. Results become text by joining text +blocks, embedded text resources and resource links; when there is no content, `structuredContent` +is serialized as JSON. Binary content and tool-reported errors fail the handler. + +```typescript +import { Effect } from "effect" +import { LanguageModel, McpClient } from "effect/ai" + +export const readRemoteReport = (client: McpClient.McpClient) => + Effect.gen(function*() { + const toolkit = yield* McpClient.toolkit(client, "reports__") + return yield* LanguageModel.generateText({ prompt: "Read the quarterly report", toolkit }) + }) +``` + +The toolkit is a snapshot: build a new one to pick up changes on the server. For custom naming, +validation, approval or result conversion, define tools with `Tool.make` or `Tool.dynamic` and +call `client.callTool` from their handlers. + +### Prompts and resources + +`listPrompts` and `listResources` return complete snapshots. `getPrompt` takes a discovered +`McpSchema.Prompt` with optional string arguments; `readResource` takes any URI. Results reuse the +`McpSchema.GetPromptResult` and `McpSchema.ReadResourceResult` types shared with the server. + +```ts +import { Effect } from "effect" +import type { McpClient } from "effect/ai" + +export const loadContext = (client: McpClient.McpClient) => + Effect.gen(function*() { + const prompts = yield* client.listPrompts() + const prompt = prompts.find((prompt) => prompt.name === "review") + if (prompt !== undefined) { + const result = yield* client.getPrompt({ prompt, arguments: { code: "const answer = 42" } }) + yield* Effect.log(result.messages) + } + + const resources = yield* client.listResources() + for (const resource of resources) { + const result = yield* client.readResource({ uri: resource.uri }) + yield* Effect.log(result.contents) + } + }) +``` diff --git a/repos/effect/packages/effect/OPTIC.md b/packages/effect/OPTIC.md similarity index 100% rename from repos/effect/packages/effect/OPTIC.md rename to packages/effect/OPTIC.md diff --git a/repos/effect/packages/effect/README.md b/packages/effect/README.md similarity index 100% rename from repos/effect/packages/effect/README.md rename to packages/effect/README.md diff --git a/repos/effect/packages/effect/SCHEMA.md b/packages/effect/SCHEMA.md similarity index 98% rename from repos/effect/packages/effect/SCHEMA.md rename to packages/effect/SCHEMA.md index ac4a29956..67cac3f09 100644 --- a/repos/effect/packages/effect/SCHEMA.md +++ b/packages/effect/SCHEMA.md @@ -2174,7 +2174,7 @@ This helper has some advantages over a dedicated constructor: - You can choose among multiple possible tag fields if present. - It supports unions that include nested unions. -Each member must have a unique discriminant property key. `toTaggedUnion` rejects overlapping discriminant types in known member tuples at compile time, including nested unions. It also throws when it encounters a duplicate at runtime. Numeric and string values that resolve to the same property key, such as `1` and `"1"`, are considered duplicates. +Each member must have a unique discriminant property key. `toTaggedUnion` throws when it encounters a duplicate at runtime, including in nested unions. Numeric and string values that resolve to the same property key, such as `1` and `"1"`, are considered duplicates. **Note**. If the tag is the standard `_tag` field, you can use `Schema.TaggedUnion` instead. @@ -2586,7 +2586,9 @@ The default formatter chooses the error label from the level that failed: An `identifier` does not name a failed filter. Use `expected` to name the filter in the default formatter, or `message` to replace the filter failure -message completely. +message completely. A filter's `message` annotation also overrides messages +returned by the filter, whether they are returned directly or attached to a +nested path. Without a `message` annotation, returned messages are preserved. **Example** (Schema identifier versus filter expected message) @@ -4254,7 +4256,7 @@ Effect.runPromiseExit(program).then((exit) => console.log(JSON.stringify(exit, n "_tag": "Failure", "cause": { "_id": "Cause", - "failures": [ + "reasons": [ { "_tag": "Fail", "error": { @@ -5136,6 +5138,34 @@ console.log(serialized) // date1 uses your custom number format, date2 uses the default ISO string format ``` +#### Decoding JSON Written by Go + +Go's `encoding/json` writes a nil pointer, slice, or map as `null`, and API documents rarely say which fields can be `null`. `Schema.toCodecGoJson` is `Schema.toCodecJson` with these rules added at every level of the schema: + +- an optional key that is `null` decodes as a missing key +- a required array that is `null` decodes as `[]` +- a required record that is `null` decodes as `{}` + +Encoding is unchanged from `Schema.toCodecJson`. A key whose schema already accepts `null` (such as `Schema.NullOr(X)`) keeps that behavior. Array and record rules also apply to top-level and nested collections, but not tuples, non-empty arrays, or structs; collection checks still apply. The codec also accepts `encoding/json/v2` output, which uses `[]` and `{}` for nil slices and maps by default. + +This unstable codec deliberately normalizes nil collections to empty ones. Go distinguishes nil from non-nil empty slices and maps, and unmarshalling missing keys, `null`, and empty collections can affect existing values differently. Use `Schema.toCodecJson` with explicit nullable schemas if those distinctions matter. + +```ts +import { Schema } from "effect" + +const Container = Schema.Struct({ + Id: Schema.String, + Names: Schema.Array(Schema.String), + Labels: Schema.Record(Schema.String, Schema.String), + Status: Schema.optionalKey(Schema.String) +}) + +const codec = Schema.toCodecGoJson(Container) + +console.log(Schema.decodeUnknownSync(codec)({ Id: "a", Names: null, Labels: null, Status: null })) +// Output: { Id: 'a', Names: [], Labels: {} } +``` + ### StringTree Canonical Codec The `StringTree` codec converts all values to strings, keeping the structure but not the original types. @@ -6404,11 +6434,38 @@ would strip. Use the imported schema's actual checks and parse options when reas Import translates a Draft 2020-12 subset. `$dynamicRef`, `contains`, `dependentRequired`, `dependentSchemas`, active `if` / `then` / `else`, `unevaluatedItems`, and `unevaluatedProperties` are rejected with an error identifying the unsupported keyword. Inactive conditional keywords and `minContains` / `maxContains` without -`contains` have no validation effect and are ignored. Unknown extension keywords are ignored and their semantics are not -enforced. Objects and arrays used as `const` values or `enum` members are rejected. Only strings, numbers, booleans, and +`contains` have no validation effect and are ignored. Unknown extension keywords are ignored by default and their +semantics are not enforced. Objects and arrays used as `const` values or `enum` members are rejected. Only strings, numbers, booleans, and null are supported. The optional `onEnter` callback can normalize each JSON Schema node before it is translated. +Both import functions accept `includeAnnotationKey` to preserve selected custom keys as Effect schema annotations. +It pairs with the exporter's option of the same name: + +```ts +import { JsonSchema, Schema, SchemaRepresentation } from "effect" + +const document = JsonSchema.fromSchemaDraft2020_12({ + type: "string", + "x-sensitive": true +}) +const options = { includeAnnotationKey: (key: string) => key.startsWith("x-") } +const schema = SchemaRepresentation.fromJsonSchemaDocument(document, options) +const exported = Schema.toJsonSchemaDocument(schema, options) +// exported.schema: { type: "string", "x-sensitive": true } +``` + +Standard annotations such as `title` and `description` are imported independently of the predicate. Selection runs +after `onEnter` and only considers additional own enumerable keys. JSON Schema structural and validation keywords, +Effect's reserved annotation keys, and keys starting with `~` are excluded before calling the predicate. Selected +values must be JSON-compatible. Objects and arrays are retained as opaque metadata, without interpreting nested +schema keywords. Exceptions raised by the predicate pass through unchanged. + +Selected metadata stays on the value's schema when intersections merge checks or simplify to literals. When annotations +collide, later values overwrite earlier ones according to the existing merge order. Metadata belonging only to eliminated +alternatives or unreachable children is discarded. The resulting schema can have a different structure from the input; +retaining annotations does not provide a lossless round trip or enforce custom validation rules. + Intersections of overlapping unions are limited to disjoint root-type partitions and finite primitive `anyOf` literal sets. Other union intersections, including cases that would duplicate a nested choice, are rejected. @@ -6767,9 +6824,9 @@ const schema = Schema.Struct({ const r = SchemaParser.decodeUnknownExit(schema)({ a: "", c: [] }, { errors: "all" }) if (r._tag === "Failure") { - const failures = r.cause.failures - if (failures[0]?._tag === "Fail") { - const failureResult = SchemaIssue.makeFormatterStandardSchemaV1()(failures[0].error) + const reasons = r.cause.reasons + if (reasons[0]?._tag === "Fail") { + const failureResult = SchemaIssue.makeFormatterStandardSchemaV1()(reasons[0].error) const serializer = Schema.toCodecJson(Schema.StandardSchemaV1FailureResult) console.dir(Schema.encodeSync(serializer)(failureResult), { depth: null }) } diff --git a/repos/effect/packages/effect/benchmark/Pool.ts b/packages/effect/benchmark/Pool.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/Pool.ts rename to packages/effect/benchmark/Pool.ts diff --git a/packages/effect/benchmark/encoding/Xml.ts b/packages/effect/benchmark/encoding/Xml.ts new file mode 100644 index 000000000..ddb592463 --- /dev/null +++ b/packages/effect/benchmark/encoding/Xml.ts @@ -0,0 +1,115 @@ +import { Effect, Stream } from "effect" +import { Xml } from "effect/encoding" +import { Bench } from "tinybench" + +const bench = new Bench() + +// A feed-style document: text, references and a few attributes per item +const feed = (items: number): string => { + const parts = ["\n\n\nFeed\n"] + for (let i = 0; i < items; i++) { + parts.push( + `\n Item ${i} & friends\n` + + ` https://example.com/items/${i}?a=1&b=2\n` + + ` urn:uuid:${i}-0000-4000-8000-000000000000\n` + + ` Mon, 01 Jan 2024 00:00:00 GMT\n` + + ` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor ` + + `incididunt ut labore et dolore magna aliqua <b>bold</b> quis nostrud exercitation.\n` + + ` newseffect\n\n` + ) + } + parts.push("\n\n") + return parts.join("") +} + +// An attribute-heavy document with little text +const table = (rows: number): string => { + const parts = [""] + for (let i = 0; i < rows; i++) { + parts.push( + `` + ) + } + parts.push("
") + return parts.join("") +} + +const split = (text: string, size: number): Array => { + const chunks: Array = [] + for (let i = 0; i < text.length; i += size) chunks.push(text.slice(i, i + size)) + return chunks +} + +const splitBytes = (bytes: Uint8Array, size: number): Array => { + const chunks: Array = [] + for (let i = 0; i < bytes.length; i += size) chunks.push(bytes.subarray(i, i + size)) + return chunks +} + +const decodeString = (chunks: Array) => + Effect.runPromise( + Stream.fromIterable(chunks).pipe(Stream.pipeThroughChannel(Xml.decodeString()), Stream.runCount) + ) + +const decode = (chunks: Array) => + Effect.runPromise(Stream.fromIterable(chunks).pipe(Stream.pipeThroughChannel(Xml.decode()), Stream.runCount)) + +const elements = (chunks: Array, path: Array) => + Effect.runPromise( + Stream.fromIterable(chunks).pipe( + Stream.pipeThroughChannel(Xml.decodeString()), + Stream.pipeThroughChannel(Xml.elements(path)), + Stream.runCount + ) + ) + +const events = (text: string) => + Effect.runPromise( + Stream.make(text).pipe(Stream.pipeThroughChannel(Xml.decodeString()), Stream.runCollect) + ) + +const encodeString = (events: ReadonlyArray) => + Effect.runPromise( + Stream.fromIterable(events).pipe(Stream.pipeThroughChannel(Xml.encodeString()), Stream.runCount) + ) + +const encode = (events: ReadonlyArray) => + Effect.runPromise(Stream.fromIterable(events).pipe(Stream.pipeThroughChannel(Xml.encode()), Stream.runCount)) + +const encoder = new TextEncoder() +const documents = [ + { name: "feed", text: feed(8000), path: ["rss", "channel", "item"] }, + { name: "table", text: table(40000), path: ["table", "row"] } +] + +for (const { name, path, text } of documents) { + const bytes = encoder.encode(text) + const decoded = await events(text) + bench + .add(`${name} decodeString (1 chunk, ${(text.length / 1024 / 1024).toFixed(1)} MiB)`, async function() { + await decodeString([text]) + }) + .add(`${name} decodeString (64 KiB chunks)`, async function() { + await decodeString(split(text, 64 * 1024)) + }) + .add(`${name} decodeString (1 KiB chunks)`, async function() { + await decodeString(split(text, 1024)) + }) + .add(`${name} decode (64 KiB chunks)`, async function() { + await decode(splitBytes(bytes, 64 * 1024)) + }) + .add(`${name} elements (64 KiB chunks)`, async function() { + await elements(split(text, 64 * 1024), path) + }) + .add(`${name} encodeString`, async function() { + await encodeString(decoded) + }) + .add(`${name} encode`, async function() { + await encode(decoded) + }) +} + +await bench.run() + +console.table(bench.table()) diff --git a/packages/effect/benchmark/eventlog/Bandwidth.md b/packages/effect/benchmark/eventlog/Bandwidth.md new file mode 100644 index 000000000..d312b539f --- /dev/null +++ b/packages/effect/benchmark/eventlog/Bandwidth.md @@ -0,0 +1,18 @@ +# EventLog bandwidth + +Run from the repository root, optionally passing the number of entries to write (default 1000): + +```sh +nix develop -c pnpm --dir packages/effect exec node benchmark/eventlog/Bandwidth.ts 1000 +``` + +Two devices with the same identity connect to one in-memory server through the socket RPC protocols with `RpcSerialization.layerSchemaBinary`. The sockets are in-memory, but every frame is serialized as it would be on a real connection, and the byte counts are the serialized frames. One device writes the entries one `EventLog.write` at a time; the other receives them. + +Columns: + +- `writer up` / `writer down`: bytes the writing device sent and received while writing, after the handshake. +- `reader down`: bytes the other device received. +- `resume sequence`: the writer journal's `nextRemoteSequence` afterwards. +- `resume down`: bytes a fresh connection receives when it resumes changes from that sequence. + +The file only uses APIs that predate write acknowledgements, so it can be copied into an older checkout to compare. diff --git a/packages/effect/benchmark/eventlog/Bandwidth.ts b/packages/effect/benchmark/eventlog/Bandwidth.ts new file mode 100644 index 000000000..5f9179101 --- /dev/null +++ b/packages/effect/benchmark/eventlog/Bandwidth.ts @@ -0,0 +1,223 @@ +import { Effect, Layer, Queue, Schema } from "effect" +import { + EventGroup, + EventJournal, + EventLog, + EventLogEncryption, + EventLogMessage, + EventLogRemote, + EventLogServerEncrypted, + EventLogServerUnencrypted +} from "effect/eventlog" +import { NetAddress } from "effect/net" +import { RpcClient, RpcSerialization, RpcServer } from "effect/rpc" +import { Socket, SocketServer } from "effect/socket" + +// Counts the bytes an event log client sends and receives while it writes, +// using the socket RPC protocols over an in-memory socket so that every frame +// is serialized exactly as it would be on a real connection. + +const entries = Number(process.argv[2] ?? 1000) +const storeId = EventLogMessage.StoreId.make("default") + +const UserGroup = EventGroup.empty.add({ + tag: "UserCreated", + primaryKey: (payload) => payload.id, + payload: Schema.Struct({ id: Schema.String, name: Schema.String }) +}) +const schema = EventLog.schema(UserGroup) + +const servers = [ + { + name: "encrypted", + handlers: EventLogServerEncrypted.layerRpcHandlers.pipe( + Layer.provide(EventLogServerEncrypted.layerStorageMemory) + ), + remote: EventLogRemote.layerEncrypted + }, + { + name: "unencrypted", + handlers: EventLogServerUnencrypted.layerRpcHandlers.pipe( + Layer.provide([ + EventLogServerUnencrypted.layerStorageMemory, + EventLog.layerRegistry, + EventLogServerUnencrypted.layerStoreMappingStatic({ storeId }), + Layer.succeed(EventLogServerUnencrypted.EventLogServerAuthorization, { + authorizeWrite: () => Effect.void, + authorizeRead: () => Effect.void, + authorizeIdentity: () => Effect.void + }) + ]) + ), + remote: EventLogRemote.layerUnencrypted + } +] as const + +// Event log messages carry binary fields, which JSON serialization rejects. +const serialization = RpcSerialization.layerSchemaBinary() + +const encoder = new TextEncoder() + +// A connected pair of in-memory sockets. `down` counts the bytes the server +// writes, `up` the bytes the client writes. +const makeSocketPair = Effect.gen(function*() { + const toServer = yield* Queue.unbounded() + const toClient = yield* Queue.unbounded() + const bytes = { up: 0, down: 0 } + const side = ( + incoming: Queue.Queue, + outgoing: Queue.Queue, + count: (bytes: number) => void + ) => { + const write = (chunk: Uint8Array | string | Socket.CloseEvent) => { + if (Socket.isCloseEvent(chunk)) return Effect.void + count(typeof chunk === "string" ? encoder.encode(chunk).byteLength : chunk.byteLength) + return Queue.offer(outgoing, chunk).pipe(Effect.asVoid) + } + return Socket.make({ + reader: Effect.succeed({ + pull: Queue.takeAll(incoming), + upgrade: () => Effect.void + }), + writer: Effect.succeed({ + write, + writeAll: (chunks) => Effect.forEach(chunks, write, { discard: true }) + }) + }) + } + return { + bytes, + client: side(toClient, toServer, (n) => bytes.up += n), + server: side(toServer, toClient, (n) => bytes.down += n) + } +}) + +const makeServer = Effect.fnUntraced(function*(server: (typeof servers)[number]) { + const sockets = yield* Queue.unbounded() + const socketServer = SocketServer.SocketServer.of({ + address: NetAddress.inetAddressUnsafe(NetAddress.ipv4Loopback, 0), + run: (handler) => + Queue.take(sockets).pipe( + Effect.flatMap((socket) => Effect.forkChild(handler(socket))), + Effect.forever + ) + }) + yield* Layer.build( + RpcServer.layer(EventLogMessage.EventLogRemoteRpcs).pipe( + Layer.provide(server.handlers), + Layer.provide(Layer.effect(RpcServer.Protocol, RpcServer.makeProtocolSocketServer)), + Layer.provide(Layer.succeed(SocketServer.SocketServer, socketServer)), + Layer.provide(serialization) + ) + ) + + // Connects a remote registered with the given registry layer. + const connectWith = Effect.fnUntraced(function*
( + identity: EventLog.Identity["Service"], + registry: Layer.Layer + ) { + const pair = yield* makeSocketPair + yield* Queue.offer(sockets, pair.server) + const context = yield* Layer.build( + server.remote.pipe( + Layer.provide(RpcClient.layerProtocolSocket()), + Layer.provide(Layer.succeed(Socket.Socket, pair.client)), + Layer.provide(serialization), + Layer.provideMerge(registry) + ) + ) + const remote = yield* EventLogRemote.EventLogRemote.pipe(Effect.provide(context)) + yield* remote.whenAuthenticated(Effect.void).pipe(Effect.provideService(EventLog.Identity, identity)) + return { bytes: pair.bytes, context, remote } + }) + + // Connects a device whose event log replicates through the remote. + const connectDevice = (identity: EventLog.Identity["Service"], journal: EventJournal.EventJournal["Service"]) => + connectWith( + identity, + EventLog.layer( + schema, + EventLog.group(UserGroup, (handlers) => handlers.handle("UserCreated", () => Effect.void)).pipe( + Layer.provide(EventLog.layerRegistry) + ) + ).pipe( + Layer.provide(Layer.succeed(EventJournal.EventJournal, journal)), + Layer.provide(Layer.succeed(EventLog.Identity, identity)) + ) + ) + + // Connects only a remote, without an event log consuming its changes. + const connectRemote = (identity: EventLog.Identity["Service"]) => connectWith(identity, EventLog.layerRegistry) + + return { connectDevice, connectRemote } +}) + +const waitUntil = Effect.fnUntraced(function*(check: Effect.Effect) { + for (let i = 0; i < 6000; i++) { + if (yield* check) return + yield* Effect.sleep("5 millis") + } + return yield* Effect.die("timed out") +}) + +// Waits until a byte counter has not moved for a while. +const settle = Effect.fnUntraced(function*(read: () => number) { + let last = read() + let still = 0 + while (still < 4) { + yield* Effect.sleep("50 millis") + const current = read() + still = current === last ? still + 1 : 0 + last = current + } +}) + +const run = Effect.fnUntraced(function*(server: (typeof servers)[number]) { + const { connectDevice, connectRemote } = yield* makeServer(server) + const identity = yield* EventLog.makeIdentity + const writerJournal = yield* EventJournal.makeMemory + const readerJournal = yield* EventJournal.makeMemory + const writer = yield* connectDevice(identity, writerJournal) + // Another device with the same identity, which receives every write. + const reader = yield* connectDevice(identity, readerJournal) + yield* settle(() => writer.bytes.down + reader.bytes.down) + const writerStart = { ...writer.bytes } + const readerStart = { ...reader.bytes } + + const log = yield* EventLog.EventLog.pipe(Effect.provide(writer.context)) + for (let i = 0; i < entries; i++) { + yield* log.write({ schema, event: "UserCreated", payload: { id: `user-${i}`, name: `User ${i}` } }) + } + yield* waitUntil(Effect.map(readerJournal.entries, (stored) => stored.length === entries)) + yield* settle(() => writer.bytes.down + writer.bytes.up) + + // A fresh connection resumes from the writer's journal. + const startSequence = yield* writerJournal.nextRemoteSequence(writer.remote.id) + const resumed = yield* connectRemote(identity) + const resumedStart = resumed.bytes.down + yield* resumed.remote.changes({ identity, storeId, startSequence }) + yield* settle(() => resumed.bytes.down) + + return { + server: server.name, + entries, + "writer up": writer.bytes.up - writerStart.up, + "writer down": writer.bytes.down - writerStart.down, + "reader down": reader.bytes.down - readerStart.down, + "resume sequence": startSequence, + "resume down": resumed.bytes.down - resumedStart + } +}, Effect.scoped) + +const program = Effect.gen(function*() { + const rows = [] + for (const server of servers) { + rows.push(yield* run(server)) + } + console.table(rows) +}).pipe(Effect.provide(EventLogEncryption.layerSubtle)) + +Effect.runPromise(program).catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/repos/effect/packages/effect/benchmark/http/multipart.ts b/packages/effect/benchmark/http/multipart.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/http/multipart.ts rename to packages/effect/benchmark/http/multipart.ts diff --git a/repos/effect/packages/effect/benchmark/http/serverAllocations.ts b/packages/effect/benchmark/http/serverAllocations.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/http/serverAllocations.ts rename to packages/effect/benchmark/http/serverAllocations.ts diff --git a/repos/effect/packages/effect/benchmark/http/tracer.ts b/packages/effect/benchmark/http/tracer.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/http/tracer.ts rename to packages/effect/benchmark/http/tracer.ts diff --git a/repos/effect/packages/effect/benchmark/httpapi/sseClientDecoder.ts b/packages/effect/benchmark/httpapi/sseClientDecoder.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/httpapi/sseClientDecoder.ts rename to packages/effect/benchmark/httpapi/sseClientDecoder.ts diff --git a/repos/effect/packages/effect/benchmark/net/NetAddress.ts b/packages/effect/benchmark/net/NetAddress.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/net/NetAddress.ts rename to packages/effect/benchmark/net/NetAddress.ts diff --git a/repos/effect/packages/effect/benchmark/rpc/RpcSerialization.ts b/packages/effect/benchmark/rpc/RpcSerialization.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/rpc/RpcSerialization.ts rename to packages/effect/benchmark/rpc/RpcSerialization.ts diff --git a/repos/effect/packages/effect/benchmark/schema/Optic.ts b/packages/effect/benchmark/schema/Optic.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/schema/Optic.ts rename to packages/effect/benchmark/schema/Optic.ts diff --git a/repos/effect/packages/effect/benchmark/schema/SchemaBinary.md b/packages/effect/benchmark/schema/SchemaBinary.md similarity index 100% rename from repos/effect/packages/effect/benchmark/schema/SchemaBinary.md rename to packages/effect/benchmark/schema/SchemaBinary.md diff --git a/repos/effect/packages/effect/benchmark/schema/SchemaBinary.ts b/packages/effect/benchmark/schema/SchemaBinary.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/schema/SchemaBinary.ts rename to packages/effect/benchmark/schema/SchemaBinary.ts diff --git a/repos/effect/packages/effect/benchmark/schema/SchemaError.ts b/packages/effect/benchmark/schema/SchemaError.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/schema/SchemaError.ts rename to packages/effect/benchmark/schema/SchemaError.ts diff --git a/repos/effect/packages/effect/benchmark/stream/splitLines.ts b/packages/effect/benchmark/stream/splitLines.ts similarity index 100% rename from repos/effect/packages/effect/benchmark/stream/splitLines.ts rename to packages/effect/benchmark/stream/splitLines.ts diff --git a/repos/effect/packages/effect/package.json b/packages/effect/package.json similarity index 83% rename from repos/effect/packages/effect/package.json rename to packages/effect/package.json index da512a378..9a155ee4a 100644 --- a/repos/effect/packages/effect/package.json +++ b/packages/effect/package.json @@ -1,7 +1,7 @@ { "name": "effect", "type": "module", - "version": "4.0.5", + "version": "4.0.6", "license": "MIT", "description": "The missing standard library for TypeScript, for writing production-grade software.", "homepage": "https://effect.website", @@ -54,8 +54,17 @@ "./workflow": "./src/workflow/index.ts", "./workers": "./src/workers/index.ts", "./*": "./src/*.ts", + "./ai/internal/*": null, "./cli/internal/*": null, "./cluster/internal/*": null, + "./eventlog/internal/*": null, + "./http/internal/*": null, + "./http/FindMyWay/internal/*": null, + "./http/MultipartParser/internal/*": null, + "./http-api/internal/*": null, + "./observability/internal/*": null, + "./rpc/internal/*": null, + "./workflow/internal/*": null, "./internal/*": null, "./index": null, "./*/index": null @@ -101,6 +110,15 @@ "./internal/*": null, "./cli/internal/*": null, "./cluster/internal/*": null, + "./ai/internal/*": null, + "./eventlog/internal/*": null, + "./http/internal/*": null, + "./http/FindMyWay/internal/*": null, + "./http/MultipartParser/internal/*": null, + "./http-api/internal/*": null, + "./observability/internal/*": null, + "./rpc/internal/*": null, + "./workflow/internal/*": null, "./index": null, "./*/index": null } @@ -117,10 +135,11 @@ "ajv-draft-04": "^1.0.0", "ast-types": "^0.16.3", "esbuild": "^0.28.2", + "fake-indexeddb": "^6.2.5", "immer": "^11.1.21", "miniflare": "5.20260925.0-alpha", - "protobufjs": "^8.8.0", - "tinybench": "^6.2.1", + "protobufjs": "^8.8.1", + "tinybench": "^6.2.2", "valibot": "^1.5.0" } } diff --git a/repos/effect/packages/effect/runtimeperf/README.md b/packages/effect/runtimeperf/README.md similarity index 100% rename from repos/effect/packages/effect/runtimeperf/README.md rename to packages/effect/runtimeperf/README.md diff --git a/repos/effect/packages/effect/runtimeperf/compare.mts b/packages/effect/runtimeperf/compare.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/compare.mts rename to packages/effect/runtimeperf/compare.mts diff --git a/repos/effect/packages/effect/runtimeperf/config.json b/packages/effect/runtimeperf/config.json similarity index 100% rename from repos/effect/packages/effect/runtimeperf/config.json rename to packages/effect/runtimeperf/config.json diff --git a/repos/effect/packages/effect/runtimeperf/materialize.mts b/packages/effect/runtimeperf/materialize.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/materialize.mts rename to packages/effect/runtimeperf/materialize.mts diff --git a/repos/effect/packages/effect/runtimeperf/run.mts b/packages/effect/runtimeperf/run.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/run.mts rename to packages/effect/runtimeperf/run.mts diff --git a/repos/effect/packages/effect/runtimeperf/stats.mts b/packages/effect/runtimeperf/stats.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/stats.mts rename to packages/effect/runtimeperf/stats.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts b/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts rename to packages/effect/runtimeperf/suites/arbitrary/fixtures/fast-check-v4.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts b/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts rename to packages/effect/runtimeperf/suites/arbitrary/fixtures/native.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts b/packages/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts rename to packages/effect/runtimeperf/suites/arbitrary/fixtures/schema.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/README.md b/packages/effect/runtimeperf/suites/compiler-rebuild/README.md similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/README.md rename to packages/effect/runtimeperf/suites/compiler-rebuild/README.md diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts b/packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts rename to packages/effect/runtimeperf/suites/compiler-rebuild/costs.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/aot.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/cases.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/generate.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/interpreted.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/jit.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/valibot.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-cases.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-compiled.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts b/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts rename to packages/effect/runtimeperf/suites/compiler-rebuild/fixtures/zod-jitless.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts b/packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts rename to packages/effect/runtimeperf/suites/compiler-rebuild/report-resources.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/resources.mts b/packages/effect/runtimeperf/suites/compiler-rebuild/resources.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/resources.mts rename to packages/effect/runtimeperf/suites/compiler-rebuild/resources.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts b/packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts rename to packages/effect/runtimeperf/suites/compiler-rebuild/run-resources.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/aot.ts b/packages/effect/runtimeperf/suites/moltar/fixtures/aot.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/aot.ts rename to packages/effect/runtimeperf/suites/moltar/fixtures/aot.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/cases.ts b/packages/effect/runtimeperf/suites/moltar/fixtures/cases.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/cases.ts rename to packages/effect/runtimeperf/suites/moltar/fixtures/cases.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/data.ts b/packages/effect/runtimeperf/suites/moltar/fixtures/data.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/data.ts rename to packages/effect/runtimeperf/suites/moltar/fixtures/data.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/generate.mts b/packages/effect/runtimeperf/suites/moltar/fixtures/generate.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/generate.mts rename to packages/effect/runtimeperf/suites/moltar/fixtures/generate.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts b/packages/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts rename to packages/effect/runtimeperf/suites/moltar/fixtures/interpreted.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/jit.ts b/packages/effect/runtimeperf/suites/moltar/fixtures/jit.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/jit.ts rename to packages/effect/runtimeperf/suites/moltar/fixtures/jit.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/valibot.ts b/packages/effect/runtimeperf/suites/moltar/fixtures/valibot.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/valibot.ts rename to packages/effect/runtimeperf/suites/moltar/fixtures/valibot.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/zod.ts b/packages/effect/runtimeperf/suites/moltar/fixtures/zod.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/moltar/fixtures/zod.ts rename to packages/effect/runtimeperf/suites/moltar/fixtures/zod.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts b/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts rename to packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts b/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts rename to packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts b/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts rename to packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts b/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts rename to packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-checked-sources/README.md b/packages/effect/runtimeperf/suites/schema-checked-sources/README.md similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-checked-sources/README.md rename to packages/effect/runtimeperf/suites/schema-checked-sources/README.md diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-checked-sources/fixtures/cases.ts b/packages/effect/runtimeperf/suites/schema-checked-sources/fixtures/cases.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-checked-sources/fixtures/cases.ts rename to packages/effect/runtimeperf/suites/schema-checked-sources/fixtures/cases.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/AST-LAYOUT.md b/packages/effect/runtimeperf/suites/schema-construction/AST-LAYOUT.md similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/AST-LAYOUT.md rename to packages/effect/runtimeperf/suites/schema-construction/AST-LAYOUT.md diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/README.md b/packages/effect/runtimeperf/suites/schema-construction/README.md similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/README.md rename to packages/effect/runtimeperf/suites/schema-construction/README.md diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/RESULTS.md b/packages/effect/runtimeperf/suites/schema-construction/RESULTS.md similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/RESULTS.md rename to packages/effect/runtimeperf/suites/schema-construction/RESULTS.md diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/TO-TYPE.md b/packages/effect/runtimeperf/suites/schema-construction/TO-TYPE.md similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/TO-TYPE.md rename to packages/effect/runtimeperf/suites/schema-construction/TO-TYPE.md diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/fixtures.mts b/packages/effect/runtimeperf/suites/schema-construction/fixtures.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/fixtures.mts rename to packages/effect/runtimeperf/suites/schema-construction/fixtures.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/profile.mts b/packages/effect/runtimeperf/suites/schema-construction/profile.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/profile.mts rename to packages/effect/runtimeperf/suites/schema-construction/profile.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/run.mts b/packages/effect/runtimeperf/suites/schema-construction/run.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/run.mts rename to packages/effect/runtimeperf/suites/schema-construction/run.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-construction/worker.mts b/packages/effect/runtimeperf/suites/schema-construction/worker.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-construction/worker.mts rename to packages/effect/runtimeperf/suites/schema-construction/worker.mts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema-excess-properties/fixtures/cases.ts b/packages/effect/runtimeperf/suites/schema-excess-properties/fixtures/cases.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema-excess-properties/fixtures/cases.ts rename to packages/effect/runtimeperf/suites/schema-excess-properties/fixtures/cases.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts b/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts rename to packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts b/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts rename to packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts b/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts rename to packages/effect/runtimeperf/suites/schema/fixtures/cold.ts diff --git a/repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/comparison.ts b/packages/effect/runtimeperf/suites/schema/fixtures/comparison.ts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/suites/schema/fixtures/comparison.ts rename to packages/effect/runtimeperf/suites/schema/fixtures/comparison.ts diff --git a/repos/effect/packages/effect/runtimeperf/test/materialize.test.mts b/packages/effect/runtimeperf/test/materialize.test.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/test/materialize.test.mts rename to packages/effect/runtimeperf/test/materialize.test.mts diff --git a/repos/effect/packages/effect/runtimeperf/test/registry.test.mts b/packages/effect/runtimeperf/test/registry.test.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/test/registry.test.mts rename to packages/effect/runtimeperf/test/registry.test.mts diff --git a/repos/effect/packages/effect/runtimeperf/test/schema-construction.test.mts b/packages/effect/runtimeperf/test/schema-construction.test.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/test/schema-construction.test.mts rename to packages/effect/runtimeperf/test/schema-construction.test.mts diff --git a/repos/effect/packages/effect/runtimeperf/test/stats.test.mts b/packages/effect/runtimeperf/test/stats.test.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/test/stats.test.mts rename to packages/effect/runtimeperf/test/stats.test.mts diff --git a/repos/effect/packages/effect/runtimeperf/test/worker.test.mts b/packages/effect/runtimeperf/test/worker.test.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/test/worker.test.mts rename to packages/effect/runtimeperf/test/worker.test.mts diff --git a/repos/effect/packages/effect/runtimeperf/test/worktree-diff.test.mts b/packages/effect/runtimeperf/test/worktree-diff.test.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/test/worktree-diff.test.mts rename to packages/effect/runtimeperf/test/worktree-diff.test.mts diff --git a/repos/effect/packages/effect/runtimeperf/utils.mts b/packages/effect/runtimeperf/utils.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/utils.mts rename to packages/effect/runtimeperf/utils.mts diff --git a/repos/effect/packages/effect/runtimeperf/worker.mts b/packages/effect/runtimeperf/worker.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/worker.mts rename to packages/effect/runtimeperf/worker.mts diff --git a/repos/effect/packages/effect/runtimeperf/worktree-diff.mts b/packages/effect/runtimeperf/worktree-diff.mts similarity index 100% rename from repos/effect/packages/effect/runtimeperf/worktree-diff.mts rename to packages/effect/runtimeperf/worktree-diff.mts diff --git a/repos/effect/packages/effect/src/Arbitrary.ts b/packages/effect/src/Arbitrary.ts similarity index 100% rename from repos/effect/packages/effect/src/Arbitrary.ts rename to packages/effect/src/Arbitrary.ts diff --git a/repos/effect/packages/effect/src/Array.ts b/packages/effect/src/Array.ts similarity index 98% rename from repos/effect/packages/effect/src/Array.ts rename to packages/effect/src/Array.ts index 31f0b052d..c36065659 100644 --- a/repos/effect/packages/effect/src/Array.ts +++ b/packages/effect/src/Array.ts @@ -2620,7 +2620,7 @@ export const rotate: { * **When to use** * * Use when checking membership with caller-provided equality instead of - * `Equal.equivalence()`. + * `Equal.asEquivalence()`. * * **Example** (Checking with custom equality) * @@ -2632,7 +2632,7 @@ export const rotate: { * pipe([1, 2, 3, 4], containsNumber(3)) // => true * ``` * - * @see {@link contains} for the `Equal.equivalence()` variant + * @see {@link contains} for the `Equal.asEquivalence()` variant * * @stability stable * @category predicates @@ -2652,7 +2652,7 @@ export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { }) /** - * Checks whether an array contains a value, using `Equal.equivalence()` for + * Checks whether an array contains a value, using `Equal.asEquivalence()` for * comparison. * * **When to use** @@ -3086,7 +3086,7 @@ export const window: { * ) // => [["a", "a"], ["b", "b", "b"], ["c"], ["a"]] * ``` * - * @see {@link group} for grouping adjacent elements with `Equal.equivalence()` + * @see {@link group} for grouping adjacent elements with `Equal.asEquivalence()` * @see {@link groupBy} for grouping all elements into a record by key, regardless of adjacency * * @stability stable @@ -3116,7 +3116,7 @@ export const groupWith: { ) /** - * Groups consecutive equal elements using `Equal.equivalence()`. + * Groups consecutive equal elements using `Equal.asEquivalence()`. * * **When to use** * @@ -3252,13 +3252,22 @@ const hashBucketsHas = (buckets: HashBuckets, value: unknown): boolean => { } /** - * Computes the union of two arrays using a custom equivalence, removing - * duplicates. + * Computes the union of two iterables using a custom equivalence. * * **When to use** * * Use when you need the union of two arrays but duplicate detection must use a - * custom equivalence instead of the default `Equal.equivalence()`. + * custom equivalence instead of the default `Equal.asEquivalence()`. + * + * **Details** + * + * Keeps the elements of `self`, then appends elements of `that` that have no + * equivalent in `self`. Calls `isEquivalent` with an element from `self` first + * and an element from `that` second. + * + * **Gotchas** + * + * Each input must already be deduplicated. * * **Example** (Computing unions with custom equality) * @@ -3268,7 +3277,7 @@ const hashBucketsHas = (buckets: HashBuckets, value: unknown): boolean => { * Array.unionWith([1, 2], [2, 3], (a, b) => a === b) // => [1, 2, 3] * ``` * - * @see {@link union} for the `Equal.equivalence()` variant + * @see {@link union} for the `Equal.asEquivalence()` variant * @see {@link intersectionWith} for keeping elements present in both arrays * @see {@link differenceWith} for keeping elements present only in the first array * @@ -3292,13 +3301,21 @@ export const unionWith: { isEquivalent: (self: A, that: B) => boolean ): NonEmptyArray (self: Iterable, that: Iterable, isEquivalent: (self: A, that: B) => boolean): Array -} = dual(3, (self: Iterable, that: Iterable, isEquivalent: (self: A, that: A) => boolean): Array => { +} = dual(3, (self: Iterable, that: Iterable, isEquivalent: (self: A, that: B) => boolean): Array => { const a = fromIterable(self) const b = fromIterable(that) if (isReadonlyArrayNonEmpty(a)) { if (isReadonlyArrayNonEmpty(b)) { - const dedupe = dedupeWith(isEquivalent) - return dedupe(appendAll(a, b)) + const out: Array = [...a] + next: for (const value of b) { + for (const candidate of a) { + if (isEquivalent(candidate, value)) { + continue next + } + } + out.push(value) + } + return out } return a } @@ -3306,8 +3323,12 @@ export const unionWith: { }) /** - * Computes the union of two arrays, removing duplicates using - * `Equal.equivalence()`. + * Computes the union of two iterables using `Equal.asEquivalence()`. + * + * **Gotchas** + * + * Each input must already be deduplicated. Use {@link dedupe} to remove + * duplicates from an input first. * * **Example** (Computing array unions) * @@ -3367,7 +3388,7 @@ export const union: { * Array.intersectionWith(isEquivalent)(array2)(array1) // => [{ id: 1 }, { id: 3 }] * ``` * - * @see {@link intersection} for the `Equal.equivalence()` variant + * @see {@link intersection} for the `Equal.asEquivalence()` variant * @see {@link unionWith} for keeping values from either array with custom equality * @see {@link differenceWith} for keeping values only from the first array with custom equality * @@ -3390,7 +3411,7 @@ export const intersectionWith = (isEquivalent: (self: A, that: A) => boolean) } /** - * Computes the intersection of two arrays using `Equal.equivalence()`. Order is + * Computes the intersection of two arrays using `Equal.asEquivalence()`. Order is * determined by the first array. * * **When to use** @@ -3444,7 +3465,7 @@ export const intersection: { * Array.differenceWith((a, b) => a === b)([1, 2, 3], [2, 3, 4]) // => [1] * ``` * - * @see {@link difference} for the `Equal.equivalence()` variant + * @see {@link difference} for the `Equal.asEquivalence()` variant * @see {@link unionWith} for keeping values from either array with custom equality * @see {@link intersectionWith} for keeping values present in both arrays with custom equality * @@ -3468,12 +3489,12 @@ export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): /** * Computes elements in the first array that are not in the second, using - * `Equal.equivalence()`. + * `Equal.asEquivalence()`. * * **When to use** * * Use when you need to keep values from the first array that are absent from - * the second and the default `Equal.equivalence()` comparison is appropriate. + * the second and the default `Equal.asEquivalence()` comparison is appropriate. * * **Example** (Computing array differences) * @@ -4642,7 +4663,7 @@ export const dedupeWith: { ) /** - * Removes duplicates using `Equal.equivalence()`, preserving the order of the + * Removes duplicates using `Equal.asEquivalence()`, preserving the order of the * first occurrence. * * **When to use** @@ -4726,7 +4747,7 @@ export const dedupeAdjacentWith: { }) /** - * Removes consecutive duplicate elements using `Equal.equivalence()`. + * Removes consecutive duplicate elements using `Equal.asEquivalence()`. * * **When to use** * diff --git a/repos/effect/packages/effect/src/BigDecimal.ts b/packages/effect/src/BigDecimal.ts similarity index 100% rename from repos/effect/packages/effect/src/BigDecimal.ts rename to packages/effect/src/BigDecimal.ts diff --git a/repos/effect/packages/effect/src/BigInt.ts b/packages/effect/src/BigInt.ts similarity index 100% rename from repos/effect/packages/effect/src/BigInt.ts rename to packages/effect/src/BigInt.ts diff --git a/repos/effect/packages/effect/src/Boolean.ts b/packages/effect/src/Boolean.ts similarity index 100% rename from repos/effect/packages/effect/src/Boolean.ts rename to packages/effect/src/Boolean.ts diff --git a/repos/effect/packages/effect/src/Brand.ts b/packages/effect/src/Brand.ts similarity index 100% rename from repos/effect/packages/effect/src/Brand.ts rename to packages/effect/src/Brand.ts diff --git a/repos/effect/packages/effect/src/ByteSize.ts b/packages/effect/src/ByteSize.ts similarity index 100% rename from repos/effect/packages/effect/src/ByteSize.ts rename to packages/effect/src/ByteSize.ts diff --git a/packages/effect/src/Bytes.ts b/packages/effect/src/Bytes.ts new file mode 100644 index 000000000..106f29506 --- /dev/null +++ b/packages/effect/src/Bytes.ts @@ -0,0 +1,111 @@ +/** + * Joins and compares byte sequences stored in `Uint8Array` values. + * + * Byte-to-text conversions live in `TextEncoder` / `TextDecoder`, and hex or + * base64 conversions live in the `Encoding` modules. + * + * @stability unstable + * @since 4.0.6 + */ +import * as Equ from "./Equivalence.ts" +import * as order from "./Order.ts" + +/** + * Joins byte chunks into one new `Uint8Array`. + * + * **When to use** + * + * Use to combine frames, headers, or streamed chunks into a single buffer. + * + * **Gotchas** + * + * The result is always a new array, even when there is zero or one chunk, so + * it never aliases an input. + * + * **Example** (Joining byte chunks) + * + * ```ts import.meta.vitest + * import { Bytes } from "effect" + * + * const bytes = Bytes.concat([new Uint8Array([1, 2]), new Uint8Array([3])]) + * Array.from(bytes) // => [1, 2, 3] + * ``` + * + * @stability unstable + * @category combinators + * @since 4.0.6 + */ +export const concat = (chunks: ReadonlyArray): Uint8Array => { + let length = 0 + for (let i = 0; i < chunks.length; i++) { + length += chunks[i].length + } + const out = new Uint8Array(length) + let offset = 0 + for (let i = 0; i < chunks.length; i++) { + out.set(chunks[i], offset) + offset += chunks[i].length + } + return out +} + +/** + * Compares byte sequences for equality, byte by byte. + * + * **Gotchas** + * + * This returns as soon as a byte differs, so it is not constant-time. Do not + * use it to compare secrets. + * + * **Example** (Comparing byte sequences) + * + * ```ts import.meta.vitest + * import { Bytes } from "effect" + * + * Bytes.Equivalence(new Uint8Array([1, 2]), new Uint8Array([1, 2])) // => true + * Bytes.Equivalence(new Uint8Array([1, 2]), new Uint8Array([1])) // => false + * ``` + * + * @stability unstable + * @category instances + * @since 4.0.6 + */ +export const Equivalence: Equ.Equivalence = Equ.make((self, that) => { + if (self.length !== that.length) { + return false + } + for (let i = 0; i < self.length; i++) { + if (self[i] !== that[i]) { + return false + } + } + return true +}) + +/** + * Orders byte sequences lexicographically by unsigned byte value, placing a + * prefix before any longer sequence that starts with it. + * + * **Example** (Sorting byte sequences) + * + * ```ts import.meta.vitest + * import { Bytes } from "effect" + * + * Bytes.Order(new Uint8Array([1, 2]), new Uint8Array([1, 3])) // => -1 + * Bytes.Order(new Uint8Array([2]), new Uint8Array([1, 9])) // => 1 + * Bytes.Order(new Uint8Array([1]), new Uint8Array([1, 0])) // => -1 + * ``` + * + * @stability unstable + * @category instances + * @since 4.0.6 + */ +export const Order: order.Order = order.make((self, that) => { + const length = Math.min(self.length, that.length) + for (let i = 0; i < length; i++) { + if (self[i] !== that[i]) { + return self[i] < that[i] ? -1 : 1 + } + } + return self.length < that.length ? -1 : self.length > that.length ? 1 : 0 +}) diff --git a/repos/effect/packages/effect/src/Cache.ts b/packages/effect/src/Cache.ts similarity index 100% rename from repos/effect/packages/effect/src/Cache.ts rename to packages/effect/src/Cache.ts diff --git a/repos/effect/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts similarity index 100% rename from repos/effect/packages/effect/src/Cause.ts rename to packages/effect/src/Cause.ts diff --git a/repos/effect/packages/effect/src/Channel.ts b/packages/effect/src/Channel.ts similarity index 97% rename from repos/effect/packages/effect/src/Channel.ts rename to packages/effect/src/Channel.ts index 35f3d6955..ba5951ad0 100644 --- a/repos/effect/packages/effect/src/Channel.ts +++ b/packages/effect/src/Channel.ts @@ -22,7 +22,7 @@ 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 } from "./internal/effect.ts" +import { ClockRef, endSpan, findRecoverableError } from "./internal/effect.ts" import { addSpanStackTrace } from "./internal/tracer.ts" import * as Iterable from "./Iterable.ts" import * as Latch from "./Latch.ts" @@ -488,7 +488,7 @@ const asyncQueue = ( Effect.tap((queue) => Scope.provide(f(queue), scope).pipe( Effect.catchCause((cause) => Queue.failCause(queue, cause)), - Effect.forkIn(scope) + Effect.forkIn(scope, { startImmediately: true }) ) ) ) @@ -1701,6 +1701,12 @@ export const fromSchedule = ( /** * Creates a channel from a lazily supplied Web `ReadableStream`. * + * **Details** + * + * Read failures are mapped with `onError`. Finalization cancels the reader and + * releases its lock, or only releases the lock when `releaseLockOnEnd` is set. + * A rejected cancel hook becomes a defect. + * * **Example** (Reading from a Web stream) * * ```ts import.meta.vitest @@ -1743,6 +1749,7 @@ export const pullIntoWritableStream = (options: { readonly writable: WritableStream readonly onError: (error: unknown) => E readonly closeOnDone?: boolean | undefined + readonly onAbort?: ((cause: Cause.Cause) => void) | undefined }): Pull.Pull => Effect.acquireUseRelease( Effect.sync(() => options.writable.getWriter()), @@ -1773,10 +1780,11 @@ export const pullIntoWritableStream = (options: { : loop return Effect.onError( withClose, - (cause) => - Pull.isDoneCause(cause) - ? Effect.void - : Effect.promise(() => writer.abort(cause).catch(constVoid)) + (cause) => { + if (Pull.isDoneCause(cause)) return Effect.void + options.onAbort?.(cause as Cause.Cause) + return Effect.promise(() => writer.abort(cause).catch(constVoid)) + } ) }, (writer) => Effect.sync(() => writer.releaseLock()) @@ -1867,7 +1875,12 @@ export const fromTransformStream = (options: { pull: upstream, writable: transform.writable, onError: options.onError, - closeOnDone: options.closeOnDone + closeOnDone: options.closeOnDone, + // Aborting errors the readable, so record the failure first. Otherwise a + // pending read reports the abort reason instead. + onAbort: (cause) => { + exit.current = Exit.failCause(cause) + } }).pipe( Effect.catchCause((cause) => { if (!Pull.isDoneCause(cause)) { @@ -1918,7 +1931,13 @@ const readableStreamToPullUnsafe = (options: { options.scope, options.releaseLockOnEnd ? Effect.sync(() => reader.releaseLock()) - : Effect.promise(() => reader.cancel().catch(constVoid)) + : Effect.promise(() => + reader.cancel().catch((error) => + // `closed` resolves only when the cancel hook failed. An errored + // stream rejects it with an error that was already reported. + reader.closed.then(() => Promise.reject(error), constVoid) + ) + ).pipe(Effect.ensuring(Effect.sync(() => reader.releaseLock()))) ), pull ) @@ -2235,7 +2254,7 @@ const mapEffectConcurrent = < const pull = yield* toTransform(self)(upstream, scope) const concurrencyN = options.concurrency === "unbounded" ? Number.MAX_SAFE_INTEGER - : options.concurrency + : Math.min(options.concurrency, Number.MAX_SAFE_INTEGER) const queue = yield* Queue.bounded>(0) yield* Scope.addFinalizer(forkedScope, Queue.shutdown(queue)) @@ -3323,12 +3342,12 @@ export const repeat: { let meta = Schedule.CurrentMetadata.defaultValue() return repeatLoop( provideServiceEffect(self, Schedule.CurrentMetadata, Effect.sync(() => meta)), - (done) => - step(done).pipe( + (exit): Pull.Pull => + Exit.isFailure(exit) ? Effect.failCause(exit.cause) : step(exit.value).pipe( Effect.map((meta_) => { meta = meta_ }), - Pull.catchDone(() => Cause.done(done)) + Pull.catchDone(() => Cause.done(exit.value)) ) ) }), @@ -3337,19 +3356,21 @@ export const repeat: { const repeatLoop = ( self: Channel, - onDone: (done: OutDone) => Pull.Pull -): Channel => + onExit: (exit: Exit.Exit, scope: Scope.Scope) => Pull.Pull +): Channel => fromTransform((upstream, scope) => Effect.sync(() => { - let currentPull: Pull.Pull | undefined - const makePull = (): Pull.Pull => { + let currentPull: Pull.Pull | undefined + const makePull = (): Pull.Pull => { const runScope = Scope.forkUnsafe(scope) return Effect.flatMap(toTransform(self)(upstream, runScope), (pull) => { - currentPull = Pull.catchDone(pull, (done) => - Scope.close(runScope, Exit.void).pipe( - Effect.flatMap(() => onDone(done as OutDone)), + currentPull = Effect.catchCause(pull, (cause) => { + const exit = Pull.doneExitFromCause(cause) as Exit.Exit + return Scope.close(runScope, exit).pipe( + Effect.flatMap(() => onExit(exit, scope)), Effect.flatMap(makePull) - )) + ) + }) return currentPull }) } @@ -3366,7 +3387,8 @@ const repeatLoop = ( self: Channel -): Channel => repeatLoop(self, () => Effect.void) +): Channel => + repeatLoop(self, (exit) => Exit.isFailure(exit) ? Effect.failCause(exit.cause) : Effect.void) /** * Runs a schedule step for each output element while preserving the emitted @@ -4277,6 +4299,12 @@ export const catchCause: { * * Typed failures and interruptions are not caught. * + * **Gotchas** + * + * `catchDefect` recovers any cause that contains a defect, including causes + * that also contain typed errors. Typed error handlers such as `catch` skip + * those causes, so defects always take precedence. + * * **Example** (Recovering from a defect) * * ```ts import.meta.vitest @@ -4788,12 +4816,19 @@ const catch_: { InErr & InErr1, InDone & InDone1, Env | Env1 -> => catchCauseFilter(self, Cause.findError, (e) => f(e))) +> => catchCauseFilter(self, findRecoverableError, (e) => f(e))) export { /** * Recovers from typed channel errors by running a fallback channel. * + * **Gotchas** + * + * Defects take precedence over typed errors. If the cause also contains a + * defect, the handler does not run and the failure is propagated with every + * typed error converted to a defect. An interruption alongside a typed error + * does not prevent recovery. + * * @stability stable * @category error handling * @since 4.0.0 @@ -4902,6 +4937,12 @@ export const tapError: { * are handled by `orElse` when provided. Without `orElse`, non-matching errors * are re-failed. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * @see {@link catch_ catch} for recovering from every typed channel error * @see {@link catchFilter} for selecting typed errors with a `Filter` * @see {@link catchTag} for selecting tagged typed errors @@ -4930,7 +4971,7 @@ export const catchIf: { InDone2 = unknown, Env2 = never >( - refinement: Predicate.Refinement, + refinement: Predicate.Refinement, EB>, f: (failure: EB) => Channel, orElse?: | (( @@ -5018,7 +5059,7 @@ export const catchIf: { Env2 = never >( self: Channel, - refinement: Predicate.Refinement, + refinement: Predicate.Refinement, EB>, f: (failure: EB) => Channel, orElse?: | (( @@ -5147,6 +5188,12 @@ export const catchIf: { * filter results are handled by `orElse` when provided. Without `orElse`, * failed filter results are re-failed. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * @see {@link catchIf} for selecting typed errors with a predicate * @see {@link catchTag} for selecting tagged typed errors * @see {@link catchCauseFilter} for selecting full causes with a `Filter` @@ -5311,6 +5358,12 @@ export const catchFilter: { * errors are handled by `orElse` when provided. Without `orElse`, * non-matching errors are re-failed. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * @stability stable * @category error handling * @since 4.0.0 @@ -5462,6 +5515,12 @@ export const catchTag: { /** * Catches a specific reason within a tagged error. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Recovering from nested reasons) * * ```ts import.meta.vitest @@ -5672,6 +5731,12 @@ export const catchReason: { /** * Catches multiple reasons within a tagged error using an object of handlers. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * @stability stable * @category error handling * @since 4.0.0 @@ -5944,9 +6009,12 @@ export const unwrapReason: { ) as any) /** - * Returns a new channel, which is the same as this one, except the failure - * value of the returned channel is created by applying the specified function - * to the failure value of this channel. + * Transforms the typed errors of a channel using the specified function. + * + * **Details** + * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. * * @stability stable * @category error handling @@ -5965,7 +6033,11 @@ export const mapError: { } = dual(2, ( self: Channel, f: (err: OutErr) => OutErr2 -): Channel => catch_(self, (err) => fail(f(err)))) +): Channel => + transformPull( + self, + (pull) => Effect.succeed(Effect.mapError(pull, (err) => Cause.isDone(err) ? err : f(err))) + )) /** * Converts all errors in the channel to defects (unrecoverable failures). @@ -6013,6 +6085,13 @@ export const orDie = < * * Use the `log` option to emit the full {@link Cause} when the channel fails. * + * **Gotchas** + * + * Defects take precedence over typed errors. If the cause also contains a + * defect, the failure is not ignored and is propagated with every typed error + * converted to a defect. An interruption alongside a typed error does not + * prevent the error from being ignored. + * * @stability stable * @category error handling * @since 4.0.0 @@ -6112,6 +6191,12 @@ export const ignoreCause: < * Returns a new channel that retries this channel according to the specified * schedule whenever it fails. * + * **Gotchas** + * + * Defects take precedence over typed errors. A failure whose cause contains a + * defect is not retried and is propagated with every typed error converted to a + * defect. A typed error alongside an interruption is retried. + * * @stability stable * @category error handling * @since 4.0.0 @@ -6155,29 +6240,20 @@ export const retry: { return Effect.void }) const resolvedSchedule = typeof schedule === "function" ? schedule(identity_) : schedule - const loop: Channel< - OutElem, - OutErr | SE, - OutDone, - InElem, - InErr, - InDone, - Env | SR - > = catch_( - withReset, - Effect.fnUntraced( - function*(error) { - if (!step) { - step = yield* Schedule.toStepWithMetadata(resolvedSchedule) - } - meta = yield* step(error) - return loop - }, - (effect, error) => Pull.catchDone(effect, () => Effect.succeed(fail(error))), - unwrap + return repeatLoop(withReset, (exit, scope) => { + if (Exit.isSuccess(exit)) return Cause.done(exit.value) + const error = findRecoverableError(exit.cause) + if (Result.isFailure(error)) return Effect.failCause(error.failure) + return Effect.gen(function*() { + if (!step) { + step = yield* Schedule.toStepWithMetadata(resolvedSchedule) + } + meta = yield* step(error.success) + }).pipe( + Pull.catchDone(() => Effect.fail(error.success)), + Scope.provide(scope) ) - ) - return loop + }) })) /** @@ -6425,7 +6501,7 @@ export const mergeAll: { : Math.max(1, concurrency) const semaphore = switch_ ? undefined : Semaphore.makeUnsafe(concurrencyN) const doneLatch = yield* Latch.make(true) - const fibers = new Set>() + const fibers = new Map, Scope.Closeable>() const queue = yield* Queue.bounded>( bufferSize @@ -6455,10 +6531,14 @@ export const mergeAll: { const childPull = yield* toTransform(channel)(upstream, childScope) while (fibers.size >= concurrencyN) { - const fiber = Iterable.headUnsafe(fibers) + const [fiber, fiberScope] = Iterable.headUnsafe(fibers) fibers.delete(fiber) if (fibers.size === 0) yield* doneLatch.open yield* Fiber.interrupt(fiber) + // A worker interrupted before it starts never closes its own + // scope. Finalizer failures of replaced workers are discarded + // either way. + yield* Effect.ignoreCause(Scope.close(fiberScope, Exit.interrupt())) } const fiber = yield* childPull.pipe( @@ -6482,7 +6562,7 @@ export const mergeAll: { ) doneLatch.closeUnsafe() - fibers.add(fiber) + fibers.set(fiber, childScope) } }).pipe( Effect.catchCause((cause) => { @@ -6850,8 +6930,9 @@ export const splitLines = (): Channel< * **Details** * * Input chunks are decoded with streaming enabled so multi-byte characters may - * span `Uint8Array` boundaries. The optional `encoding` and `options` are - * passed to `TextDecoder`. + * span `Uint8Array` boundaries. On normal completion, incomplete trailing + * characters become U+FFFD or, with `fatal: true`, a defect. The optional + * `encoding` and `options` are passed to `TextDecoder`. * * @stability stable * @category decoding @@ -6872,7 +6953,15 @@ export const decodeText = ( Effect.sync(() => { const decoder = new TextDecoder(encoding, options) const streamOptions = { stream: true } - return Effect.map(upstream, Arr.map((line) => decoder.decode(line, streamOptions))) + let pull: Pull.Pull, Err, Done> = upstream.pipe( + Effect.map(Arr.map((bytes) => decoder.decode(bytes, streamOptions))), + Pull.catchDone((leftover) => { + pull = Cause.done(leftover as Done) + const last = decoder.decode() + return last.length > 0 ? Effect.succeed(Arr.of(last)) : pull + }) + ) + return Effect.suspend(() => pull) }) ) diff --git a/repos/effect/packages/effect/src/ChannelSchema.ts b/packages/effect/src/ChannelSchema.ts similarity index 100% rename from repos/effect/packages/effect/src/ChannelSchema.ts rename to packages/effect/src/ChannelSchema.ts diff --git a/repos/effect/packages/effect/src/Chunk.ts b/packages/effect/src/Chunk.ts similarity index 98% rename from repos/effect/packages/effect/src/Chunk.ts rename to packages/effect/src/Chunk.ts index 364d98e47..dcca7c24b 100644 --- a/repos/effect/packages/effect/src/Chunk.ts +++ b/packages/effect/src/Chunk.ts @@ -521,6 +521,11 @@ export const reverse: >(self: S) => Chunk.With(self: Chunk, index: number): Option } = dual( 2, - (self: Chunk, index: number): Option => - index < 0 || index >= self.length ? O.none() : O.some(getUnsafe(self, index)) + (self: Chunk, index: number): Option => { + const i = Math.floor(index) + return i >= 0 && i < self.length ? O.some(getUnchecked(self, i)) : O.none() + } ) /** @@ -624,7 +631,8 @@ export const fromNonEmptyArrayUnsafe = (self: NonEmptyReadonlyArray): NonE * * **Gotchas** * - * Throws if the index is out of bounds. + * Throws if the floored index is out of bounds, including for `NaN` and + * infinite indexes. * * **Example** (Accessing elements unsafely) * @@ -649,32 +657,33 @@ export const getUnsafe: { (self: Chunk, index: number): A } = dual(2, (self: Chunk, index: number): A => { const i = Math.floor(index) + if (!(i >= 0 && i < self.length)) { + throw new Error(`Index out of bounds: ${i}`) + } + return getUnchecked(self, i) +}) + +const getUnchecked = (self: Chunk, i: number): A => { switch (self.backing._tag) { case "IEmpty": { throw new Error(`Index out of bounds: ${i}`) } case "ISingleton": { - if (index !== 0) { - throw new Error(`Index out of bounds: ${i}`) - } return self.backing.a } case "IArray": { - if (i >= self.length || i < 0) { - throw new Error(`Index out of bounds: ${i}`) - } return self.backing.array[i]! } case "IConcat": { return i < self.left.length - ? getUnsafe(self.left, i) - : getUnsafe(self.right, i - self.left.length) + ? getUnchecked(self.left, i) + : getUnchecked(self.right, i - self.left.length) } case "ISlice": { - return getUnsafe(self.backing.chunk, i + self.backing.offset) + return getUnchecked(self.backing.chunk, i + self.backing.offset) } } -}) +} /** * Appends the specified element to the end of the `Chunk`. @@ -2224,7 +2233,12 @@ export const takeWhile: { }) /** - * Creates a Chunks of unique values, in order, from all given Chunks. + * Computes the union of two chunks using Effect equality. + * + * **Gotchas** + * + * Each input must already be deduplicated. Use {@link dedupe} to remove + * duplicates from an input first. * * **Example** (Unioning chunks) * @@ -2234,11 +2248,6 @@ export const takeWhile: { * const chunk1 = Chunk.make(1, 2, 3) * const chunk2 = Chunk.make(3, 4, 5) * Chunk.toArray(Chunk.union(chunk1, chunk2)) // => [1, 2, 3, 4, 5] - * - * // Handles duplicates within the same chunk - * const withDupes1 = Chunk.make(1, 1, 2) - * const withDupes2 = Chunk.make(2, 3, 3) - * Chunk.toArray(Chunk.union(withDupes1, withDupes2)) // => [1, 2, 3] * ``` * * @stability stable diff --git a/repos/effect/packages/effect/src/Clock.ts b/packages/effect/src/Clock.ts similarity index 100% rename from repos/effect/packages/effect/src/Clock.ts rename to packages/effect/src/Clock.ts diff --git a/repos/effect/packages/effect/src/Combiner.ts b/packages/effect/src/Combiner.ts similarity index 100% rename from repos/effect/packages/effect/src/Combiner.ts rename to packages/effect/src/Combiner.ts diff --git a/repos/effect/packages/effect/src/Config.ts b/packages/effect/src/Config.ts similarity index 99% rename from repos/effect/packages/effect/src/Config.ts rename to packages/effect/src/Config.ts index ba1819372..610c2ebbb 100644 --- a/repos/effect/packages/effect/src/Config.ts +++ b/packages/effect/src/Config.ts @@ -706,7 +706,6 @@ const toConfigCursorAST = memoize((root: SchemaAST.AST): SchemaAST.AST => { seen.add(ast) switch (ast._tag) { case "Objects": { - const matchesIndex = ast.indexSignatures.map((is) => SchemaParser._is(is.parameter)) const materialize = Effect.fnUntraced(function*(cursor: ConfigCursor) { if (cursor.node?._tag !== "Record") { return undefined @@ -716,9 +715,15 @@ const toConfigCursorAST = memoize((root: SchemaAST.AST): SchemaAST.AST => { for (const property of ast.propertySignatures) { if (typeof property.name === "string") keys.add(property.name) } - if (matchesIndex.length > 0) { + if (ast.indexSignatures.length > 0) { + const input: Record = {} for (const key of node.keys) { - if (matchesIndex.some((matches) => matches(key))) keys.add(key) + InternalRecord.assignProperty(input, key, undefined) + } + for (const is of ast.indexSignatures) { + for (const key of SchemaAST.getIndexSignatureKeys(input, is.parameter)) { + if (typeof key === "string") keys.add(key) + } } } const out: Record = {} diff --git a/repos/effect/packages/effect/src/ConfigProvider.ts b/packages/effect/src/ConfigProvider.ts similarity index 99% rename from repos/effect/packages/effect/src/ConfigProvider.ts rename to packages/effect/src/ConfigProvider.ts index b07eef48f..6d728b454 100644 --- a/repos/effect/packages/effect/src/ConfigProvider.ts +++ b/packages/effect/src/ConfigProvider.ts @@ -1127,11 +1127,12 @@ function interpolate( parsed: Record, seen: ReadonlySet ): string { - const reference = /(? 0 && envValue[match.index - 1] === "\\") continue const [, name] = match let end = reference.lastIndex let defaultValue = "" diff --git a/repos/effect/packages/effect/src/Console.ts b/packages/effect/src/Console.ts similarity index 100% rename from repos/effect/packages/effect/src/Console.ts rename to packages/effect/src/Console.ts diff --git a/repos/effect/packages/effect/src/Context.ts b/packages/effect/src/Context.ts similarity index 100% rename from repos/effect/packages/effect/src/Context.ts rename to packages/effect/src/Context.ts diff --git a/repos/effect/packages/effect/src/Cron.ts b/packages/effect/src/Cron.ts similarity index 98% rename from repos/effect/packages/effect/src/Cron.ts rename to packages/effect/src/Cron.ts index 477667a2a..2bee2f2af 100644 --- a/repos/effect/packages/effect/src/Cron.ts +++ b/packages/effect/src/Cron.ts @@ -815,8 +815,8 @@ export const next = (cron: Cron, now?: DateTime.DateTime.Input): Date => { * * **Gotchas** * - * The search is strict: if the supplied date/time already matches the schedule, - * the result is the earlier occurrence. + * The search is strict: if the supplied date/time is exactly an occurrence of + * the schedule, the result is the earlier occurrence. * * @see {@link next} for finding the next scheduled occurrence * @@ -866,7 +866,10 @@ const stepCron = (cron: Cron, now: DateTime.DateTime.Input | undefined, directio }).epochMilliseconds <= zoned.epochMilliseconds const result = dateTime.mutate(zoned, (current) => { - current.setUTCSeconds(current.getUTCSeconds() + tick, 0) + // Start from the nearest whole second strictly past the input in the search + // direction. Going backward, a fractional input already has one. + const offset = reverse && current.getUTCMilliseconds() > 0 ? 0 : tick + current.setUTCSeconds(current.getUTCSeconds() + offset, 0) for (let i = 0; i < 10_000; i++) { if (cron.seconds.size !== 0) { diff --git a/repos/effect/packages/effect/src/Crypto.ts b/packages/effect/src/Crypto.ts similarity index 100% rename from repos/effect/packages/effect/src/Crypto.ts rename to packages/effect/src/Crypto.ts diff --git a/repos/effect/packages/effect/src/Data.ts b/packages/effect/src/Data.ts similarity index 100% rename from repos/effect/packages/effect/src/Data.ts rename to packages/effect/src/Data.ts diff --git a/repos/effect/packages/effect/src/DateTime.ts b/packages/effect/src/DateTime.ts similarity index 100% rename from repos/effect/packages/effect/src/DateTime.ts rename to packages/effect/src/DateTime.ts diff --git a/repos/effect/packages/effect/src/Deferred.ts b/packages/effect/src/Deferred.ts similarity index 100% rename from repos/effect/packages/effect/src/Deferred.ts rename to packages/effect/src/Deferred.ts diff --git a/repos/effect/packages/effect/src/Differ.ts b/packages/effect/src/Differ.ts similarity index 100% rename from repos/effect/packages/effect/src/Differ.ts rename to packages/effect/src/Differ.ts diff --git a/repos/effect/packages/effect/src/Duration.ts b/packages/effect/src/Duration.ts similarity index 93% rename from repos/effect/packages/effect/src/Duration.ts rename to packages/effect/src/Duration.ts index 5d4a3a402..d02081a09 100644 --- a/repos/effect/packages/effect/src/Duration.ts +++ b/packages/effect/src/Duration.ts @@ -35,6 +35,10 @@ const bigint60 = BigInt(60) const bigint1e3 = BigInt(1_000) const bigint1e6 = BigInt(1_000_000) const bigint1e9 = BigInt(1_000_000_000) +const bigintNanosPerMinute = BigInt(60_000_000_000) +const bigintNanosPerHour = BigInt(3_600_000_000_000) +const bigintNanosPerDay = BigInt(86_400_000_000_000) +const bigintNanosPerWeek = BigInt(604_800_000_000_000) const roundTiesAwayFromZero = (input: number): bigint => BigInt(Math.sign(input) * Math.round(Math.abs(input))) @@ -43,6 +47,14 @@ const roundMillisToNanos = (millis: number): bigint => const fromNanosNumber = (nanos: number): Duration => make(Number.isFinite(nanos) ? roundTiesAwayFromZero(nanos) : nanos) +const nanosToUnit = (nanos: bigint, divisor: number): number => { + const value = Number(nanos) + // Preserve ordinary-input rounding; scale first only when nanoseconds overflow. + if (Number.isFinite(value)) return value / divisor + const scale = BigInt(divisor) + return Number(nanos / scale) + Number(nanos % scale) / divisor +} + const parseNanos = (input: string, scale: bigint): bigint => { const decimalIndex = input.indexOf(".") if (decimalIndex === -1) return BigInt(input) * scale @@ -62,6 +74,14 @@ const divideTiesAwayFromZero = (numerator: bigint, denominator: bigint): bigint return numerator < bigint0 ? -rounded : rounded } +const fromMillisString = (input: string, scale: number): Duration => { + const millis = Number(input) * scale + if (!Number.isFinite(millis)) return make(millis) + const nanos = parseNanos(input, BigInt(scale) * bigint1e6) + // Keep the millisecond representation only when it preserves the exact value. + return make(Number.isInteger(millis) && BigInt(millis) * bigint1e6 === nanos ? millis : nanos) +} + const nanosToHrTime = (nanos: bigint): [seconds: number, nanos: number] => { const sign = nanos < bigint0 ? -bigint1 : bigint1 const absolute = nanos < bigint0 ? -nanos : nanos @@ -274,26 +294,25 @@ export const fromInputUnsafe = (input: Input): Duration => { if (unit === "micro" || unit === "micros") { return nanos(parseNanos(valueStr, bigint1e3)) } - const value = Number(valueStr) switch (unit) { case "milli": case "millis": - return millis(value) + return fromMillisString(valueStr, 1) case "second": case "seconds": - return seconds(value) + return fromMillisString(valueStr, 1_000) case "minute": case "minutes": - return minutes(value) + return fromMillisString(valueStr, 60_000) case "hour": case "hours": - return hours(value) + return fromMillisString(valueStr, 3_600_000) case "day": case "days": - return days(value) + return fromMillisString(valueStr, 86_400_000) case "week": case "weeks": - return weeks(value) + return fromMillisString(valueStr, 604_800_000) } break } @@ -313,7 +332,10 @@ export const fromInputUnsafe = (input: Input): Duration => { if (input[0] === Infinity || input[1] === Infinity) { return infinity } - return fromNanosNumber(input[0] * 1_000_000_000 + input[1]) + const nanos = input[0] * 1_000_000_000 + input[1] + return Number.isFinite(nanos) && Number.isInteger(input[0]) && Number.isInteger(input[1]) + ? make(BigInt(input[0]) * bigint1e9 + BigInt(input[1])) + : fromNanosNumber(nanos) } const obj = input as DurationObject let millis = 0 @@ -324,13 +346,41 @@ export const fromInputUnsafe = (input: Input): Duration => { if (obj.minutes) millis += obj.minutes * 60_000 if (obj.seconds) millis += obj.seconds * 1_000 if (obj.milliseconds) millis += obj.milliseconds - if (!obj.microseconds && !obj.nanoseconds) return make(millis) - return fromNanosNumber(millis * 1_000_000 + (obj.microseconds ?? 0) * 1_000 + (obj.nanoseconds ?? 0)) + if (!obj.microseconds && !obj.nanoseconds) { + if (!Number.isFinite(millis)) return make(millis) + const exact = sumObjectNanos(obj) + // keep the Millis representation when the numeric total is already exact + return exact === undefined || BigInt(millis) * bigint1e6 === exact ? make(millis) : make(exact) + } + const nanos = millis * 1_000_000 + (obj.microseconds ?? 0) * 1_000 + (obj.nanoseconds ?? 0) + const exact = Number.isFinite(nanos) ? sumObjectNanos(obj) : undefined + return exact === undefined ? fromNanosNumber(nanos) : make(exact) } } return invalid(input) } +const isIntegerOrUnset = (n: number | undefined): boolean => !n || Number.isInteger(n) + +// sums the fields exactly, or returns undefined if any field is fractional +const sumObjectNanos = (obj: DurationObject): bigint | undefined => { + if ( + !isIntegerOrUnset(obj.weeks) || !isIntegerOrUnset(obj.days) || !isIntegerOrUnset(obj.hours) || + !isIntegerOrUnset(obj.minutes) || !isIntegerOrUnset(obj.seconds) || !isIntegerOrUnset(obj.milliseconds) || + !isIntegerOrUnset(obj.microseconds) || !isIntegerOrUnset(obj.nanoseconds) + ) return undefined + let nanos = bigint0 + if (obj.weeks) nanos += BigInt(obj.weeks) * bigintNanosPerWeek + if (obj.days) nanos += BigInt(obj.days) * bigintNanosPerDay + if (obj.hours) nanos += BigInt(obj.hours) * bigintNanosPerHour + if (obj.minutes) nanos += BigInt(obj.minutes) * bigintNanosPerMinute + if (obj.seconds) nanos += BigInt(obj.seconds) * bigint1e9 + if (obj.milliseconds) nanos += BigInt(obj.milliseconds) * bigint1e6 + if (obj.microseconds) nanos += BigInt(obj.microseconds) * bigint1e3 + if (obj.nanoseconds) nanos += BigInt(obj.nanoseconds) + return nanos +} + const invalid = (input: unknown): never => { throw new Error(`Invalid Input: ${input}`) } @@ -817,7 +867,7 @@ export const weeks = (weeks: number): Duration => make(weeks * 604_800_000) export const toMillis = (self: Input): number => match(fromInputUnsafe(self), { onMillis: identity, - onNanos: (nanos) => Number(nanos) / 1_000_000, + onNanos: (nanos) => nanosToUnit(nanos, 1_000_000), onInfinity: () => Infinity, onNegativeInfinity: () => -Infinity }) @@ -841,7 +891,7 @@ export const toMillis = (self: Input): number => export const toSeconds = (self: Input): number => match(fromInputUnsafe(self), { onMillis: (millis) => millis / 1_000, - onNanos: (nanos) => Number(nanos) / 1_000_000_000, + onNanos: (nanos) => nanosToUnit(nanos, 1_000_000_000), onInfinity: () => Infinity, onNegativeInfinity: () => -Infinity }) @@ -865,7 +915,7 @@ export const toSeconds = (self: Input): number => export const toMinutes = (self: Input): number => match(fromInputUnsafe(self), { onMillis: (millis) => millis / 60_000, - onNanos: (nanos) => Number(nanos) / 60_000_000_000, + onNanos: (nanos) => nanosToUnit(nanos, 60_000_000_000), onInfinity: () => Infinity, onNegativeInfinity: () => -Infinity }) @@ -889,7 +939,7 @@ export const toMinutes = (self: Input): number => export const toHours = (self: Input): number => match(fromInputUnsafe(self), { onMillis: (millis) => millis / 3_600_000, - onNanos: (nanos) => Number(nanos) / 3_600_000_000_000, + onNanos: (nanos) => nanosToUnit(nanos, 3_600_000_000_000), onInfinity: () => Infinity, onNegativeInfinity: () => -Infinity }) @@ -913,7 +963,7 @@ export const toHours = (self: Input): number => export const toDays = (self: Input): number => match(fromInputUnsafe(self), { onMillis: (millis) => millis / 86_400_000, - onNanos: (nanos) => Number(nanos) / 86_400_000_000_000, + onNanos: (nanos) => nanosToUnit(nanos, 86_400_000_000_000), onInfinity: () => Infinity, onNegativeInfinity: () => -Infinity }) @@ -937,7 +987,7 @@ export const toDays = (self: Input): number => export const toWeeks = (self: Input): number => match(fromInputUnsafe(self), { onMillis: (millis) => millis / 604_800_000, - onNanos: (nanos) => Number(nanos) / 604_800_000_000_000, + onNanos: (nanos) => nanosToUnit(nanos, 604_800_000_000_000), onInfinity: () => Infinity, onNegativeInfinity: () => -Infinity }) diff --git a/repos/effect/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts similarity index 97% rename from repos/effect/packages/effect/src/Effect.ts rename to packages/effect/src/Effect.ts index 94a25e42f..6410d6568 100644 --- a/repos/effect/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -1,3 +1,5 @@ +/// + /** * Describes workflows that run only when executed by the Effect runtime. * @@ -882,8 +884,9 @@ export const whileLoop: (options: { * * **Details** * - * An optional `AbortSignal` can be provided to allow for interruption of the - * wrapped `Promise` API. + * If `evaluate` declares a parameter, it receives an `AbortSignal` that is + * aborted if the effect is interrupted. Zero-argument thunks do not allocate + * an `AbortController`. * * **Gotchas** * @@ -893,6 +896,13 @@ export const whileLoop: (options: { * Interruption aborts the provided `AbortSignal`, but the underlying * asynchronous operation only stops if it observes that signal. * + * The parameter check reads the `length` property of `evaluate`, which does + * not count a rest parameter or any parameter from the first default value + * onward. `(...args) => ...` and `(signal = fallback) => ...` therefore + * receive `undefined` instead of a signal, as do wrappers that forward + * arguments through rest parameters. Declare the signal as an ordinary + * parameter: `(signal) => ...`. + * * **Example** (Wrapping a non-rejecting Promise) * * ```ts import.meta.vitest @@ -937,9 +947,10 @@ export const promise: ( * Passing `{ try, catch }` uses `catch` to map failures to an error of type * `E`. * - * The thunk receives an `AbortSignal` that is aborted if the effect is - * interrupted. The underlying asynchronous operation only stops if it observes - * that signal. + * If the thunk declares a parameter, it receives an `AbortSignal` that is + * aborted if the effect is interrupted. The underlying asynchronous operation + * only stops if it observes that signal. Zero-argument thunks do not allocate + * an `AbortController`. * * **Gotchas** * @@ -947,6 +958,13 @@ export const promise: ( * defect. Return the error value you want in the error channel instead of * throwing it. * + * The parameter check reads the thunk's `length` property, which does not + * count a rest parameter or any parameter from the first default value + * onward. `(...args) => ...` and `(signal = fallback) => ...` therefore + * receive `undefined` instead of a signal, as do wrappers that forward + * arguments through rest parameters. Declare the signal as an ordinary + * parameter: `(signal) => ...`. + * * **Example** (Wrapping a fetch request that may fail) * * ```ts import.meta.vitest @@ -1230,10 +1248,20 @@ export { * * **Details** * - * The registration function receives a `resume` callback and, when requested, - * an `AbortSignal`. Call `resume` at most once with the effect that should - * complete the fiber; later calls are ignored. Return an optional cleanup - * effect from the registration function to run if the fiber is interrupted. + * The registration function receives a `resume` callback and, if it declares + * a second parameter, an `AbortSignal` that is aborted if the fiber is + * interrupted. Call `resume` at most once with the effect that should complete + * the fiber; later calls are ignored. Return an optional cleanup effect from + * the registration function to run if the fiber is interrupted. + * + * **Gotchas** + * + * The parameter check reads the registration function's `length` property, + * which must be at least `2` and does not count a rest parameter or any + * parameter from the first default value onward. `(...args) => ...` and + * `(resume, signal = fallback) => ...` therefore receive `undefined` instead + * of a signal, as do wrappers that forward arguments through rest parameters. + * Declare both as ordinary parameters: `(resume, signal) => ...`. * * **Example** (Integrating callback APIs) * @@ -1889,7 +1917,9 @@ export const fromResult: (result: Result.Result) => Effect = i * `Option.some` becomes a successful effect with the contained value, while * `Option.none` becomes a failed effect. By default the failure is a * `NoSuchElementError`, but you can provide an `onNone` callback to customize - * the error value. + * the error value. For `Option.none`, `onNone` is evaluated on each execution, + * not when the effect is constructed. It is never called for `Option.some`. + * If `onNone` throws, the effect dies with the thrown value as a defect. * * **Example** (Converting an Option into an Effect) * @@ -2302,9 +2332,12 @@ export const tap: { * * **Gotchas** * - * `result` only captures typed, recoverable failures. Defects and - * interruptions are not captured inside the `Result` and still fail the - * effect. + * `result` only captures typed, recoverable failures. If the cause contains a + * defect, nothing is captured and every typed error is converted to a defect, + * preserving reason order and interrupts. A cause with only interruptions + * still fails the effect, but an interruption alongside a typed error does not + * prevent capture. The first typed error becomes a `Result.Failure` and the + * interruption is discarded. * * **Example** (Capturing success or failure as Result) * @@ -2346,9 +2379,12 @@ export const result: (self: Effect) => Effect Effect, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> ( self: Effect, - refinement: Predicate.Refinement, + refinement: Predicate.Refinement, EB>, f: (e: EB) => Effect, orElse?: ((e: Exclude) => Effect) | undefined ): Effect, E2 | E3 | (A3 extends unassigned ? Exclude : never), R | R2 | R3> @@ -3484,6 +3561,12 @@ export const catchIf: { * `orElse` when provided. Without `orElse`, the original failure cause is * preserved. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * @see {@link catchIf} for predicate-based recovery from typed errors * @see {@link catchTag} for recovering from a single tagged error * @see {@link catchTags} for recovering from several tagged errors @@ -3652,6 +3735,9 @@ export const catchCauseFilter: { * Only the failure channel is transformed. The success channel and requirements * are preserved. * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * * **Example** (Transforming the error channel) * * ```ts import.meta.vitest @@ -3699,6 +3785,9 @@ export const mapError: { * the error and the success values without altering the overall success or * failure status of the effect. * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * * **Example** (Transforming success and failure channels) * * ```ts import.meta.vitest @@ -4148,7 +4237,9 @@ export declare namespace Retry { * applied. For example, `Schedule.recurs(3)` allows up to three retries after * the initial attempt. * - * Defects and interruptions are not retried. + * Defects take precedence over typed errors. A failure whose cause contains a + * defect is not retried and is propagated with every typed error converted to a + * defect. A typed error alongside an interruption is retried. * * **Example** (Retrying with a schedule) * @@ -4218,6 +4309,14 @@ export const retry: { * If the retries are exhausted and the effect still fails, it runs a fallback * effect instead. * + * **Gotchas** + * + * Defects take precedence over typed errors. A failure whose cause contains a + * defect is not retried and is propagated with every typed error converted to a + * defect. A typed error alongside an interruption is retried. When the cause + * contains a defect, `orElse` does not run. + * This applies to source failures only; policy failures propagate unchanged. + * * **Example** (Falling back after retries are exhausted) * * ```ts import.meta.vitest @@ -4274,8 +4373,8 @@ export const retryOrElse: { * **Details** * * Use `sandbox` when downstream error handling needs to distinguish typed - * failures, defects, and interruptions. Use `unsandbox` to restore the original - * typed error channel after cause-level handling. + * failures, defects, and interruptions. Use `Effect.catch(Effect.failCause)` to + * restore the original typed error channel after cause-level handling. * * **Example** (Exposing failures as causes) * @@ -4314,6 +4413,13 @@ export const sandbox: ( * Use the `log` option to emit the full {@link Cause} when the effect fails, * and `message` to prepend a custom log message. * + * **Gotchas** + * + * Defects take precedence over typed errors. If the cause also contains a + * defect, the failure is not ignored and is propagated with every typed error + * converted to a defect. An interruption alongside a typed error does not + * prevent the error from being ignored. + * * **Example** (Discarding success and failure values) * * ```ts import.meta.vitest @@ -4538,6 +4644,12 @@ export const withErrorReporting: < * * Defects and interruptions are not recovered by this operator. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Replacing failures with a value) * * ```ts import.meta.vitest @@ -5421,6 +5533,12 @@ export const when: { * to process the result if the effect succeeds, or handle the error if the * effect fails. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Matching success and failure values) * * ```ts import.meta.vitest @@ -5486,6 +5604,12 @@ export const match: { * the handlers immediately without fiber scheduling. For unresolved effects, * it falls back to the regular `match` behavior. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Pattern matching eagerly when possible) * * ```ts import.meta.vitest @@ -5741,6 +5865,12 @@ export const matchCauseEffect: { * performing logging, recovery, notification, or other effectful work. The * returned effect succeeds or fails according to the handler that is run. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Matching success and failure with effectful handlers) * * ```ts import.meta.vitest @@ -5976,9 +6106,9 @@ export const contextWith: ( ) => Effect = internal.contextWith /** - * Provides dependencies to an effect using layers or a context. Use `options.local` - * to build the layer every time; by default, layers are shared between provide - * calls. + * Provides dependencies to an effect using layers or a context. A layer already + * built by an enclosing `provide` or runtime is reused. Sibling `provide` calls + * build their own copies. Use `options.local` to build a fresh layer subtree. * * **Example** (Providing dependencies with a layer) * @@ -7788,6 +7918,10 @@ export const forever: < * The source effect is always evaluated once before the schedule is stepped. * The schedule controls additional repetitions, not the initial execution. * + * If the effect or policy fails with a cause that contains a defect, the + * failure is propagated with every typed error converted to a defect. + * An interruption alongside a typed error does not block the typed error path. + * * **Example** (Repeating successful effects with a schedule) * * ```ts import.meta.vitest @@ -7877,6 +8011,13 @@ export const repeat: { * otherwise it receives `None`. If the schedule completes normally, the * returned effect succeeds with the schedule's output. * + * **Gotchas** + * + * Defects take precedence over typed errors from either the effect or policy. + * If the cause contains a defect, `orElse` does not run and every typed error + * is converted to a defect. An interruption alongside a typed error does not + * prevent `orElse` from running. + * * **Example** (Recovering after repetition stops) * * ```ts import.meta.vitest @@ -8752,8 +8893,8 @@ export const requestUnsafe: ( * You can use the `forkChild` method whenever you want to execute an effect in a * new fiber, concurrently and without "blocking" the fiber executing other * effects. Using fibers can be tricky, so instead of using this method - * directly, consider other higher-level methods, such as `raceWith`, - * `zipPar`, and so forth. + * directly, consider higher-level methods such as `Effect.race`, + * `Effect.raceFirst`, or `Effect.all` with a concurrency option. * * The fiber returned by this method has methods to interrupt the fiber and to * wait for it to finish executing the effect. See `Fiber` for more @@ -8891,9 +9032,14 @@ export const forkScoped: < : (self: Effect) => Effect, never, R | Scope> = internal.forkScoped /** - * Forks the effect into a new fiber attached to the global scope. Because the - * new fiber is attached to the global scope, when the fiber executing the - * returned effect terminates, the forked fiber will continue running. + * Forks the effect into a new fiber with no parent and no scope, so its lifetime + * is independent of the forking fiber. + * + * **Details** + * + * The detached fiber inherits the forking fiber's context. It continues running + * until it completes or is explicitly interrupted through the returned handle, + * even after the forking fiber terminates. * * **Example** (Forking a detached fiber) * @@ -9051,11 +9197,11 @@ export interface RunOptions { * import { Effect, Fiber } from "effect" * const output: Array = [] * - * // ┌─── Effect + * // ┌─── Effect * // ▼ * const program = Effect.sync(() => { output.push("running...") }).pipe(Effect.as("done")) * - * // ┌─── RuntimeFiber + * // ┌─── Fiber * // ▼ * const fiber = Effect.runFork(program) * @@ -9390,9 +9536,10 @@ export const runPromiseExitWith: ( * * **Details** * - * If the effect fails, dies, is interrupted, or performs asynchronous work, - * `runSync` throws a `FiberFailure` instead of returning a value. Use - * `runSyncExit` when you want the failure captured as an `Exit`. + * If the effect fails, dies, or is interrupted, `runSync` throws the squashed + * cause instead of returning a value. Typed errors can be thrown directly, + * including non-Error values. Asynchronous work throws `Cause.AsyncFiberError`. + * Use `runSyncExit` when you want the failure captured as an `Exit`. * * **Example** (Running a synchronous effect) * @@ -15354,7 +15501,7 @@ type EffectifyArgs) => any> = Parameters exten * **Details** * * The original function is called with the supplied arguments plus a final - * callback. A non-null callback error fails the returned effect, while a + * callback. A truthy callback error fails the returned effect, while a * successful callback value becomes the effect success. Use `onError` to map * callback errors and `onSyncError` to turn synchronous throws into typed * failures; otherwise synchronous throws become defects. @@ -15415,16 +15562,26 @@ export const effectify: { ((fn: Function, onError?: (e: any, args: any) => any, onSyncError?: (e: any, args: any) => any) => (...args: Array) => callback((resume) => { + let synchronous = true + let pending: Effect | undefined try { fn(...args, (err: globalThis.Error | null, result: A) => { - if (err) { - resume(onError ? suspend(() => fail(onError(err, args))) : fail(err)) + const effect = err + ? onError ? suspend(() => fail(onError(err, args))) : fail(err) + : succeed(result) + if (synchronous) { + // Keep the first callback result until registration succeeds. + pending ??= effect } else { - resume(succeed(result)) + resume(effect) } }) } catch (err) { - resume(onSyncError ? fail(onSyncError(err, args)) : die(err)) + pending = onSyncError ? fail(onSyncError(err, args)) : die(err) + } + synchronous = false + if (pending) { + resume(pending) } })) as any @@ -15582,6 +15739,9 @@ export const mapEager: { * transform. Failure effects apply the mapping function immediately, and * pending effects fall back to regular `mapError` behavior. * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * * **Example** (Mapping errors eagerly when possible) * * ```ts import.meta.vitest @@ -15629,6 +15789,9 @@ export const mapErrorEager: { * `onFailure` immediately. Pending effects fall back to regular `mapBoth` * behavior. * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * * **Example** (Mapping both channels eagerly when possible) * * ```ts import.meta.vitest @@ -15723,6 +15886,12 @@ export const flatMapEager: { * Failure effects apply the catch function immediately, and pending effects * fall back to regular `catch` behavior. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Catching failures eagerly when possible) * * ```ts import.meta.vitest diff --git a/repos/effect/packages/effect/src/Effectable.ts b/packages/effect/src/Effectable.ts similarity index 100% rename from repos/effect/packages/effect/src/Effectable.ts rename to packages/effect/src/Effectable.ts diff --git a/repos/effect/packages/effect/src/Equal.ts b/packages/effect/src/Equal.ts similarity index 100% rename from repos/effect/packages/effect/src/Equal.ts rename to packages/effect/src/Equal.ts diff --git a/repos/effect/packages/effect/src/Equivalence.ts b/packages/effect/src/Equivalence.ts similarity index 99% rename from repos/effect/packages/effect/src/Equivalence.ts rename to packages/effect/src/Equivalence.ts index ecfd537fb..9120258eb 100644 --- a/repos/effect/packages/effect/src/Equivalence.ts +++ b/packages/effect/src/Equivalence.ts @@ -362,7 +362,8 @@ export const combine: { * comparison stops at the first equivalence that returns `false`. Empty * collections return an equivalence that always returns `true`. The result is * also an equivalence that satisfies reflexive, symmetric, and transitive - * properties. + * properties. The collection is materialized when the equivalence is created, + * so it must be finite. * * **Example** (Combining multiple field equivalences) * @@ -414,15 +415,17 @@ export const combine: { * @category combining * @since 2.0.0 */ -export const combineAll = (collection: Iterable>): Equivalence => - make((x, y) => { - for (const equivalence of collection) { +export const combineAll = (collection: Iterable>): Equivalence => { + const equivalences = Array.from(collection) + return make((x, y) => { + for (const equivalence of equivalences) { if (!equivalence(x, y)) { return false } } return true }) +} /** * Transforms an equivalence relation by mapping the input values before comparison. diff --git a/repos/effect/packages/effect/src/ErrorReporter.ts b/packages/effect/src/ErrorReporter.ts similarity index 100% rename from repos/effect/packages/effect/src/ErrorReporter.ts rename to packages/effect/src/ErrorReporter.ts diff --git a/repos/effect/packages/effect/src/ExecutionPlan.ts b/packages/effect/src/ExecutionPlan.ts similarity index 100% rename from repos/effect/packages/effect/src/ExecutionPlan.ts rename to packages/effect/src/ExecutionPlan.ts diff --git a/repos/effect/packages/effect/src/Exit.ts b/packages/effect/src/Exit.ts similarity index 98% rename from repos/effect/packages/effect/src/Exit.ts rename to packages/effect/src/Exit.ts index 00f9f46c9..1874d5308 100644 --- a/repos/effect/packages/effect/src/Exit.ts +++ b/packages/effect/src/Exit.ts @@ -835,8 +835,10 @@ export const map: { * * **Gotchas** * - * Only transforms typed errors (Fail reasons). If the Cause contains only - * defects or interruptions, the failure passes through unchanged. + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * + * If the Cause contains only defects or interruptions, no mapper is called. * * **Example** (Mapping over an error) * @@ -868,11 +870,14 @@ export const mapError: { * **Details** * * `onSuccess` transforms the value if the Exit is a Success. `onFailure` - * transforms the typed error if the Exit is a Failure with a Fail reason. + * transforms each typed error if the Exit is a Failure with Fail reasons. * Allocates a new Exit. * * **Gotchas** * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * * If the Cause contains only defects or interruptions, the failure passes * through unchanged. * diff --git a/repos/effect/packages/effect/src/Fiber.ts b/packages/effect/src/Fiber.ts similarity index 100% rename from repos/effect/packages/effect/src/Fiber.ts rename to packages/effect/src/Fiber.ts diff --git a/repos/effect/packages/effect/src/FiberHandle.ts b/packages/effect/src/FiberHandle.ts similarity index 98% rename from repos/effect/packages/effect/src/FiberHandle.ts rename to packages/effect/src/FiberHandle.ts index 015862edf..d5103ba0f 100644 --- a/repos/effect/packages/effect/src/FiberHandle.ts +++ b/packages/effect/src/FiberHandle.ts @@ -62,6 +62,8 @@ export interface FiberHandle extends Pipeable, state: { readonly _tag: "Open" fiber: Fiber.Fiber | undefined + /** Every fiber the handle still owns: the current one plus replaced fibers that are still interrupting. */ + readonly fibers: Set> } | { readonly _tag: "Closed" } @@ -105,7 +107,7 @@ const Proto = { const makeUnsafe = (): FiberHandle => { const self = Object.create(Proto) - self.state = { _tag: "Open", fiber: undefined } + self.state = { _tag: "Open", fiber: undefined, fibers: new Set() } self.deferred = Deferred.makeUnsafe() return self } @@ -154,12 +156,7 @@ export const make = (): Effect.Effect { - if (self.state._tag === "Open" && fiber === self.state.fiber) { - self.state.fiber = undefined + if (self.state._tag === "Open") { + self.state.fibers.delete(fiber) + if (fiber === self.state.fiber) { + self.state.fiber = undefined + } } if ( Exit.isFailure(exit) && diff --git a/repos/effect/packages/effect/src/FiberMap.ts b/packages/effect/src/FiberMap.ts similarity index 98% rename from repos/effect/packages/effect/src/FiberMap.ts rename to packages/effect/src/FiberMap.ts index 2790e50a3..671f50afb 100644 --- a/repos/effect/packages/effect/src/FiberMap.ts +++ b/packages/effect/src/FiberMap.ts @@ -67,6 +67,8 @@ export interface FiberMap state: { readonly _tag: "Open" readonly backing: MutableHashMap.MutableHashMap> + /** Every fiber the map still owns: the keyed ones plus replaced fibers that are still interrupting. */ + readonly fibers: Set> } | { readonly _tag: "Closed" } @@ -122,7 +124,7 @@ const makeUnsafe = ( deferred: Deferred.Deferred ): FiberMap => { const self = Object.create(Proto) - self.state = { _tag: "Open", backing } + self.state = { _tag: "Open", backing, fibers: new Set() } self.deferred = deferred return self } @@ -176,7 +178,7 @@ export const make = (): Effect.Effect Effect.Effect + /** + * Get information about a file at `path` without following symbolic links. + * See `File.Info` for metadata limits. + */ + readonly lstat: ( + path: string + ) => Effect.Effect /** * Create a directory at `path`. You can optionally specify the mode and * whether to recursively create nested directories. @@ -671,6 +678,9 @@ export const makeNoop = (fileSystem: Partial): FileSystem => link(path) { return Effect.fail(notFound("link", path)) }, + lstat(path) { + return Effect.fail(notFound("lstat", path)) + }, makeDirectory() { return Effect.die("not implemented") }, diff --git a/repos/effect/packages/effect/src/Filter.ts b/packages/effect/src/Filter.ts similarity index 100% rename from repos/effect/packages/effect/src/Filter.ts rename to packages/effect/src/Filter.ts diff --git a/repos/effect/packages/effect/src/Formatter.ts b/packages/effect/src/Formatter.ts similarity index 100% rename from repos/effect/packages/effect/src/Formatter.ts rename to packages/effect/src/Formatter.ts diff --git a/repos/effect/packages/effect/src/Function.ts b/packages/effect/src/Function.ts similarity index 100% rename from repos/effect/packages/effect/src/Function.ts rename to packages/effect/src/Function.ts diff --git a/repos/effect/packages/effect/src/Graph.ts b/packages/effect/src/Graph.ts similarity index 100% rename from repos/effect/packages/effect/src/Graph.ts rename to packages/effect/src/Graph.ts diff --git a/repos/effect/packages/effect/src/HKT.ts b/packages/effect/src/HKT.ts similarity index 100% rename from repos/effect/packages/effect/src/HKT.ts rename to packages/effect/src/HKT.ts diff --git a/repos/effect/packages/effect/src/Hash.ts b/packages/effect/src/Hash.ts similarity index 100% rename from repos/effect/packages/effect/src/Hash.ts rename to packages/effect/src/Hash.ts diff --git a/repos/effect/packages/effect/src/HashMap.ts b/packages/effect/src/HashMap.ts similarity index 100% rename from repos/effect/packages/effect/src/HashMap.ts rename to packages/effect/src/HashMap.ts diff --git a/repos/effect/packages/effect/src/HashRing.ts b/packages/effect/src/HashRing.ts similarity index 100% rename from repos/effect/packages/effect/src/HashRing.ts rename to packages/effect/src/HashRing.ts diff --git a/repos/effect/packages/effect/src/HashSet.ts b/packages/effect/src/HashSet.ts similarity index 100% rename from repos/effect/packages/effect/src/HashSet.ts rename to packages/effect/src/HashSet.ts diff --git a/repos/effect/packages/effect/src/Inspectable.ts b/packages/effect/src/Inspectable.ts similarity index 100% rename from repos/effect/packages/effect/src/Inspectable.ts rename to packages/effect/src/Inspectable.ts diff --git a/repos/effect/packages/effect/src/Iterable.ts b/packages/effect/src/Iterable.ts similarity index 99% rename from repos/effect/packages/effect/src/Iterable.ts rename to packages/effect/src/Iterable.ts index 380637585..5777f6a5e 100644 --- a/repos/effect/packages/effect/src/Iterable.ts +++ b/packages/effect/src/Iterable.ts @@ -2282,21 +2282,22 @@ export const dedupeAdjacentWith: { let first = true let last: A function next(): IteratorResult { - const result = iterator.next() - if (result.done) { - return { done: true, value: undefined } - } - if (first) { - first = false - last = result.value - return result - } - const current = result.value - if (isEquivalent(last, current)) { - return next() + while (true) { + const result = iterator.next() + if (result.done) { + return { done: true, value: undefined } + } + if (first) { + first = false + last = result.value + return result + } + const current = result.value + if (!isEquivalent(last, current)) { + last = current + return result + } } - last = current - return result } return { next } } diff --git a/repos/effect/packages/effect/src/JsonPatch.ts b/packages/effect/src/JsonPatch.ts similarity index 100% rename from repos/effect/packages/effect/src/JsonPatch.ts rename to packages/effect/src/JsonPatch.ts diff --git a/repos/effect/packages/effect/src/JsonPointer.ts b/packages/effect/src/JsonPointer.ts similarity index 100% rename from repos/effect/packages/effect/src/JsonPointer.ts rename to packages/effect/src/JsonPointer.ts diff --git a/repos/effect/packages/effect/src/JsonSchema.ts b/packages/effect/src/JsonSchema.ts similarity index 97% rename from repos/effect/packages/effect/src/JsonSchema.ts rename to packages/effect/src/JsonSchema.ts index 125b00f61..4b60929de 100644 --- a/repos/effect/packages/effect/src/JsonSchema.ts +++ b/packages/effect/src/JsonSchema.ts @@ -416,9 +416,15 @@ export function fromSchemaOpenApi3_1(js: JsonSchema): Document<"draft-2020-12"> * * **Gotchas** * - * OpenAPI 3.0 `nullable` is applied only when the same Schema Object has an - * explicit string `type`; other constraints such as `enum` are left - * unchanged. Unknown keywords, vendor extensions, and annotation values are + * OpenAPI 3.0 specifies that `nullable` applies only when the same Schema + * Object has an explicit string `type`. For compatibility with tooling that + * uses a single-member `allOf` for nullable references, this converter also + * rewrites `{ nullable: true, allOf: [schema] }` to + * `{ anyOf: [schema, { type: "null" }] }` when there is no string `type` or + * existing `anyOf`. Sibling constraints such as `enum` remain unchanged and + * may still reject null. + * + * Unknown keywords, vendor extensions, and annotation values are * copied opaquely unless their name would become active in Draft 2020-12 and * change meaning, in which case conversion throws. Siblings of a valid * OpenAPI 3.0 `$ref` are ignored. @@ -1331,8 +1337,17 @@ function convertOpenApi30(root: JsonSchema): JsonSchema { } rejectKeywordCollisions(source, OPEN_API_30_TO_2020_COLLISIONS, "Draft 2020-12", "OpenAPI 3.0") + const nullableAllOf = isOpenApi30NullableAllOf(source) + for (const key of Object.keys(source)) { const value = source[key] + if (nullableAllOf && key === "allOf") { + InternalRecord.assignProperty(out, "anyOf", [ + context.schemaAt(source.allOf[0], ["allOf", "0"], ["anyOf", "0"]), + { type: "null" } + ]) + continue + } if ( convertSubschemaKeyword( out, @@ -1369,6 +1384,16 @@ function convertOpenApi30(root: JsonSchema): JsonSchema { }) } +function isOpenApi30NullableAllOf( + source: JsonSchema +): source is JsonSchema & { readonly allOf: readonly [unknown] } { + return source.nullable === true && + typeof source.type !== "string" && + source.anyOf === undefined && + Array.isArray(source.allOf) && + source.allOf.length === 1 +} + function convertOpenApiExclusiveBound( source: JsonSchema, out: JsonSchema, diff --git a/repos/effect/packages/effect/src/Latch.ts b/packages/effect/src/Latch.ts similarity index 100% rename from repos/effect/packages/effect/src/Latch.ts rename to packages/effect/src/Latch.ts diff --git a/repos/effect/packages/effect/src/Layer.ts b/packages/effect/src/Layer.ts similarity index 99% rename from repos/effect/packages/effect/src/Layer.ts rename to packages/effect/src/Layer.ts index 982f864e8..2c7c6948f 100644 --- a/repos/effect/packages/effect/src/Layer.ts +++ b/packages/effect/src/Layer.ts @@ -1278,7 +1278,7 @@ export const mergeAll = , ...Array, Error, Services -> => fromBuild((memoMap, scope) => mergeAllEffect(layers, memoMap, scope)) +> => fromBuildMemo((memoMap, scope) => mergeAllEffect(layers, memoMap, scope)) /** * Merges this layer with another layer concurrently, producing a new layer with @@ -1366,7 +1366,7 @@ const provideWith = ( thatContext: Context.Context ) => Context.Context ) => - fromBuild((memoMap, scope) => + fromBuildMemo((memoMap, scope) => internalEffect.flatMap( Array.isArray(that) ? mergeAllEffect(that as NonEmptyArray>, memoMap, scope) diff --git a/repos/effect/packages/effect/src/LayerMap.ts b/packages/effect/src/LayerMap.ts similarity index 100% rename from repos/effect/packages/effect/src/LayerMap.ts rename to packages/effect/src/LayerMap.ts diff --git a/repos/effect/packages/effect/src/LayerRef.ts b/packages/effect/src/LayerRef.ts similarity index 100% rename from repos/effect/packages/effect/src/LayerRef.ts rename to packages/effect/src/LayerRef.ts diff --git a/repos/effect/packages/effect/src/LogLevel.ts b/packages/effect/src/LogLevel.ts similarity index 100% rename from repos/effect/packages/effect/src/LogLevel.ts rename to packages/effect/src/LogLevel.ts diff --git a/repos/effect/packages/effect/src/Logger.ts b/packages/effect/src/Logger.ts similarity index 100% rename from repos/effect/packages/effect/src/Logger.ts rename to packages/effect/src/Logger.ts diff --git a/repos/effect/packages/effect/src/ManagedRuntime.ts b/packages/effect/src/ManagedRuntime.ts similarity index 79% rename from repos/effect/packages/effect/src/ManagedRuntime.ts rename to packages/effect/src/ManagedRuntime.ts index 9f11d4412..f5ba8669d 100644 --- a/repos/effect/packages/effect/src/ManagedRuntime.ts +++ b/packages/effect/src/ManagedRuntime.ts @@ -1,3 +1,5 @@ +/// + /** * Runs many effects against services built once from a `Layer`. * @@ -10,11 +12,12 @@ * @stability stable * @since 2.0.0 */ -import type * as Context from "./Context.ts" +import * as Context from "./Context.ts" +import * as Deferred from "./Deferred.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import * as Fiber from "./Fiber.ts" -import { type FiberImpl, scopeRemoveFinalizerUnsafe } from "./internal/effect.ts" +import { fiberEnterUninterruptibleUnsafe, type FiberImpl, scopeRemoveFinalizerUnsafe } from "./internal/effect.ts" import * as Layer from "./Layer.ts" import { hasProperty } from "./Predicate.ts" import * as Scope from "./Scope.ts" @@ -209,6 +212,10 @@ export interface ManagedRuntime { * **When to use** * * Use to release this runtime's layer resources from Promise-based code. + * + * **Details** + * + * Calls made while a disposal is in progress wait for it to finish. */ readonly dispose: () => Promise @@ -228,10 +235,32 @@ export interface ManagedRuntime { * **When to use** * * Use to release this runtime's layer resources from an `Effect` workflow. + * + * **Details** + * + * Disposal runs uninterruptibly. Calls made while a disposal is in progress + * wait for it to finish, and interrupting a waiting call only stops the wait. + * Fibers started by this runtime, their child fibers, and the runtime's + * finalizers do not wait: the disposal interrupts or runs them, so waiting + * would deadlock. */ readonly disposeEffect: Effect.Effect } +// The runtimes whose disposal the current fiber is running. A disposal adds its +// runtime to the set it inherits, so a finalizer that disposes another runtime +// still belongs to the outer disposals. Finalizers and their parallel fibers +// inherit the set from the disposing fiber. It is not provided to the layer +// build, because finalizers of layers shared through a MemoMap run with the +// build context of the runtime that built them, which may not be the runtime +// disposing them. +const DisposingRuntimes = Context.Reference>>( + "effect/ManagedRuntime/DisposingRuntimes", + { + defaultValue: () => new Set() + } +) + /** * Creates a `ManagedRuntime` from a layer. * @@ -252,6 +281,11 @@ export interface ManagedRuntime { * Dispose the runtime when it is no longer needed. A runtime cannot be reused * after disposal. * + * A failed layer build is also cached: subsequent runs return the same build + * failure without rebuilding the layer. For transient failures, retry inside + * the layer before passing it to `make`, or dispose the runtime and create a + * new one to attempt a fresh build. + * * **Example** (Creating a managed runtime) * * ```ts import.meta.vitest @@ -298,8 +332,17 @@ export const make = ( const scope = Scope.makeUnsafe("sequential") const layerScope = Scope.forkUnsafe(scope, "sequential") const fiberScope = Scope.forkUnsafe(scope, "parallel") + // Fibers started by the runtime, including the layer build, are interrupted + // and awaited by its disposal, so they and their child fibers must not wait + // for it. + const managedFibers = new WeakSet>() + const disposingFibers = new WeakSet>() const defaultRunOptions: Effect.RunOptions = { - onFiberStart: Fiber.runIn(fiberScope) + onFiberStart(fiber) { + managedFibers.add(fiber) + if (disposingFibers.has(fiber)) return + Fiber.runIn(fiber, fiberScope) + } } const mergeRunOptions = (options?: O): O => options @@ -314,6 +357,7 @@ export const make = ( } : defaultRunOptions as O let buildFiber: Fiber.Fiber, ER> | undefined + let disposed: Deferred.Deferred | undefined const contextEffect = Effect.withFiber, ER>((fiber) => { if (!buildFiber) { buildFiber = Effect.runFork( @@ -324,7 +368,15 @@ export const make = ( self.cachedContext = context }) ), - { ...defaultRunOptions, scheduler: fiber.cache.scheduler } + { + scheduler: fiber.cache.scheduler, + // Unlike requests, the build joins fiberScope even if it disposed the + // runtime, so disposal still interrupts it. + onFiberStart(fiber) { + managedFibers.add(fiber) + Fiber.runIn(fiber, fiberScope) + } + } ) } return Effect.flatten(Fiber.await(buildFiber)) @@ -347,14 +399,28 @@ export const make = ( return self.dispose() }, disposeEffect: Effect.withFiber((fiber) => { - // Closing fiberScope interrupts and awaits every managed fiber. Skip the + if (disposed !== undefined) { + if (fiber.getRef(DisposingRuntimes).has(self)) return Effect.void + for (let current = fiber as FiberImpl | undefined; current; current = current._parent) { + if (managedFibers.has(current)) return Effect.void + } + return Deferred.await(disposed) + } + // Once `disposed` is set the close must run, or later calls would wait forever. + fiberEnterUninterruptibleUnsafe(fiber) + const done = disposed = Deferred.makeUnsafe() + // Closing fiberScope interrupts and awaits every managed fiber. Exempt the // disposing fiber and its ancestors, which would otherwise await themselves. for (let current = fiber as FiberImpl | undefined; current; current = current._parent) { + disposingFibers.add(current) scopeRemoveFinalizerUnsafe(fiberScope, current) } ;(self as Mutable>).contextEffect = Effect.die("ManagedRuntime disposed") self.cachedContext = undefined - return Scope.close(self.scope, Exit.void) + return Scope.close(self.scope, Exit.void).pipe( + Effect.provideService(DisposingRuntimes, new Set(fiber.getRef(DisposingRuntimes)).add(self)), + Effect.ensuring(Deferred.done(done, Exit.void)) + ) }), runFork(effect: Effect.Effect, options?: Effect.RunOptions): Fiber.Fiber { return self.cachedContext === undefined ? diff --git a/repos/effect/packages/effect/src/Match.ts b/packages/effect/src/Match.ts similarity index 100% rename from repos/effect/packages/effect/src/Match.ts rename to packages/effect/src/Match.ts diff --git a/repos/effect/packages/effect/src/Metric.ts b/packages/effect/src/Metric.ts similarity index 93% rename from repos/effect/packages/effect/src/Metric.ts rename to packages/effect/src/Metric.ts index 9b8e31635..f0b05ab3e 100644 --- a/repos/effect/packages/effect/src/Metric.ts +++ b/packages/effect/src/Metric.ts @@ -17,7 +17,7 @@ import * as Context from "./Context.ts" import * as Duration from "./Duration.ts" import type { Effect } from "./Effect.ts" import type { Exit } from "./Exit.ts" -import { constUndefined, dual } from "./Function.ts" +import { constUndefined, constVoid, dual, identity } from "./Function.ts" import * as InternalEffect from "./internal/effect.ts" import * as InternalMetric from "./internal/metric.ts" import * as InternalRecord from "./internal/record.ts" @@ -1678,6 +1678,7 @@ abstract class Metric$ implements Metric { declare readonly State: Covariant #extraAttributes: Metric.AttributeSet | undefined + #mergedAttributes: Metric.AttributeSet | undefined #key = "" readonly id: string @@ -1709,29 +1710,61 @@ abstract class Metric$ implements Metric { abstract createHooks(): Metric.Hooks hook(context: Context.Context): Metric.Hooks { - const extraAttributes = Context.get(context, CurrentMetricAttributes) - if (extraAttributes !== this.#extraAttributes) { - this.#extraAttributes = extraAttributes - this.#key = makeKey(this, mergeAttributes(this.attributes, extraAttributes)) - } + const key = this.key(context) // The registry owns the series, so it is looked up on every update. const registry = Context.get(context, MetricRegistry) - let metadata = registry.get(this.#key) + let metadata = registry.get(key) if (Predicate.isUndefined(metadata)) { - metadata = { - id: this.id, - type: this.type, - description: this.description, - attributes: Object.keys(extraAttributes).length > 0 - ? mergeAttributes(this.attributes, extraAttributes) - : this.attributes, - hooks: this.createHooks() - } - registry.set(this.#key, metadata) + metadata = this.metadata(this.createHooks()) + registry.set(key, metadata) } return metadata.hooks } + removeUnsafe(context: Context.Context): void { + Context.get(context, MetricRegistry).delete(this.key(context)) + } + + registerUnsafe(context: Context.Context, hooks: Metric.Hooks): Registration { + const key = this.key(context) + // Keep the registry from registration time, so removal does not depend on + // the context it is called from. + const registry = Context.get(context, MetricRegistry) + const metadata = this.metadata({ get: hooks.get, update: constVoid, modify: constVoid }) + registry.set(key, metadata) + return { + remove() { + if (registry.get(key) === metadata) { + registry.delete(key) + } + } + } + } + + // Merges the attributes once per contextual attribute set, so series key and + // metadata share the result. + private key(context: Context.Context): string { + const extraAttributes = Context.get(context, CurrentMetricAttributes) + if (extraAttributes !== this.#extraAttributes) { + this.#extraAttributes = extraAttributes + const merged = mergeAttributes(this.attributes, extraAttributes) + this.#mergedAttributes = Object.keys(extraAttributes).length > 0 ? merged : this.attributes + this.#key = makeKey(this, merged) + } + return this.#key + } + + // Must follow `key(context)`, which computes the merged attributes. + private metadata(hooks: Metric.Hooks): Metric.Metadata { + return { + id: this.id, + type: this.type, + description: this.description, + attributes: this.#mergedAttributes, + hooks + } + } + pipe() { return pipeArguments(this, arguments) } @@ -1992,29 +2025,50 @@ class SummaryMetric extends Metric$ extends Metric$ { +// Every operation maps the context first and the input second, so wrappers +// such as `withAttributes` address the same series for update, remove and +// register. +class MetricTransform extends Metric$ { type: Metric.Type readonly metric: Metric - override readonly valueUnsafe: (context: Context.Context) => State - override readonly updateUnsafe: (input: Input2, context: Context.Context) => void - override readonly modifyUnsafe: (input: Input2, context: Context.Context) => void + readonly mapInput: (input: Input2, context: Context.Context) => Input + readonly mapContext: (context: Context.Context) => Context.Context constructor( metric: Metric, - valueUnsafe: (context: Context.Context) => State, - updateUnsafe: (input: Input2, context: Context.Context) => void, - modifyUnsafe: (input: Input2, context: Context.Context) => void + mapInput: (input: Input2, context: Context.Context) => Input, + mapContext: (context: Context.Context) => Context.Context ) { super(metric.id, metric.description, metric.attributes) this.metric = metric - this.valueUnsafe = valueUnsafe - this.updateUnsafe = updateUnsafe - this.modifyUnsafe = modifyUnsafe + this.mapInput = mapInput + this.mapContext = mapContext this.type = metric.type } createHooks(): Metric.Hooks { return (this.metric as any).createHooks() } + override valueUnsafe(context: Context.Context): State { + return this.metric.valueUnsafe(this.mapContext(context)) + } + override updateUnsafe(input: Input2, context: Context.Context): void { + const mapped = this.mapContext(context) + this.metric.updateUnsafe(this.mapInput(input, mapped), mapped) + } + override modifyUnsafe(input: Input2, context: Context.Context): void { + const mapped = this.mapContext(context) + this.metric.modifyUnsafe(this.mapInput(input, mapped), mapped) + } + override removeUnsafe(context: Context.Context): void { + removeUnsafe(this.metric, this.mapContext(context)) + } + override registerUnsafe(context: Context.Context, hooks: Metric.Hooks): Registration { + return registerUnsafe(this.metric, this.mapContext(context), { + get: hooks.get, + update: constVoid, + modify: constVoid + }) + } } /** @@ -2732,6 +2786,148 @@ export const update: { InternalEffect.contextWith((services) => InternalEffect.sync(() => self.updateUnsafe(input, services))) ) +/** + * Handle for a series registered with `Metric.registerUnsafe`. + * + * **Details** + * + * `remove` deletes the series from the registry it was registered into, but + * only while the registry still holds this registration. Calling it again, or + * after another registration has replaced the series, does nothing. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface Registration { + readonly remove: () => void +} + +/** + * Removes the series of a metric for the current attributes from the metric + * registry. + * + * **When to use** + * + * Use when a series describes something that has gone away, such as a + * connection or an entity, so the registry and exporters stop tracking it. + * + * **Details** + * + * The series is identified the same way `update` identifies it: by the metric + * and its attributes merged with `CurrentMetricAttributes`. Other series of the + * same metric are left alone. A later update creates the series again from its + * initial state. Removing a series that does not exist does nothing. + * + * **Example** (Removing a series) + * + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" + * + * const connections = Metric.gauge("connections") + * + * const program = Effect.gen(function*() { + * yield* Metric.update(connections, 3).pipe( + * Effect.provideService(Metric.CurrentMetricAttributes, { pool: "a" }) + * ) + * yield* Metric.update(connections, 5).pipe( + * Effect.provideService(Metric.CurrentMetricAttributes, { pool: "b" }) + * ) + * yield* Metric.remove(connections).pipe( + * Effect.provideService(Metric.CurrentMetricAttributes, { pool: "a" }) + * ) + * const snapshots = yield* Metric.snapshot + * return snapshots.map((snapshot) => snapshot.attributes) + * }) + * + * await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [{ pool: "b" }] + * ``` + * + * @see {@link removeUnsafe} for removing a series with an explicit `Context` + * + * @stability unstable + * @category mutations + * @since 4.0.6 + */ +export const remove = (self: Metric): Effect => + InternalEffect.contextWith((context) => InternalEffect.sync(() => removeUnsafe(self, context))) + +/** + * Removes the series of a metric for the attributes in the provided `Context` + * from the metric registry in that `Context`. + * + * **When to use** + * + * Use when a low-level integration already holds a `Context`. Use `remove` + * in application code. + * + * @see {@link remove} for the `Effect` version + * + * @stability unstable + * @category mutations + * @since 4.0.6 + */ +export const removeUnsafe = (self: Metric, context: Context.Context): void => + (self as Metric$).removeUnsafe(context) + +/** + * Registers a series backed by caller-supplied hooks, for gauges whose value is + * read when metrics are snapshotted rather than pushed with updates. + * + * **When to use** + * + * Use when the value already lives in some data structure, such as a queue + * size, and updating a metric on every change would cost more than reading the + * value at export time. + * + * **Details** + * + * The series is identified as in `remove`, using the attributes and registry + * from `context`, and replaces any series with the same identity. `hooks.get` + * is called each time the series is read, for example by `snapshot` or an + * exporter. The returned {@link Registration} removes it again. + * `update` and `modify` on this series do nothing; the supplied mutation hooks + * are ignored. + * + * **Gotchas** + * + * The registry keeps `hooks.get` reachable until the series is removed, along + * with anything it closes over. Call `remove` once the source goes away. + * + * **Example** (Reporting a queue size on snapshot) + * + * ```ts import.meta.vitest + * import { Context, Metric } from "effect" + * + * const context = Context.make(Metric.MetricRegistry, new Map()) + * const queueSize = Metric.gauge("queue_size") + * const queue: Array = ["a", "b"] + * + * const registration = Metric.registerUnsafe(queueSize, context, { + * get: () => ({ value: queue.length }), + * update: () => {}, + * modify: () => {} + * }) + * queue.push("c") + * const sizes = Metric.snapshotUnsafe(context).map((snapshot) => snapshot.state) + * registration.remove() + * + * sizes // => [{ value: 3 }] + * Metric.snapshotUnsafe(context).length // => 0 + * ``` + * + * @see {@link remove} for removing a series updated with `update` + * + * @stability unstable + * @category mutations + * @since 4.0.6 + */ +export const registerUnsafe = ( + self: Metric, + context: Context.Context, + hooks: Metric.Hooks +): Registration => (self as Metric$).registerUnsafe(context, hooks) + /** * Returns a new metric that is powered by this one, but which accepts updates * of the specified new type, which must be transformable to the input type of @@ -2785,13 +2981,7 @@ export const mapInput: { >(2, ( self: Metric, f: (input: Input2, context: Context.Context) => Input -): Metric => - new MetricTransform( - self, - (context) => self.valueUnsafe(context), - (input, context) => self.updateUnsafe(f(input, context), context), - (input, context) => self.modifyUnsafe(f(input, context), context) - )) +): Metric => new MetricTransform(self, f, identity)) /** * Returns a new metric that is powered by this one, but which accepts updates @@ -2905,13 +3095,7 @@ export const withAttributes: { self: Metric, attributes: Metric.Attributes ): Metric => { - const attributed = addAttributesToContext(attributes) - return new MetricTransform( - self, - (context) => self.valueUnsafe(attributed(context)), - (input, context) => self.updateUnsafe(input, attributed(context)), - (input, context) => self.modifyUnsafe(input, attributed(context)) - ) + return new MetricTransform(self, identity, addAttributesToContext(attributes)) }) // Metric Snapshots diff --git a/repos/effect/packages/effect/src/MutableHashMap.ts b/packages/effect/src/MutableHashMap.ts similarity index 100% rename from repos/effect/packages/effect/src/MutableHashMap.ts rename to packages/effect/src/MutableHashMap.ts diff --git a/repos/effect/packages/effect/src/MutableHashSet.ts b/packages/effect/src/MutableHashSet.ts similarity index 100% rename from repos/effect/packages/effect/src/MutableHashSet.ts rename to packages/effect/src/MutableHashSet.ts diff --git a/repos/effect/packages/effect/src/MutableList.ts b/packages/effect/src/MutableList.ts similarity index 100% rename from repos/effect/packages/effect/src/MutableList.ts rename to packages/effect/src/MutableList.ts diff --git a/repos/effect/packages/effect/src/MutableRef.ts b/packages/effect/src/MutableRef.ts similarity index 100% rename from repos/effect/packages/effect/src/MutableRef.ts rename to packages/effect/src/MutableRef.ts diff --git a/repos/effect/packages/effect/src/Newtype.ts b/packages/effect/src/Newtype.ts similarity index 100% rename from repos/effect/packages/effect/src/Newtype.ts rename to packages/effect/src/Newtype.ts diff --git a/repos/effect/packages/effect/src/NonEmptyIterable.ts b/packages/effect/src/NonEmptyIterable.ts similarity index 100% rename from repos/effect/packages/effect/src/NonEmptyIterable.ts rename to packages/effect/src/NonEmptyIterable.ts diff --git a/repos/effect/packages/effect/src/Number.ts b/packages/effect/src/Number.ts similarity index 99% rename from repos/effect/packages/effect/src/Number.ts rename to packages/effect/src/Number.ts index 03fdfa973..326bf088c 100644 --- a/repos/effect/packages/effect/src/Number.ts +++ b/packages/effect/src/Number.ts @@ -708,8 +708,8 @@ function toScientificInteger(n: number): readonly [coefficient: bigint, exponent * @since 2.0.0 */ export const nextPow2 = (n: number): number => { - const nextPow = Math.ceil(Math.log(n) / Math.log(2)) - return Math.max(Math.pow(2, nextPow), 2) + const nextPow = Math.pow(2, Math.ceil(Math.log2(n))) + return Math.max(nextPow < n ? nextPow * 2 : nextPow, 2) } /** diff --git a/repos/effect/packages/effect/src/Optic.ts b/packages/effect/src/Optic.ts similarity index 100% rename from repos/effect/packages/effect/src/Optic.ts rename to packages/effect/src/Optic.ts diff --git a/repos/effect/packages/effect/src/Option.ts b/packages/effect/src/Option.ts similarity index 100% rename from repos/effect/packages/effect/src/Option.ts rename to packages/effect/src/Option.ts diff --git a/repos/effect/packages/effect/src/Order.ts b/packages/effect/src/Order.ts similarity index 100% rename from repos/effect/packages/effect/src/Order.ts rename to packages/effect/src/Order.ts diff --git a/repos/effect/packages/effect/src/Ordering.ts b/packages/effect/src/Ordering.ts similarity index 100% rename from repos/effect/packages/effect/src/Ordering.ts rename to packages/effect/src/Ordering.ts diff --git a/repos/effect/packages/effect/src/PartitionedSemaphore.ts b/packages/effect/src/PartitionedSemaphore.ts similarity index 100% rename from repos/effect/packages/effect/src/PartitionedSemaphore.ts rename to packages/effect/src/PartitionedSemaphore.ts diff --git a/repos/effect/packages/effect/src/Path.ts b/packages/effect/src/Path.ts similarity index 99% rename from repos/effect/packages/effect/src/Path.ts rename to packages/effect/src/Path.ts index 61438f46a..0512c46fd 100644 --- a/repos/effect/packages/effect/src/Path.ts +++ b/packages/effect/src/Path.ts @@ -430,7 +430,15 @@ function fromFileUrl(url: URL): Effect.Effect { } } } - return Effect.succeed(decodeURIComponent(pathname)) + return Effect.try({ + try: () => decodeURIComponent(pathname), + catch: (cause) => + new BadArgument({ + module: "Path", + method: "fromFileUrl", + cause + }) + }) } const resolve: Path["resolve"] = function resolve() { diff --git a/repos/effect/packages/effect/src/Pipeable.ts b/packages/effect/src/Pipeable.ts similarity index 100% rename from repos/effect/packages/effect/src/Pipeable.ts rename to packages/effect/src/Pipeable.ts diff --git a/repos/effect/packages/effect/src/PlatformError.ts b/packages/effect/src/PlatformError.ts similarity index 100% rename from repos/effect/packages/effect/src/PlatformError.ts rename to packages/effect/src/PlatformError.ts diff --git a/repos/effect/packages/effect/src/Pool.ts b/packages/effect/src/Pool.ts similarity index 100% rename from repos/effect/packages/effect/src/Pool.ts rename to packages/effect/src/Pool.ts diff --git a/repos/effect/packages/effect/src/Predicate.ts b/packages/effect/src/Predicate.ts similarity index 100% rename from repos/effect/packages/effect/src/Predicate.ts rename to packages/effect/src/Predicate.ts diff --git a/repos/effect/packages/effect/src/PrimaryKey.ts b/packages/effect/src/PrimaryKey.ts similarity index 100% rename from repos/effect/packages/effect/src/PrimaryKey.ts rename to packages/effect/src/PrimaryKey.ts diff --git a/repos/effect/packages/effect/src/PubSub.ts b/packages/effect/src/PubSub.ts similarity index 99% rename from repos/effect/packages/effect/src/PubSub.ts rename to packages/effect/src/PubSub.ts index 15ab11e10..30065637c 100644 --- a/repos/effect/packages/effect/src/PubSub.ts +++ b/packages/effect/src/PubSub.ts @@ -1469,16 +1469,16 @@ 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 limit = Count.normalize(max) let replay: Array | undefined = undefined - if (self.replayWindow.remaining >= max) { - return Effect.succeed(self.replayWindow.takeN(max)) + if (self.replayWindow.remaining >= limit) { + return Effect.succeed(self.replayWindow.takeN(limit)) } else if (self.replayWindow.remaining > 0) { replay = self.replayWindow.takeAll() - max = max - replay.length + limit = limit - replay.length } const as = self.pollers.length === 0 - ? self.subscription.pollUpTo(max) + ? self.subscription.pollUpTo(limit) : [] self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers) return replay ? Effect.succeed(replay.concat(as)) : Effect.succeed(as) @@ -1537,7 +1537,9 @@ const takeRemainderLoop = ( return Effect.succeed(acc) } return Effect.flatMap(takeUpTo(self, max), (bs) => { - acc.push(...bs) + for (let i = 0; i < bs.length; i++) { + acc.push(bs[i]) + } const remaining = min - bs.length if (remaining === 1) { return Effect.map(take(self), (b) => { @@ -2935,7 +2937,10 @@ class ReplayBuffer { } this.tail = this.tail.next if (this.size === this.capacity) { - this.head = this.head.next! + const dropped = this.head + this.head = dropped.next! + dropped.next = null + dropped.value = AbsentValue } else { this.size += 1 } diff --git a/repos/effect/packages/effect/src/Pull.ts b/packages/effect/src/Pull.ts similarity index 100% rename from repos/effect/packages/effect/src/Pull.ts rename to packages/effect/src/Pull.ts diff --git a/repos/effect/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts similarity index 99% rename from repos/effect/packages/effect/src/Queue.ts rename to packages/effect/src/Queue.ts index 6ea790b82..950b73d6a 100644 --- a/repos/effect/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -695,7 +695,10 @@ export const offer: { ) /** - * Adds a message to the queue synchronously. Returns `false` if the queue is done. + * Adds a message to the queue synchronously. Returns `false` without adding the + * message if the queue is closing or done, or if it is full with a non-sliding + * strategy (`"suspend"` or `"dropping"`). A queue with non-positive capacity can + * still accept a message when a taker is waiting. * * **When to use** * diff --git a/repos/effect/packages/effect/src/Random.ts b/packages/effect/src/Random.ts similarity index 99% rename from repos/effect/packages/effect/src/Random.ts rename to packages/effect/src/Random.ts index e2fc7b61a..75c0a38e0 100644 --- a/repos/effect/packages/effect/src/Random.ts +++ b/packages/effect/src/Random.ts @@ -318,7 +318,7 @@ export const withSeed: { } = dual(2, ( self: Effect.Effect, seed: string | number -) => Effect.provideService(self, Random, ISAAC_CSPRNG(seed))) +) => Effect.suspend(() => Effect.provideService(self, Random, ISAAC_CSPRNG(seed)))) /*/////////////////////////////////////////////////////////////////////////////////////////////////// This is a derivative work copyright (c) 2025 Effectful Technologies Inc, under MIT license. diff --git a/repos/effect/packages/effect/src/RcMap.ts b/packages/effect/src/RcMap.ts similarity index 97% rename from repos/effect/packages/effect/src/RcMap.ts rename to packages/effect/src/RcMap.ts index d3c5b6cb7..397481d77 100644 --- a/repos/effect/packages/effect/src/RcMap.ts +++ b/packages/effect/src/RcMap.ts @@ -4,10 +4,12 @@ * An `RcMap` runs a lookup effect the first time a key is requested, shares the * in-progress or acquired resource with other callers for the same key, and * tracks each caller through its current `Scope`. When the last scope for a key - * closes, the resource can be released, kept alive for an idle time, or removed - * by capacity limits or explicit invalidation. It is meant for resource - * lifecycles such as clients, sessions, and connections, not as a general - * mutable cache. + * closes, the resource is released immediately or kept alive until its idle + * time-to-live expires. Entries can also be explicitly invalidated. Capacity + * limits do not evict entries: idle entries count toward capacity, and a new + * key fails with `ExceededCapacityError` when the map is full. It is meant for + * resource lifecycles such as clients, sessions, and connections, not as a + * general mutable cache. * * @stability stable * @since 3.5.0 @@ -204,7 +206,8 @@ const makeUnsafe = (options: { * * Complex keys can extend `Equal` and `Hash` to allow lookups by value. * - * - `capacity`: The maximum number of resources that can be held in the map. + * - `capacity`: The maximum number of entries, including idle entries. A new key + * fails with `ExceededCapacityError` when full; existing entries are not evicted. * - `idleTimeToLive`: When the reference count reaches zero, the resource will be released after this duration. * * **Example** (Creating a reference-counted map) @@ -285,7 +288,8 @@ export const make: { Effect.sync(() => { MutableHashMap.clear(map) }) - ) + ), + Effect.flatMap(Exit.asVoidAll) ) }), self diff --git a/repos/effect/packages/effect/src/RcRef.ts b/packages/effect/src/RcRef.ts similarity index 100% rename from repos/effect/packages/effect/src/RcRef.ts rename to packages/effect/src/RcRef.ts diff --git a/repos/effect/packages/effect/src/Record.ts b/packages/effect/src/Record.ts similarity index 99% rename from repos/effect/packages/effect/src/Record.ts rename to packages/effect/src/Record.ts index 74c9af49f..9865b45f6 100644 --- a/repos/effect/packages/effect/src/Record.ts +++ b/packages/effect/src/Record.ts @@ -1181,6 +1181,11 @@ export const isSubrecord: { /** * Reduces a record to a single value by combining its entries with a specified function. * + * **Details** + * + * The reducer function receives the accumulator, the current value, and the + * key of the current entry. + * * **Example** (Reducing record values) * * ```ts import.meta.vitest @@ -1189,6 +1194,18 @@ export const isSubrecord: { * Record.reduce({ a: 1, b: 2, c: 3 }, 0, (acc, value) => acc + value) // => 6 * ``` * + * **Example** (Using the key in the reducer) + * + * ```ts import.meta.vitest + * import { Record } from "effect" + * + * Record.reduce( + * { a: 1, b: 2, c: 3 }, + * "", + * (acc, value, key) => (acc === "" ? `${key}${value}` : `${acc}-${key}${value}`) + * ) // => "a1-b2-c3" + * ``` + * * @stability stable * @category folding * @since 2.0.0 diff --git a/repos/effect/packages/effect/src/Redactable.ts b/packages/effect/src/Redactable.ts similarity index 100% rename from repos/effect/packages/effect/src/Redactable.ts rename to packages/effect/src/Redactable.ts diff --git a/repos/effect/packages/effect/src/Redacted.ts b/packages/effect/src/Redacted.ts similarity index 100% rename from repos/effect/packages/effect/src/Redacted.ts rename to packages/effect/src/Redacted.ts diff --git a/repos/effect/packages/effect/src/Reducer.ts b/packages/effect/src/Reducer.ts similarity index 100% rename from repos/effect/packages/effect/src/Reducer.ts rename to packages/effect/src/Reducer.ts diff --git a/repos/effect/packages/effect/src/Ref.ts b/packages/effect/src/Ref.ts similarity index 100% rename from repos/effect/packages/effect/src/Ref.ts rename to packages/effect/src/Ref.ts diff --git a/repos/effect/packages/effect/src/References.ts b/packages/effect/src/References.ts similarity index 98% rename from repos/effect/packages/effect/src/References.ts rename to packages/effect/src/References.ts index 95b479181..1b882d53c 100644 --- a/repos/effect/packages/effect/src/References.ts +++ b/packages/effect/src/References.ts @@ -563,12 +563,13 @@ export const TracerSpanLinks: Context.Reference> = refer export const TracerTimingEnabled: Context.Reference = references.TracerTimingEnabled /** - * Context reference for the log severity used when a pool finalizer reports an - * unhandled error. + * Context reference for the log severity used to report errors that have no + * caller to receive them, such as pool finalizer failures or defects in + * background callbacks. * * **When to use** * - * Use to choose whether and at which severity pool finalizer failures are + * Use to choose whether and at which severity these unhandled errors are * reported. * * **Details** @@ -577,7 +578,7 @@ export const TracerTimingEnabled: Context.Reference = references.Tracer * * **Gotchas** * - * Providing `undefined` suppresses this report; it does not fall back to + * Providing `undefined` suppresses these reports; it does not fall back to * `CurrentLogLevel`. * * @see {@link CurrentLogLevel} for the default severity used by ordinary `Effect.log` calls diff --git a/repos/effect/packages/effect/src/RegExp.ts b/packages/effect/src/RegExp.ts similarity index 100% rename from repos/effect/packages/effect/src/RegExp.ts rename to packages/effect/src/RegExp.ts diff --git a/repos/effect/packages/effect/src/Request.ts b/packages/effect/src/Request.ts similarity index 100% rename from repos/effect/packages/effect/src/Request.ts rename to packages/effect/src/Request.ts diff --git a/repos/effect/packages/effect/src/RequestResolver.ts b/packages/effect/src/RequestResolver.ts similarity index 95% rename from repos/effect/packages/effect/src/RequestResolver.ts rename to packages/effect/src/RequestResolver.ts index 32d6d22ad..dd78faa10 100644 --- a/repos/effect/packages/effect/src/RequestResolver.ts +++ b/packages/effect/src/RequestResolver.ts @@ -172,7 +172,9 @@ export const isRequestResolver = (u: unknown): u is RequestResolver => hasP * * `batchKey` groups request entries, `delay` schedules batch execution, * `collectWhile` can end collection early, and `runAll` receives a non-empty - * batch for one key. + * batch for one key. The batch runs with the context of the caller that created + * it, even when a later caller ends collection. Each entry retains its own + * caller context in `entry.context`. * * **Gotchas** * @@ -209,6 +211,17 @@ const defaultKey = (_request: unknown): unknown => defaultKeyObject /** * Constructs a request resolver with the specified method to run requests. * + * **Details** + * + * `runAll` runs with the context of the caller that created the batch, including + * when `collectWhile` ends collection early (for example with `batchN`). + * + * **Gotchas** + * + * Different caller contexts do not split batches. Use `entry.context` for + * per-entry services, or `makeGrouped` to separate requests by the services + * that affect the backend operation, such as tenant or authorization settings. + * * **Example** (Creating a request resolver) * * ```ts import.meta.vitest @@ -256,6 +269,14 @@ export const make = ( * **Details** * * The key can use the Equal trait to determine if two keys are equal. + * Each batch runs with the context of the caller that created that batch, + * whether collection ends after the delay or at a `collectWhile` cutoff. + * + * **Gotchas** + * + * Caller services that are not included in the key do not split batches. Include + * tenant or authorization settings in the key when they affect the backend + * operation, or read each caller's services from `entry.context`. * * **Example** (Grouping requests by key) * @@ -411,6 +432,15 @@ export const fromFunctionBatched = ( /** * Constructs a request resolver from an effectual function. * + * **Details** + * + * Each entry's effect runs with its own caller context. Services provided around + * `runAll` are not visible to it; provide them inside `f` instead. Entries with + * different caller contexts can still share a batch. + * + * With `withSpan`, spans created inside `f` are children of the caller's span, + * not the resolver span. The resolver span still links to each caller's span. + * * **Example** (Creating a resolver from an effectful function) * * ```ts import.meta.vitest @@ -451,7 +481,11 @@ export const fromEffect = ( let done = 0 for (let i = 0; i < entries.length; i++) { const entry = entries[i] - const fiber = effect.forkUnsafe(parent as any, f(entry), true) + const fiber = effect.forkUnsafe( + parent as any, + effect.setContext(f(entry), entry.context), + true + ) fiber.addObserver((exit) => { entry.completeUnsafe(exit) done++ @@ -1020,7 +1054,7 @@ export const withSpan: { links.push({ span: span.value, attributes: {} }) } return Effect.withSpan(self.runAll(entries, key), name, { - ...options, + ...opts, links, attributes: { batchSize: entries.length, @@ -1183,6 +1217,9 @@ export const withCache: { preCheck(entry) { const ocached = MutableHashMap.get(cache, entry.request) if (ocached._tag === "None") { + if (self.preCheck !== undefined && !self.preCheck(entry)) { + return false + } const cached: CacheEntry = { entry, exit: undefined, pending: [] } MutableHashMap.set(cache, entry.request, cached) const prevComplete = entry.completeUnsafe @@ -1304,6 +1341,8 @@ export const persisted: { exit === undefined || (options.staleWhileRevalidate && options.staleWhileRevalidate(exit as any, entry.request)) ) { + // Deliver stale results before tracking refresh completions for persistence. + if (exit !== undefined) entry.completeUnsafe(exit as any) const prevComplete = entry.completeUnsafe entry.completeUnsafe = function(exit) { completed.add(entry) @@ -1311,7 +1350,7 @@ export const persisted: { prevComplete(exit) } leftover.push(entry) - if (exit === undefined) continue + continue } entry.completeUnsafe(exit as any) } diff --git a/repos/effect/packages/effect/src/Resource.ts b/packages/effect/src/Resource.ts similarity index 98% rename from repos/effect/packages/effect/src/Resource.ts rename to packages/effect/src/Resource.ts index ef03a2eb9..b8e7c1420 100644 --- a/repos/effect/packages/effect/src/Resource.ts +++ b/packages/effect/src/Resource.ts @@ -136,7 +136,7 @@ export const auto = ( ): Effect.Effect, never, R | R2 | Scope.Scope> => Effect.tap( manual(acquire), - (self) => Effect.forkScoped(Effect.repeat(refresh(self), policy)) + (self) => Effect.forkScoped(Effect.schedule(Effect.ignoreCause(refresh(self)), policy)) ) /** diff --git a/repos/effect/packages/effect/src/Result.ts b/packages/effect/src/Result.ts similarity index 100% rename from repos/effect/packages/effect/src/Result.ts rename to packages/effect/src/Result.ts diff --git a/repos/effect/packages/effect/src/Runtime.ts b/packages/effect/src/Runtime.ts similarity index 93% rename from repos/effect/packages/effect/src/Runtime.ts rename to packages/effect/src/Runtime.ts index e9769d40c..996908ad1 100644 --- a/repos/effect/packages/effect/src/Runtime.ts +++ b/packages/effect/src/Runtime.ts @@ -126,9 +126,9 @@ export const defaultTeardown: Teardown = ( * * **Details** * - * The runner executes Effect programs as main entry points. The provided - * function receives a forked fiber and a teardown callback so it can install - * platform-specific signal handling, fiber observers, and final exit behavior. + * The setup function runs before user code and receives the main fiber and a + * teardown callback to install platform-specific signal handlers, observers, + * and exit behavior. * * Most applications should use a platform-provided runner, such as * `NodeRuntime.runMain`, rather than constructing one directly. @@ -208,23 +208,24 @@ export const makeRunMain = ( readonly disableErrorReporting?: boolean | undefined readonly teardown?: Teardown | undefined }) => { - const fiber = options?.disableErrorReporting === true - ? Effect.runFork(effect) - : Effect.runFork( - Effect.tapCause(effect, (cause) => { - if (Cause.hasInterruptsOnly(cause)) return Effect.void - const isReported = getErrorReported(Cause.squash(cause)) - return isReported ? Effect.logError(cause) : Effect.void - }) - ) - try { - const keepAlive = globalThis.setInterval(constVoid, 2_147_483_647) - fiber.addObserver(() => { - clearInterval(keepAlive) + const main = options?.disableErrorReporting === true + ? effect + : Effect.tapCause(effect, (cause) => { + if (Cause.hasInterruptsOnly(cause)) return Effect.void + const isReported = getErrorReported(Cause.squash(cause)) + return isReported ? Effect.logError(cause) : Effect.void }) - } catch {} const teardown = options?.teardown ?? defaultTeardown - return f({ fiber, teardown }) + Effect.runFork(Effect.withFiber((fiber) => { + try { + const keepAlive = globalThis.setInterval(constVoid, 2_147_483_647) + fiber.addObserver(() => { + clearInterval(keepAlive) + }) + } catch {} + f({ fiber, teardown }) + return main + })) }) declare global { diff --git a/repos/effect/packages/effect/src/Schedule.ts b/packages/effect/src/Schedule.ts similarity index 100% rename from repos/effect/packages/effect/src/Schedule.ts rename to packages/effect/src/Schedule.ts diff --git a/repos/effect/packages/effect/src/Scheduler.ts b/packages/effect/src/Scheduler.ts similarity index 100% rename from repos/effect/packages/effect/src/Scheduler.ts rename to packages/effect/src/Scheduler.ts diff --git a/repos/effect/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts similarity index 96% rename from repos/effect/packages/effect/src/Schema.ts rename to packages/effect/src/Schema.ts index bfef2b661..fe2850894 100644 --- a/repos/effect/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -79,6 +79,8 @@ import * as SchemaIssue from "./SchemaIssue.ts" import * as SchemaParser from "./SchemaParser.ts" import type * as SchemaRepresentation from "./SchemaRepresentation.ts" import * as SchemaTransformation from "./SchemaTransformation.ts" +import * as SemVer_ from "./SemVer.ts" +import * as SemVerRange_ from "./SemVerRange.ts" import type { StandardJSONSchemaV1, StandardSchemaV1 } from "./StandardSchema.ts" import type { Assign, Lambda, Mutable, Simplify } from "./Struct.ts" import * as Struct_ from "./Struct.ts" @@ -123,8 +125,8 @@ export type ConstructorDefault = "no-default" | "with-default" * * **When to use** * - * Use when passing `disableChecks: true` to skip validation when you trust the - * data. Pass `parseOptions` to control parsing behavior. + * Use when passing `disableChecks: true` to skip filters and checks while still + * parsing types and structure. Pass `parseOptions` to control parsing behavior. * * @see {@link BottomWithoutNew.makeEffect} * @see {@link BottomWithoutNew.make} @@ -139,7 +141,8 @@ export interface MakeOptions { */ readonly parseOptions?: SchemaAST.ParseOptions | undefined /** - * Whether to disable validation for the schema. + * Whether to skip filters and checks. Types and structure are still parsed, + * and constructor defaults still apply. */ readonly disableChecks?: boolean | undefined @@ -2961,8 +2964,8 @@ export interface Enum readonly enums: A } /** - * Creates a schema from a TypeScript enum object. Validates that the input is one of the enum's values. - * Numeric enum values must be finite. + * Creates a schema from a TypeScript enum or an object mapping names to string or number values. + * Validates that the input is one of those values. Numeric values must be finite. * * **Gotchas** * @@ -2986,7 +2989,7 @@ export interface Enum * @category constructors * @since 4.0.0 */ -export function Enum(enums: A): Enum { +export function Enum(enums: A): Enum { return make( new SchemaAST.Enum( Object.keys(enums).filter( @@ -5632,8 +5635,8 @@ export interface compose extends * **Gotchas** * * In a custom transformation, `decode` maps `From["Type"]` to `To["Encoded"]` - * and is used on the encoding path, while `encode` maps `To["Encoded"]` to - * `From["Type"]` and is used on the decoding path. + * and is used on the decoding path, while `encode` maps `To["Encoded"]` to + * `From["Type"]` and is used on the encoding path. * * **Example** (Transforming strings to numbers with a schema transformation) * @@ -6316,12 +6319,6 @@ type Flatten = Schemas extends readonly [infer Head, ...infer Tail] ? Head extends Union ? [...Flatten, ...Flatten] : [Head, ...Flatten] : [] -type DiscriminantKey = T extends number ? `${T}` : T -type EnsureUniqueDiscriminants, Seen = never> = Tags extends - readonly [infer Head extends PropertyKey, ...infer Tail extends ReadonlyArray] - ? [DiscriminantKey & Seen] extends [never] ? EnsureUniqueDiscriminants> - : never - : unknown type MatchCasesResult = { [K in keyof Cases]-?: NonNullable extends (...args: Array) => infer R ? R : never }[keyof Cases] @@ -6406,10 +6403,9 @@ export type toTaggedUnion< * * **Gotchas** * - * Rejects overlapping discriminant types in known member tuples at compile - * time, including nested unions. Numeric and string values such as `1` and - * `"1"` count as duplicates. Also throws at runtime if multiple members use the - * same discriminant property key. + * Throws when multiple members use the same discriminant property key, + * including members of nested unions. Numeric and string values such as `1` + * and `"1"` count as duplicates. * * **Example** (Adding tagged-union utilities to an existing union) * @@ -6436,7 +6432,7 @@ export type toTaggedUnion< */ export function toTaggedUnion(tag: Tag) { return >( - self: Union & NoInfer["discriminants"]>> + self: Union ): toTaggedUnion => { const cases: Record = {} const discriminants: Array = [] @@ -7137,16 +7133,21 @@ export function isULID(annotations?: Annotations.Filter) { * @since 4.0.0 */ export function isBase64(annotations?: Annotations.Filter) { - const regExp = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/ - return isPattern( - regExp, + const pattern = "^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$" + // `pattern` overflows the V8 stack on large inputs + const regExp = /^[0-9a-zA-Z+/]*={0,2}$/ + return makeFilter( + (s: string) => s.length % 4 === 0 && regExp.test(s), { expected: "a base64 encoded string", representation: { id: "effect/schema/isBase64", payload: null }, - toJsonSchema: () => ({ pattern: regExp.source }), + toJsonSchema: () => ({ pattern }), + arbitraryConstraint: { + patterns: [{ source: pattern, flags: "" }] + }, toCode: () => ({ runtime: "Schema.isBase64()" }), ...annotations } @@ -7173,16 +7174,21 @@ export function isBase64(annotations?: Annotations.Filter) { * @since 4.0.0 */ export function isBase64Url(annotations?: Annotations.Filter) { - const regExp = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/ - return isPattern( - regExp, + const pattern = "^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$" + // `pattern` overflows the V8 stack on large inputs + const regExp = /^[0-9a-zA-Z-_]*={0,2}$/ + return makeFilter( + (s: string) => regExp.test(s) && (s.endsWith("=") ? s.length % 4 === 0 : s.length % 4 !== 1), { expected: "a base64url encoded string", representation: { id: "effect/schema/isBase64Url", payload: null }, - toJsonSchema: () => ({ pattern: regExp.source }), + toJsonSchema: () => ({ pattern }), + arbitraryConstraint: { + patterns: [{ source: pattern, flags: "" }] + }, toCode: () => ({ runtime: "Schema.isBase64Url()" }), ...annotations } @@ -9939,7 +9945,9 @@ export interface FormData extends instanceOf { * @category schemas * @since 4.0.0 */ -export const FormData: FormData = instanceOf(globalThis.FormData, { +// Read the global inside the guard: resolving globalThis.FormData at module +// scope loads undici on Node. +export const FormData: FormData = declare((u): u is globalThis.FormData => u instanceof globalThis.FormData, { representation: { id: "effect/schema/FormData", payload: null @@ -11277,7 +11285,7 @@ export function Cause(error: E, defe } return Effect.mapBothEager(SchemaParser.decodeUnknownEffect(failures)(input.reasons, options), { onSuccess: Cause_.fromReasons, - onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "failures", issue, input, options) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "reasons", issue, input, options) }) } }, @@ -11680,6 +11688,65 @@ export const DateTimeUtcFromString: DateTimeUtcFromString = String.annotate({ dateTimeUtcFromString ) ) +const dateTimeUtcFromHttpDate: SchemaTransformation.Transformation = SchemaTransformation + .transformEffect({ + decode: (s, options) => + Result_.match(Headers_.parseHttpDate(s), { + onFailure: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "an HTTP date (RFC 9110 §5.6.7)" }, + s, + options + ) + ), + onSuccess: Effect.succeed + }), + encode: (utc) => Effect.succeed(Headers_.formatHttpDate(utc)) + }) +/** + * Type-level representation of {@link DateTimeUtcFromHttpDate}. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface DateTimeUtcFromHttpDate extends decodeTo { + readonly "Rebuild": DateTimeUtcFromHttpDate +} +/** + * Schema for HTTP date header values, such as `Date`, `Last-Modified` and + * `Expires`, decoded as `DateTime.Utc`. + * + * **When to use** + * + * Use with HTTP API header schemas that carry dates. Decoding accepts the + * three formats of RFC 9110 §5.6.7 and reads them as UTC; encoding emits the + * preferred IMF-fixdate form (`Sun, 06 Nov 1994 08:49:37 GMT`). + * + * **Example** (Decoding an HTTP date) + * + * ```ts + * import { DateTime, Schema } from "effect" + * + * const date = Schema.decodeSync(Schema.DateTimeUtcFromHttpDate)("Sun, 06 Nov 1994 08:49:37 GMT") + * const millis = DateTime.toEpochMillis(date) // => 784111777000 + * ``` + * + * @see {@link DateTimeUtcFromString} for ISO 8601 strings + * + * @stability unstable + * @category schemas + * @since 4.0.0 + */ +export const DateTimeUtcFromHttpDate: DateTimeUtcFromHttpDate = String.annotate({ + expected: "an HTTP date string" +}).pipe( + decodeTo( + DateTimeUtc, + dateTimeUtcFromHttpDate + ) +) /** * Type-level representation of {@link DateTimeUtcFromMillis}. * @@ -15190,6 +15257,110 @@ export const Headers: Headers = declare( } ) +/** + * Type-level representation of {@link RetryAfter}. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface RetryAfter extends + Union< + readonly [ + Struct<{ readonly _tag: tag<"Duration">; readonly duration: Duration }>, + Struct<{ readonly _tag: tag<"DateTime">; readonly dateTime: DateTimeUtc }> + ] + > +{ + readonly "Rebuild": RetryAfter +} + +/** + * Schema for parsed `Retry-After` header values. + * + * **Details** + * + * Matches `Headers.RetryAfter`: either `{ _tag: "Duration", duration }` or + * `{ _tag: "DateTime", dateTime }`. The duration must be finite and + * non-negative. Use {@link RetryAfterFromString} for the header string form. + * + * @stability unstable + * @category schemas + * @since 4.0.0 + */ +export const RetryAfter: RetryAfter = Union([ + Struct({ + _tag: tag("Duration"), + duration: Duration.check( + makeFilter((d) => Duration_.isFinite(d) && !Duration_.isNegative(d), { + expected: "a finite, non-negative duration" + }) + ) + }), + Struct({ _tag: tag("DateTime"), dateTime: DateTimeUtc }) +]) + +const retryAfterFromString: SchemaTransformation.Transformation = SchemaTransformation + .transformEffect({ + decode: (s, options) => + Result_.match(Headers_.parseRetryAfter(s), { + onFailure: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "whole non-negative seconds or an HTTP date (RFC 9110 §10.2.3)" }, + s, + options + ) + ), + onSuccess: Effect.succeed + }), + encode: (self) => Effect.succeed(Headers_.formatRetryAfter(self)) + }) + +/** + * Type-level representation of {@link RetryAfterFromString}. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface RetryAfterFromString extends decodeTo { + readonly "Rebuild": RetryAfterFromString +} + +/** + * Schema for `Retry-After` header values, decoded with + * `Headers.parseRetryAfter`. + * + * **When to use** + * + * Use with HTTP API header schemas that carry `Retry-After`. Decoding accepts + * whole non-negative seconds or an HTTP date and rejects anything else; + * encoding emits the delay as whole seconds, rounding a fractional second up, + * or the date as an IMF-fixdate. + * + * **Example** (Decoding a Retry-After header) + * + * ```ts + * import { Schema } from "effect" + * + * const value = Schema.decodeSync(Schema.RetryAfterFromString)("120") + * // => { _tag: "Duration", duration: Duration.seconds(120) } + * ``` + * + * @stability unstable + * @category schemas + * @since 4.0.0 + */ +export const RetryAfterFromString: RetryAfterFromString = String.annotate({ + expected: "a Retry-After header value" +}).pipe( + decodeTo( + RetryAfter, + retryAfterFromString + ) +) + /** * Type-level representation of {@link UrlParams}. * @@ -15743,6 +15914,198 @@ export function Result( return make(schema.ast, { success, failure }) } +// ----------------------------------------------------------------------------- +// SemVer schemas +// ----------------------------------------------------------------------------- + +const semVerStringTransformation = ( + parse: (input: string) => Result_.Result, + format: (value: A) => string +): SchemaTransformation.Transformation => + SchemaTransformation.transformEffect({ + decode: (input, options) => + Effect.mapErrorEager( + Effect.fromResult(parse(input)), + (error) => new SchemaIssue.InvalidValue({ message: error.message }, input, options) + ), + encode: (value) => Effect.succeed(format(value)) + }) +// Alphanumeric identifiers contain at least one non-digit. +const SemVerCoreArbitrary = Struct({ + major: Natural, + minor: Natural, + patch: Natural, + prerelease: ArraySchema(Union([Natural, String.check(isPattern(/^[0-9]*[A-Za-z-][0-9A-Za-z-]*$/))])) +}) +const semVerFromString = semVerStringTransformation(SemVer_.parse, SemVer_.format) +const SemVerString = String.annotate({ expected: "a string that will be decoded as a SemVer" }) +/** + * Type-level representation of {@link SemVer}. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface SemVer extends declare { + readonly "Rebuild": SemVer +} +/** + * Schema for `SemVer` values. + * + * **Details** + * + * The JSON and StringTree codecs encode a version as its canonical string, + * including build metadata. + * + * @see {@link SemVerFromString} for parsing version strings + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const SemVer: SemVer = declare( + SemVer_.isSemVer, + { + representation: { + id: "effect/schema/SemVer", + payload: null + }, + toCode: () => ({ + runtime: `Schema.SemVer`, + Type: `SemVer.SemVer`, + importDeclarations: [`import * as SemVer from "effect/SemVer"`] + }), + expected: "SemVer", + toCodecArbitrary: () => + linkDecoding()( + Struct({ + ...SemVerCoreArbitrary.fields, + build: ArraySchema(String.check(isPattern(/^[0-9A-Za-z-]+$/))) + }), + SchemaGetter.transform(SemVer_.make) + ), + toCodecJson: () => + link()( + SemVerString, + semVerFromString + ), + toFormatter: () => SemVer_.format + } +) +/** + * Type-level representation of {@link SemVerFromString}. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface SemVerFromString extends decodeTo { + readonly "Rebuild": SemVerFromString +} +/** + * Schema that parses a Semantic Versioning 2.0.0 string into a `SemVer`. + * + * **Details** + * + * Decodes with `SemVer.parse`, rejecting a `v` prefix, surrounding whitespace, + * partial versions, and leading zeros. Encodes with `SemVer.format`, including + * build metadata. + * + * @see {@link SemVer} for validating values that are already SemVer values + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const SemVerFromString: SemVerFromString = SemVerString.pipe( + decodeTo(SemVer, semVerFromString) +) +const semVerRangeFromString = semVerStringTransformation(SemVerRange_.parse, SemVerRange_.format) +const SemVerRangeString = String.annotate({ expected: "a string that will be decoded as a SemVer range" }) +/** + * Type-level representation of {@link SemVerRange}. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface SemVerRange extends declare { + readonly "Rebuild": SemVerRange +} +/** + * Schema for `SemVerRange` values. + * + * **Details** + * + * The JSON and StringTree codecs encode a range with `SemVerRange.format`. + * + * @see {@link SemVerRangeFromString} for parsing range strings + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const SemVerRange: SemVerRange = declare( + SemVerRange_.isSemVerRange, + { + representation: { + id: "effect/schema/SemVerRange", + payload: null + }, + toCode: () => ({ + runtime: `Schema.SemVerRange`, + Type: `SemVerRange.SemVerRange`, + importDeclarations: [`import * as SemVerRange from "effect/SemVerRange"`] + }), + expected: "SemVerRange", + toCodecArbitrary: () => + linkDecoding()( + ArraySchema( + ArraySchema(Struct({ + operator: Literals(["", "=", "<", "<=", ">", ">=", "~", "^"]), + version: SemVerCoreArbitrary + })).check(isMinLength(1)) + ), + SchemaGetter.transform((sets) => + SemVerRange_.parseUnsafe( + sets.map((set) => + set.map(({ operator, version }) => `${operator}${SemVer_.format(SemVer_.make(version))}`).join(" ") + ).join(" || ") + ) + ) + ), + toCodecJson: () => + link()( + SemVerRangeString, + semVerRangeFromString + ), + toFormatter: () => SemVerRange_.format + } +) +/** + * Type-level representation of {@link SemVerRangeFromString}. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface SemVerRangeFromString extends decodeTo { + readonly "Rebuild": SemVerRangeFromString +} +/** + * Schema that parses an npm-style version range string into a `SemVerRange`. + * + * **Details** + * + * Decodes with `SemVerRange.parse` and encodes with `SemVerRange.format`, + * preserving parsed operators and normalizing wildcard spellings. + * + * @see {@link SemVerRange} for validating values that are already ranges + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const SemVerRangeFromString: SemVerRangeFromString = SemVerRangeString.pipe( + decodeTo(SemVerRange, semVerRangeFromString) +) + /** * Type-level representation returned by {@link Class}. * @@ -15958,16 +16321,19 @@ function makeClass< return out } +/** + * Constructs a class instance from fields that were already validated against + * the class struct, without running the constructor's schema pass. + * + * @internal + */ +export function makeClassTrusted(self: new(input: any, options?: MakeOptions) => Self, fields: object): Self { + return new self(fields, { "~payload": { token: payloadToken, value: fields } }) +} + function getClassTransformation(self: new(...args: ReadonlyArray) => any) { return new SchemaTransformation.Transformation( - SchemaGetter.transform((input) => - new self(input, { - "~payload": { - token: payloadToken, - value: input - } - }) - ), + SchemaGetter.transform((input) => makeClassTrusted(self, input)), SchemaGetter.passthrough() ) } @@ -16051,7 +16417,9 @@ function isStruct(schema: Struct.Fields | Struct): schema is Stru * * **Gotchas** * - * Passing `disableChecks` in the options skips constructor validation. + * Passing `disableChecks: true` skips filters and checks, not parsing of types + * and structure. Wrong types and shapes are still rejected, and constructor + * defaults still apply. * * **Example** (Defining a basic class) * @@ -16769,6 +17137,108 @@ export interface toCodecArrayFromSingle extends export const toCodecArrayFromSingle: { (schema: S): toCodecArrayFromSingle } = InternalToCodec.toCodecArrayFromSingle +/** + * Type-level representation returned by {@link toCodecGoJson}. + * + * @stability unstable + * @category converting + * @since 4.0.6 + */ +export interface toCodecGoJson extends + BottomLazy< + S["ast"], + toCodecGoJson, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": Json + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} +/** + * Derives a JSON codec, like {@link toCodecJson}, that also accepts the + * `null` values Go's `encoding/json` writes for nil pointers, slices, and maps. + * + * **When to use** + * + * Use when decoding JSON produced by a Go service (for example the Docker + * Engine or Kubernetes APIs), where any field the API document describes as + * optional, or any slice or map, may arrive as `null`. + * + * **Details** + * + * Decoding applies these rules at every level of the schema: + * + * - An optional key that is `null` decodes as a missing key. + * - A required array (`Schema.Array`) that is `null` decodes as `[]`. + * - A required record (`Schema.Record`) that is `null` decodes as `{}`. + * + * Array and record rules also apply at the top level and to nested collection + * values. Tuples, non-empty arrays, and structs are not treated as collections; + * collection checks still apply to the empty value. + * + * Encoding is unchanged from {@link toCodecJson}: the added rules only decode + * `null` and do not emit it. A missing key stays missing. + * + * These rules also accept `encoding/json/v2` output. By default, v2 writes + * nil slices and maps as `[]` and `{}`, but it can be configured to emit + * `null` instead. + * + * **Gotchas** + * + * - Normalization deliberately discards the distinction between nil and empty + * Go slices or maps. They are distinguishable in Go, and unmarshalling a + * missing key, `null`, or an empty collection can affect existing values + * differently. Use {@link toCodecJson} with explicit nullable schemas when + * that distinction matters. + * - A key whose schema already accepts `null` (for example + * `Schema.NullOr(X)`, `Schema.Unknown`, or `Schema.Json`) keeps `null` as a + * value. + * - A required field that is not an array or a record (a nil pointer to a Go + * struct, for example) still fails on `null`. Make it optional if the API + * can omit it. + * - A nil `[]byte` is written as `null` by `encoding/json` (and as `""` by + * version 2), but its schema is a base64 string, not an array. A required + * `[]byte` field that is `null` fails; make it optional if the API can send + * `null`. + * - Only these `null` rules are covered. Field formats such as durations, + * base64 bytes, and numbers encoded as strings belong in the field schemas. + * + * **Example** (Decoding a Go response with nil fields) + * + * ```ts import.meta.vitest + * import { Schema } from "effect" + * + * const Container = Schema.Struct({ + * Id: Schema.String, + * Names: Schema.Array(Schema.String), + * Labels: Schema.Record(Schema.String, Schema.String), + * Status: Schema.optionalKey(Schema.String) + * }) + * + * const decode = Schema.decodeUnknownSync(Schema.toCodecGoJson(Container)) + * + * decode({ Id: "a", Names: null, Labels: null, Status: null }) // => { Id: "a", Names: [], Labels: {} } + * ``` + * + * @see {@link toCodecJson} for the canonical JSON codec + * @stability unstable + * @category converting + * @since 4.0.6 + */ +export const toCodecGoJson: { + (schema: S): toCodecGoJson +} = InternalToCodec.toCodecGoJson type XmlEncoderOptions = { /** Root element name for the returned XML string. Default: "root" */ readonly rootName?: string | undefined @@ -17386,7 +17856,7 @@ export declare namespace Annotations { readonly "~sentinels"?: ReadonlyArray | undefined } /** - * Annotations for filter schema nodes (created via `Schema.filter`). Extends + * Annotations for filter checks (created via `Schema.makeFilter`). Extends * {@link Augment} with an optional error message, identifier, and metadata. * Filters are intentionally non-parametric to keep them covariant. * diff --git a/repos/effect/packages/effect/src/SchemaAST.ts b/packages/effect/src/SchemaAST.ts similarity index 99% rename from repos/effect/packages/effect/src/SchemaAST.ts rename to packages/effect/src/SchemaAST.ts index 4bf42ef43..943329d78 100644 --- a/repos/effect/packages/effect/src/SchemaAST.ts +++ b/packages/effect/src/SchemaAST.ts @@ -480,7 +480,7 @@ export type Encoding = readonly [Link, ...Array] * * **Details** * - * Pass to `Schema.decodeUnknown`, `Schema.encode`, and related APIs to customize + * Pass to `Schema.decodeUnknownEffect`, `Schema.encodeEffect`, and related APIs to customize * error reporting, excess property handling, check execution, and product * concurrency. Options apply throughout the parse; schema annotations do not * override them. @@ -814,7 +814,13 @@ export const Declaration: new( let run: ReturnType return (input, options) => { if (input === InternalParser.missing) return InternalParser.missingExit - return (run ??= this.run(this.typeParameters))(input, this, options) + // Keep this declaration's initialization errors outside its callback catch. + run ??= this.run(this.typeParameters) + try { + return run(input, this, options) + } catch (error) { + return Effect.die(error) + } } } private _rebuild( @@ -2435,7 +2441,7 @@ export const Arrays: new( input, len, tailThreshold: Math.max(elementLen, len - tailLen), - output: new globalThis.Array(len), + output: [], issues: undefined, options } @@ -2599,9 +2605,12 @@ const wrapPropertyKeyIssue = ( /** * floating point or integer, with optional exponent + * + * The mantissa alternation keeps rejection linear: `\d*\.?\d+` retries + * every split of a long digit run. * @internal */ -export const FINITE_PATTERN = "[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?" +export const FINITE_PATTERN = "[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:[Ee][+-]?\\d+)?" /** * Returns the object keys that match the index signature parameter schema. @@ -3101,12 +3110,14 @@ export const Objects: new( // Resumes at the property whose parser suspended, without replaying the // properties already parsed. const resume = ( - state: ObjectParserState, + snapshot: ObjectParserState, index: number, pending: Effect.Effect ): Effect.Effect => { const property = properties![index] return Effect.flatMap(Effect.exit(pending), (exit) => { + // Copy the parsed prefix per execution so reruns cannot overwrite earlier results. + const state: ObjectParserState = { ...snapshot, out: { ...snapshot.out } } const terminal = stepProperty(state, property, exit) if (terminal) return terminal const done = () => InternalParser.succeed(state.out) @@ -4278,7 +4289,7 @@ const numberToJson = new Link( * * **Details** * - * The filter can be used with `Schema.filter` or attached directly to a + * The filter can be used with `Schema.check` or attached directly to a * `String` AST node through checks. The regular expression is cloned and its * `lastIndex` is reset before each test, so global and sticky expressions are * deterministic and the provided regular expression is not mutated. The @@ -4777,7 +4788,7 @@ function flipEncoding(ast: AST, encoding: Encoding): AST { * **Details** * * After flipping, what was decoding becomes encoding and vice versa. This is - * the core operation behind `Schema.encode` — encoding a value is decoding + * the core operation behind `Schema.encodeEffect` — encoding a value is decoding * with a flipped SchemaAST. * * - Memoized: same input reference → same output reference. @@ -5110,7 +5121,7 @@ export function getConstructorDescriptor(ast: AST): ConstructorDescriptor | unde * **Details** * * If the node has {@link Checks}, returns annotations from the last check - * (which is where user-supplied annotations end up after `.pipe(Schema.annotations(...))`). + * (which is where user-supplied annotations end up after `.pipe(Schema.annotate(...))`). * Otherwise returns the node's `annotations` directly. * * **Example** (Reading annotations) @@ -5153,7 +5164,7 @@ export const resolveAt: (key: string) => (ast: AST) => A | undefined = Intern * * **Details** * - * The identifier is typically set by `Schema.annotations({ identifier: "..." })` + * The identifier is typically set by `Schema.annotate({ identifier: "..." })` * and is used for error messages and schema identification. * * @see {@link resolve} diff --git a/repos/effect/packages/effect/src/SchemaGetter.ts b/packages/effect/src/SchemaGetter.ts similarity index 98% rename from repos/effect/packages/effect/src/SchemaGetter.ts rename to packages/effect/src/SchemaGetter.ts index f99a8bd28..609b5eb2d 100644 --- a/repos/effect/packages/effect/src/SchemaGetter.ts +++ b/packages/effect/src/SchemaGetter.ts @@ -992,7 +992,8 @@ export function Boolean(): Getter { * **Details** * * - Delegates to `globalThis.BigInt`. - * - Throws at runtime if the input cannot be converted (e.g. non-numeric string). + * - Fails with `SchemaIssue.InvalidValue` if the input cannot be converted + * (e.g. a non-numeric string or a non-integer number). * * **Example** (Coercing to a bigint) * @@ -1008,7 +1009,19 @@ export function Boolean(): Getter { * @since 4.0.0 */ export function BigInt(): Getter { - return transform(globalThis.BigInt) + return transformEffect((input, options) => { + try { + return Effect.succeed(globalThis.BigInt(input)) + } catch { + return Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a value that can be converted to a bigint" }, + input, + options + ) + ) + } + }) } /** @@ -1510,7 +1523,8 @@ export function encodeBase64(): Getter * * **Details** * - * The getter is pure and never fails. + * The getter is pure and never fails. The output is unpadded unless + * `padding` is `true`. * * **Example** (Encoding to Base64Url) * @@ -1530,8 +1544,11 @@ export function encodeBase64(): Getter * @category encoding * @since 4.0.0 */ -export function encodeBase64Url(): Getter { - return transform(Base64Url.encode) +export function encodeBase64Url(options?: { + /** Keep the `=` padding. Defaults to `false`. */ + readonly padding?: boolean | undefined +}): Getter { + return transform(options?.padding ? Base64Url.encodePadded : Base64Url.encode) } /** diff --git a/repos/effect/packages/effect/src/SchemaIssue.ts b/packages/effect/src/SchemaIssue.ts similarity index 95% rename from repos/effect/packages/effect/src/SchemaIssue.ts rename to packages/effect/src/SchemaIssue.ts index 6e32f0911..6f60b279a 100644 --- a/repos/effect/packages/effect/src/SchemaIssue.ts +++ b/packages/effect/src/SchemaIssue.ts @@ -720,8 +720,8 @@ export const InvalidType: new( * * **When to use** * - * Use when you need to detect constraint violations from `Schema.filter`, - * `Schema.minLength`, `Schema.greaterThan`, or similar checks. + * Use when you need to detect constraint violations from `Schema.check` with + * `Schema.isMinLength`, `Schema.isGreaterThan`, or similar checks. * * **Details** * @@ -1242,8 +1242,8 @@ export type CheckHook = (issue: Filter) => string | undefined * * **Details** * - * - Looks for a `message` annotation on the inner issue first, then on the - * filter itself. + * - Looks for a `message` annotation on the filter first, then on the inner + * issue. * - Returns `undefined` when no annotation is found, causing the formatter to * fall back to `"Expected "` or, when the filter reports input, * `"Expected , got "`. @@ -1255,7 +1255,7 @@ export type CheckHook = (issue: Filter) => string | undefined * @category formatting * @since 4.0.0 */ -export const defaultCheckHook: CheckHook = (issue): string | undefined => findMessage(issue.issue) ?? findMessage(issue) +export const defaultCheckHook: CheckHook = (issue): string | undefined => findMessage(issue) ?? findMessage(issue.issue) /** * Creates a {@link Formatter} that produces a `StandardSchemaV1.FailureResult`. @@ -1329,46 +1329,48 @@ function getExpectedMessage(expected: string, issue: Issue): string { return input === undefined ? `Expected ${expected}` : `Expected ${expected}, got ${input}` } +function getFilterMessage(issue: Filter, checkHook: CheckHook): string | undefined { + const message = checkHook(issue) + if (message !== undefined || issue.issue._tag !== "InvalidValue") return message + const expected = findExpected(issue.issue) + return expected === undefined + ? getExpectedMessage(formatCheck(issue.filter), issue) + : getExpectedMessage(expected, issue.issue) +} + function toDefaultIssues( issue: Issue, path: ReadonlyArray, leafHook: LeafHook, - checkHook: CheckHook + checkHook: CheckHook, + messageOverride?: string ): Array { switch (issue._tag) { - case "Filter": { - const message = checkHook(issue) - if (message !== undefined) { - return [{ path, message }] - } - if (issue.issue._tag !== "InvalidValue") { - return toDefaultIssues(issue.issue, path, leafHook, checkHook) - } - const expected = findExpected(issue.issue) - return [{ + case "Filter": + return toDefaultIssues( + issue.issue, path, - message: expected === undefined - ? getExpectedMessage(formatCheck(issue.filter), issue) - : getExpectedMessage(expected, issue.issue) - }] - } + leafHook, + checkHook, + messageOverride ?? getFilterMessage(issue, checkHook) + ) case "Encoding": - return toDefaultIssues(issue.issue, path, leafHook, checkHook) + return toDefaultIssues(issue.issue, path, leafHook, checkHook, messageOverride) case "Pointer": - return toDefaultIssues(issue.issue, [...path, ...issue.path], leafHook, checkHook) + return toDefaultIssues(issue.issue, [...path, ...issue.path], leafHook, checkHook, messageOverride) case "Composite": - return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook)) case "AnyOf": { - if (issue.issues.length === 0) { - return [{ - path, - message: findMessage(issue) ?? getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue) - }] + if (issue._tag === "Composite" || issue.issues.length > 0) { + return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook, messageOverride)) } - return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook)) + return [{ + path, + message: messageOverride ?? findMessage(issue) ?? + getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue) + }] } default: - return [{ path, message: leafHook(issue) }] + return [{ path, message: messageOverride ?? leafHook(issue) }] } } @@ -1431,45 +1433,34 @@ export function makeFormatterDefault(): Formatter { /** @internal */ export const defaultFormatter = makeFormatterDefault() -function formatIssue(issue: Issue, path: string): string { +function formatIssue(issue: Issue, path: string, messageOverride?: string): string { let message: string switch (issue._tag) { - case "Filter": { - const annotated = defaultCheckHook(issue) - if (annotated !== undefined) { - message = annotated - } else { - if (issue.issue._tag !== "InvalidValue") { - return formatIssue(issue.issue, path) - } - const expected = findExpected(issue.issue) - message = expected === undefined - ? getExpectedMessage(formatCheck(issue.filter), issue) - : getExpectedMessage(expected, issue.issue) - } - break - } + case "Filter": + return formatIssue(issue.issue, path, messageOverride ?? getFilterMessage(issue, defaultCheckHook)) case "Encoding": - return formatIssue(issue.issue, path) + return formatIssue(issue.issue, path, messageOverride) case "Pointer": - return formatIssue(issue.issue, path + formatPath(issue.path)) + return formatIssue(issue.issue, path + formatPath(issue.path), messageOverride) case "Composite": case "AnyOf": { if (issue._tag === "Composite" || issue.issues.length > 0) { - return issue.issues.map((issue) => formatIssue(issue, path)).join("\n") + return issue.issues.map((issue) => formatIssue(issue, path, messageOverride)).join("\n") } - message = findMessage(issue) ?? getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue) + message = messageOverride ?? findMessage(issue) ?? + getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue) break } default: - message = defaultLeafHook(issue) + message = messageOverride ?? defaultLeafHook(issue) break } return path ? `${message}\n at ${path}` : message } function findMessage(issue: Issue): string | undefined { - if (issue._tag === "Pointer") return + // A Composite's ast describes the whole schema, not the failures it groups. + if (issue._tag === "Pointer" || issue._tag === "Composite") return if (issue._tag === "Encoding") return findMessage(issue.issue) const annotations = issue._tag === "Filter" ? issue.filter.annotations diff --git a/repos/effect/packages/effect/src/SchemaParser.ts b/packages/effect/src/SchemaParser.ts similarity index 100% rename from repos/effect/packages/effect/src/SchemaParser.ts rename to packages/effect/src/SchemaParser.ts diff --git a/repos/effect/packages/effect/src/SchemaRepresentation.ts b/packages/effect/src/SchemaRepresentation.ts similarity index 97% rename from repos/effect/packages/effect/src/SchemaRepresentation.ts rename to packages/effect/src/SchemaRepresentation.ts index d6153da6e..c591caf1e 100644 --- a/repos/effect/packages/effect/src/SchemaRepresentation.ts +++ b/packages/effect/src/SchemaRepresentation.ts @@ -1944,6 +1944,32 @@ export const BigDecimalReviver: DeclarationReviver = makeFixedDeclarationR Schema.BigDecimal ) +/** + * Reviver for persisted {@link Schema.SemVer} declarations. + * + * @see {@link Schema.SemVer} for the corresponding schema + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const SemVerReviver: DeclarationReviver = makeFixedDeclarationReviver( + "effect/schema/SemVer", + Schema.SemVer +) + +/** + * Reviver for persisted {@link Schema.SemVerRange} declarations. + * + * @see {@link Schema.SemVerRange} for the corresponding schema + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const SemVerRangeReviver: DeclarationReviver = makeFixedDeclarationReviver( + "effect/schema/SemVerRange", + Schema.SemVerRange +) + /** * Reviver for persisted `File` declarations. * @@ -2387,7 +2413,8 @@ const jsonSchemaRevivers: ReadonlyArray = [ * * **When to use** * - * Use when you need to configure pattern handling or transform each JSON Schema node before translation. + * Use when you need to configure pattern handling, preserve custom annotations, or transform each JSON Schema node + * before translation. * * **Details** * @@ -2422,6 +2449,42 @@ export interface FromJsonSchemaOptions { * @default "error" */ readonly patterns?: "error" | "ignore" | "apply" | undefined + /** + * Selects additional JSON Schema keys to preserve as Effect schema annotations. + * + * **Details** + * + * Standard annotations such as `title` and `description` are imported independently of this predicate. + * Other keys are ignored by default. Selected values must be JSON-compatible and are kept as opaque metadata. + * Selection runs after `onEnter`. JSON Schema structural and validation keywords, Effect's reserved annotation + * keys, and keys starting with `~` are excluded before calling the predicate. Exceptions from the predicate + * pass through unchanged. + * + * **Gotchas** + * + * Import normalizes the schema. Selected annotations stay on surviving values when checks simplify, and + * annotations from eliminated alternatives or unreachable children are discarded. When schemas merge, + * later annotations overwrite earlier values for the same key, following the importer's merge order. + * Retaining an annotation does not enforce its semantics or preserve the original JSON Schema structure. + * + * **Example** (Preserving custom metadata on import and export) + * + * ```ts import.meta.vitest + * import { JsonSchema, Schema, SchemaRepresentation } from "effect" + * + * const document = JsonSchema.fromSchemaDraft2020_12({ + * type: "string", + * "x-sensitive": true + * }) + * const options = { includeAnnotationKey: (key: string) => key.startsWith("x-") } + * const schema = SchemaRepresentation.fromJsonSchemaDocument(document, options) + * + * Schema.toJsonSchemaDocument(schema, options).schema // => { type: "string", "x-sensitive": true } + * ``` + * + * @see Schema.ToJsonSchemaOptions + */ + readonly includeAnnotationKey?: ((key: string) => boolean) | undefined } /** diff --git a/repos/effect/packages/effect/src/SchemaTransformation.ts b/packages/effect/src/SchemaTransformation.ts similarity index 99% rename from repos/effect/packages/effect/src/SchemaTransformation.ts rename to packages/effect/src/SchemaTransformation.ts index 7824aa0e3..6a2dfdf02 100644 --- a/repos/effect/packages/effect/src/SchemaTransformation.ts +++ b/packages/effect/src/SchemaTransformation.ts @@ -1818,7 +1818,7 @@ export const stringFromHexString: Transformation = new Transform * **When to use** * * Use when you need a schema transformation to store structured data in URL - * query parameters or fragments, such as composing with `Schema.parseJson` to + * query parameters or fragments, such as composing with `Schema.fromJsonString` to * round-trip JSON through a URL. * * **Details** diff --git a/repos/effect/packages/effect/src/Scope.ts b/packages/effect/src/Scope.ts similarity index 97% rename from repos/effect/packages/effect/src/Scope.ts rename to packages/effect/src/Scope.ts index 2746d1a2c..8ff3748f8 100644 --- a/repos/effect/packages/effect/src/Scope.ts +++ b/packages/effect/src/Scope.ts @@ -159,8 +159,13 @@ export declare namespace State { finalizers: Map<{}, (exit: Exit) => Effect> | undefined } /** - * Represents a closed scope state where finalizers have been executed - * and the scope is no longer accepting new resources. + * Represents a closed scope state where the scope is no longer accepting + * new resources. + * + * **Details** + * + * The scope enters this state when closing starts, so its finalizers may + * still be running. * * **Example** (Inspecting a closed scope state) * @@ -481,6 +486,11 @@ export const forkUnsafe: (scope: Scope, finalizerStrategy?: "sequential" | "para * By default, finalizers run uninterruptibly, so interrupting the closing fiber * waits for them to finish; a finalizer can explicitly restore interruptibility. * + * **Gotchas** + * + * Closing a scope that is already closed returns immediately, even when the + * finalizers of an earlier close are still running. + * * **Example** (Running scope finalizers) * * ```ts import.meta.vitest diff --git a/repos/effect/packages/effect/src/ScopedCache.ts b/packages/effect/src/ScopedCache.ts similarity index 96% rename from repos/effect/packages/effect/src/ScopedCache.ts rename to packages/effect/src/ScopedCache.ts index cdeeaa17a..b34aa0e92 100644 --- a/repos/effect/packages/effect/src/ScopedCache.ts +++ b/packages/effect/src/ScopedCache.ts @@ -7,6 +7,8 @@ * remaining entries when the cache is closed. Lookups for the same missing key * share one in-progress effect, and entries can expire, be refreshed, be * invalidated, or be evicted by capacity limits. + * Expiry cleanup is lazy: an expired entry is removed on access to that key or + * a `keys`/`values`/`entries` scan, not by a background timer. * * @stability stable * @since 4.0.0 @@ -39,14 +41,15 @@ const TypeId = "~effect/ScopedCache" * **When to use** * * Use to cache values that acquire scoped resources and must release those - * resources when entries expire, are evicted, or are invalidated. + * resources when entries are removed or the owning scope closes. * * **Details** * * Concurrent requests for the same key share the same in-flight lookup. * Entries can expire based on the lookup exit, are evicted when capacity is - * exceeded, and release their entry scopes when invalidated, evicted, expired, - * or when the cache's owning scope closes. + * exceeded, and release their entry scopes when invalidated, evicted, or when + * the cache's owning scope closes. Expired entries are cleaned up lazily on + * access to that key or a `keys`/`values`/`entries` scan, never in the background. * * @see {@link make} for creating a scoped cache with a fixed time-to-live * @see {@link makeWith} for creating a scoped cache with dynamic time-to-live @@ -127,8 +130,9 @@ export interface Entry { * **Details** * * The cache must be constructed in a `Scope`. Each lookup runs in its own entry - * scope, and that scope is closed when the entry expires, is invalidated, is - * evicted by capacity, or when the cache's owning scope closes. + * scope, and that scope is closed when the entry is invalidated, is evicted by + * capacity, or when the cache's owning scope closes. Expired entries are cleaned + * up on access to that key or a `keys`/`values`/`entries` scan, not in the background. * `requireServicesAt` controls whether lookup services are captured at * construction time or required when lookup operations run. * @@ -192,8 +196,9 @@ export const makeWith = < * * This is the constant-TTL variant of `makeWith`: values are acquired by the * lookup effect in per-entry scopes, capacity can evict older entries, and - * entry scopes are closed when entries expire, are invalidated, are evicted, or - * when the cache's owning scope closes. + * entry scopes are closed when entries are invalidated, are evicted, or when + * the cache's owning scope closes. Expiry cleanup is lazy: expired entries close + * on access to that key or a `keys`/`values`/`entries` scan, not in the background. * * @see {@link makeWith} for computing time-to-live from each lookup result and key * @@ -770,11 +775,11 @@ const invalidateAllImpl = ( // Detach the batch before finalizers can reenter the cache. const entries = Array.from(MutableHashMap.values(map)) MutableHashMap.clear(map) - const fibers = Arr.empty>() + const fibers = Arr.empty>() for (const entry of entries) { fibers.push(effect.forkUnsafe(parent as any, Scope.close(entry.scope, effect.exitVoid), true, true)) } - return effect.fiberAwaitAll(fibers) + return effect.flatMap(effect.fiberAwaitAll(fibers), Exit.asVoidAll) } /** diff --git a/repos/effect/packages/effect/src/ScopedRef.ts b/packages/effect/src/ScopedRef.ts similarity index 100% rename from repos/effect/packages/effect/src/ScopedRef.ts rename to packages/effect/src/ScopedRef.ts diff --git a/packages/effect/src/SemVer.ts b/packages/effect/src/SemVer.ts new file mode 100644 index 000000000..89798fd39 --- /dev/null +++ b/packages/effect/src/SemVer.ts @@ -0,0 +1,1042 @@ +/** + * Immutable [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) + * values with strict parsing, canonical formatting, spec precedence ordering, + * and version bumps. + * + * A `SemVer` has numeric major, minor, and patch components, optional + * pre-release identifiers, and optional build metadata. Structural equality + * and hashing include build metadata, while `Order` follows spec precedence + * and ignores it. Version ranges live in `SemVerRange`. + * + * @stability unstable + * @since 4.0.6 + */ +import * as Data from "./Data.ts" +import * as Equal from "./Equal.ts" +import * as Equ from "./Equivalence.ts" +import { dual } from "./Function.ts" +import * as Hash from "./Hash.ts" +import { type Inspectable, NodeInspectSymbol } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import * as order from "./Order.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import * as Result from "./Result.ts" + +const TypeId = "~effect/SemVer" +const SemVerErrorTypeId = "~effect/SemVer/SemVerError" + +/** + * Represents a semantic version. + * + * **Details** + * + * Pre-release identifiers are numbers when they consist only of digits and + * strings otherwise. Build metadata identifiers are always strings because + * they may contain leading zeros. Both arrays are empty when absent. + * + * **Example** (Reading version components) + * + * ```ts import.meta.vitest + * import { SemVer } from "effect" + * + * const version = SemVer.parseUnsafe("1.2.3-rc.1+build.5") + * + * version.minor // => 2 + * version.prerelease // => ["rc", 1] + * version.build // => ["build", "5"] + * ``` + * + * @see {@link parse} for decoding version strings + * @see {@link make} for constructing versions from components + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface SemVer extends Pipeable, Equal.Equal, Inspectable { + readonly [TypeId]: typeof TypeId + readonly major: number + readonly minor: number + readonly patch: number + readonly prerelease: ReadonlyArray + readonly build: ReadonlyArray +} + +/** + * Error raised for invalid version strings, ranges, or components. + * + * @see {@link isSemVerError} for narrowing unknown values + * @stability unstable + * @category errors + * @since 4.0.6 + */ +export class SemVerError extends Data.TaggedError("SemVerError")<{ + readonly message: string + readonly input?: string +}> { + readonly [SemVerErrorTypeId]: typeof SemVerErrorTypeId = SemVerErrorTypeId +} + +/** + * Checks whether a value is a `SemVerError`. + * + * @stability unstable + * @category guards + * @since 4.0.6 + */ +export const isSemVerError = (u: unknown): u is SemVerError => hasProperty(u, SemVerErrorTypeId) + +/** @internal */ +export const invalid = (message: string): never => { + throw new SemVerError({ message }) +} + +const attempt = (evaluate: () => A): Result.Result => + Result.try({ + try: evaluate, + catch: (error) => { + if (!isSemVerError(error)) throw error + return error + } + }) + +/** @internal */ +export const tryParse = ( + label: string, + input: string, + parse: (input: string) => A +): Result.Result => + Result.mapError( + attempt(() => parse(input)), + (error) => new SemVerError({ message: `Invalid ${label} ${JSON.stringify(input)}: ${error.message}`, input }) + ) + +const numericPattern = /^(?:0|[1-9]\d*)$/ +const digitsPattern = /^\d+$/ +const identifierPattern = /^[0-9A-Za-z-]+$/ + +const checkComponent = (value: number, label: string): number => { + if (!Number.isSafeInteger(value) || value < 0) { + return invalid(`${label} must be a non-negative safe integer, received ${value}`) + } + return value +} + +/** @internal */ +export const parseComponent = (value: string, label: string): number => { + if (!numericPattern.test(value)) { + return invalid(`${label} must be a non-negative integer without leading zeros, received ${JSON.stringify(value)}`) + } + return checkComponent(Number(value), label) +} + +/** @internal */ +export const prereleaseIdentifier = (identifier: string | number): string | number => { + if (typeof identifier === "number") return checkComponent(identifier, "numeric pre-release identifier") + if (!identifierPattern.test(identifier)) { + return invalid(`pre-release identifier must be non-empty [0-9A-Za-z-], received ${JSON.stringify(identifier)}`) + } + return digitsPattern.test(identifier) ? parseComponent(identifier, "numeric pre-release identifier") : identifier +} + +const buildIdentifier = (identifier: string): string => { + if (!identifierPattern.test(identifier)) { + return invalid(`build identifier must be non-empty [0-9A-Za-z-], received ${JSON.stringify(identifier)}`) + } + return identifier +} + +/** @internal */ +export type Core = Pick + +const comparePrerelease = ( + self: ReadonlyArray, + that: ReadonlyArray +): -1 | 0 | 1 => { + if (self.length === 0) return that.length === 0 ? 0 : 1 + if (that.length === 0) return -1 + const length = Math.min(self.length, that.length) + for (let i = 0; i < length; i++) { + const a = self[i] + const b = that[i] + if (a === b) continue + if (typeof a === "number") return typeof b === "number" ? order.Number(a, b) : -1 + if (typeof b === "number") return 1 + return order.String(a, b) + } + return order.Number(self.length, that.length) +} + +/** @internal */ +export const sameCore = (self: Core, that: Core): boolean => + self.major === that.major && self.minor === that.minor && self.patch === that.patch + +/** @internal */ +export const compareCore = (self: Core, that: Core): -1 | 0 | 1 => + order.Number(self.major, that.major) || + order.Number(self.minor, that.minor) || + order.Number(self.patch, that.patch) || + comparePrerelease(self.prerelease, that.prerelease) + +const SemVerProto: Omit = { + [TypeId]: TypeId, + [Equal.symbol](this: SemVer, that: unknown): boolean { + return isSemVer(that) && Equivalence(this, that) + }, + [Hash.symbol](this: SemVer): number { + return Hash.string(format(this)) + }, + toString(this: SemVer) { + return `SemVer(${format(this)})` + }, + toJSON(this: SemVer) { + return { _id: "SemVer", value: format(this) } + }, + [NodeInspectSymbol](this: SemVer) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +const unchecked = ( + major: number, + minor: number, + patch: number, + prerelease: ReadonlyArray, + build: ReadonlyArray +): SemVer => { + const self = Object.create(SemVerProto) + self.major = major + self.minor = minor + self.patch = patch + self.prerelease = prerelease + self.build = build + return self +} + +const increment = (value: number, label: string): number => { + if (value >= Number.MAX_SAFE_INTEGER) return invalid(`${label} cannot be incremented past Number.MAX_SAFE_INTEGER`) + return value + 1 +} + +/** + * Checks whether a value is a `SemVer`. + * + * @stability unstable + * @category guards + * @since 4.0.6 + */ +export const isSemVer = (u: unknown): u is SemVer => hasProperty(u, TypeId) + +/** + * The numeric components and identifiers of a version. + * + * @see {@link make} for validating components into a version + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface Components { + readonly major: number + readonly minor: number + readonly patch: number + readonly prerelease?: ReadonlyArray | undefined + readonly build?: ReadonlyArray | undefined +} + +// The intersection excludes signs, leading zeros, and exponent notation. +type NumericComponent = "0" | (`${bigint}` & `${"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"}${string}`) + +/** + * Version string literals accepted by `fromInput`. + * + * **Details** + * + * Requires three numeric components without signs or leading zeros, followed + * by an optional `-` pre-release and `+` build suffix. Component magnitudes + * (at most `Number.MAX_SAFE_INTEGER`) and identifiers are only validated at + * runtime. + * + * @see {@link parse} for arbitrary strings + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type VersionString = `${NumericComponent}.${NumericComponent}.${NumericComponent}${ + | "" + | `-${string}` + | `+${string}`}` + +/** + * Values accepted by `fromInput`: a version, a version string literal, or + * version components. + * + * @see {@link fromInput} for converting inputs + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type Input = SemVer | VersionString | Components + +/** + * Creates a version from its components and throws `SemVerError` for invalid + * components. + * + * **Details** + * + * Components must be non-negative safe integers. Identifiers must be non-empty + * and contain only ASCII alphanumerics and hyphens. Digit-only pre-release + * strings are normalized to numbers and must not have leading zeros. + * + * A `SemVer` spreads into exactly these components, so `make({ ...version, + * patch: version.patch + 1 })` derives a validated version from an existing + * one. + * + * **Example** (Creating a pre-release version) + * + * ```ts import.meta.vitest + * import { SemVer } from "effect" + * + * SemVer.format(SemVer.make({ major: 1, minor: 0, patch: 0, prerelease: ["beta", "2"] })) // => "1.0.0-beta.2" + * ``` + * + * @see {@link parse} for decoding version strings + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const make = (components: Components): SemVer => + unchecked( + checkComponent(components.major, "major"), + checkComponent(components.minor, "minor"), + checkComponent(components.patch, "patch"), + components.prerelease?.map(prereleaseIdentifier) ?? [], + components.build?.map(buildIdentifier) ?? [] + ) + +const parseOrThrow = (input: string): SemVer => { + let rest = input + let build: ReadonlyArray = [] + const plus = rest.indexOf("+") + if (plus !== -1) { + build = rest.slice(plus + 1).split(".").map(buildIdentifier) + rest = rest.slice(0, plus) + } + let prerelease: ReadonlyArray = [] + const dash = rest.indexOf("-") + if (dash !== -1) { + prerelease = rest.slice(dash + 1).split(".").map(prereleaseIdentifier) + rest = rest.slice(0, dash) + } + const core = rest.split(".") + if (core.length !== 3) return invalid("expected MAJOR.MINOR.PATCH") + return unchecked( + parseComponent(core[0], "major"), + parseComponent(core[1], "minor"), + parseComponent(core[2], "patch"), + prerelease, + build + ) +} + +/** + * Parses a strict Semantic Versioning 2.0.0 string. + * + * **Details** + * + * The input must match the spec grammar exactly. Leading zeros in numeric + * components or pre-release identifiers, empty identifiers, and numbers above + * `Number.MAX_SAFE_INTEGER` are rejected. + * + * **Gotchas** + * + * A `v` prefix, surrounding whitespace, and partial versions such as `1.2` are + * rejected because they are not semantic versions. + * + * **Example** (Parsing version strings) + * + * ```ts import.meta.vitest + * import { Result, SemVer } from "effect" + * + * Result.isSuccess(SemVer.parse("1.2.3-alpha.1+001")) // => true + * Result.isFailure(SemVer.parse("v1.2.3")) // => true + * ``` + * + * @see {@link parseUnsafe} for throwing on invalid input + * @see {@link format} for the inverse operation + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const parse = (input: string): Result.Result => tryParse("SemVer", input, parseOrThrow) + +/** + * Parses a strict Semantic Versioning 2.0.0 string and throws `SemVerError` + * for invalid input. + * + * @see {@link parse} for non-throwing parsing + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const parseUnsafe = (input: string): SemVer => Result.getOrThrow(parse(input)) + +/** + * Converts a version, version string, or version components into a `SemVer`. + * + * **Details** + * + * Versions are returned unchanged, strings are parsed like {@link parse}, and + * components are validated like {@link make}. Invalid input fails with + * `SemVerError`. + * + * **Example** (Accepting flexible version input) + * + * ```ts import.meta.vitest + * import { Result, SemVer } from "effect" + * + * Result.map(SemVer.fromInput("1.2.3-rc.1"), SemVer.format) // => Result.succeed("1.2.3-rc.1") + * Result.map(SemVer.fromInput({ major: 1, minor: 2, patch: 3 }), SemVer.format) // => Result.succeed("1.2.3") + * Result.isFailure(SemVer.fromInput({ major: -1, minor: 0, patch: 0 })) // => true + * ``` + * + * @see {@link fromInputUnsafe} for throwing on invalid input + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const fromInput = (input: Input): Result.Result => { + if (isSemVer(input)) return Result.succeed(input) + if (typeof input === "string") return parse(input) + return attempt(() => make(input)) +} + +/** + * Converts a version, version string, or version components into a `SemVer` + * and throws `SemVerError` for invalid input. + * + * @see {@link fromInput} for non-throwing conversion + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const fromInputUnsafe = (input: Input): SemVer => Result.getOrThrow(fromInput(input)) + +/** + * Formats a version as its canonical string, including pre-release identifiers + * and build metadata. + * + * @see {@link parse} for the inverse operation + * @stability unstable + * @category converting + * @since 4.0.6 + */ +export const format = (self: SemVer): string => { + let out = `${self.major}.${self.minor}.${self.patch}` + if (self.prerelease.length > 0) out += `-${self.prerelease.join(".")}` + if (self.build.length > 0) out += `+${self.build.join(".")}` + return out +} + +/** + * Checks whether a version has pre-release identifiers. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const isPrerelease = (self: SemVer): boolean => self.prerelease.length > 0 + +/** + * Checks whether a version has build metadata. + * + * **Details** + * + * Build metadata does not affect precedence, but it is part of structural + * equality. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const hasBuild = (self: SemVer): boolean => self.build.length > 0 + +/** + * Checks whether a version is in initial development, meaning its major + * version is `0`. + * + * **Details** + * + * The SemVer spec treats `0.y.z` as initial development where anything may + * change at any time and the public API should not be considered stable. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const isInitialDevelopment = (self: SemVer): boolean => self.major === 0 + +/** + * Orders versions by Semantic Versioning precedence. + * + * **Details** + * + * Major, minor, and patch are compared numerically. A pre-release has lower + * precedence than its release. Pre-release identifiers are compared left to + * right: numeric identifiers compare numerically and sort before alphanumeric + * identifiers, which compare in ASCII order. A longer list of otherwise equal + * identifiers has higher precedence. + * + * **Gotchas** + * + * Build metadata is ignored, so versions that differ only in build metadata + * compare as `0` even though they are not `Equal`. + * + * **Example** (Sorting by precedence) + * + * ```ts import.meta.vitest + * import { Array, SemVer } from "effect" + * + * const versions = ["1.0.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.0.0-beta.2", "1.0.0-alpha"].map(SemVer.parseUnsafe) + * + * Array.sort(versions, SemVer.Order).map(SemVer.format) // => ["1.0.0-alpha", "1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0"] + * ``` + * + * @see {@link Equivalence} for structural equality including build metadata + * @stability unstable + * @category instances + * @since 4.0.6 + */ +export const Order: order.Order = order.make(compareCore) + +const identifiersEqual = Equ.Array(Equ.strictEqual()) + +/** + * Provides structural equivalence for versions, including build metadata. + * + * @see {@link Order} for precedence that ignores build metadata + * @stability unstable + * @category instances + * @since 4.0.6 + */ +export const Equivalence: Equ.Equivalence = Equ.make((self, that) => + self.major === that.major && + self.minor === that.minor && + self.patch === that.patch && + identifiersEqual(self.prerelease, that.prerelease) && + identifiersEqual(self.build, that.build) +) + +/** + * Checks whether two versions are structurally equal, including build + * metadata. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const equals: { + (that: SemVer): (self: SemVer) => boolean + (self: SemVer, that: SemVer): boolean +} = dual(2, Equivalence) + +/** + * Checks whether the first version has lower precedence than the second. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const isLessThan: { + (that: SemVer): (self: SemVer) => boolean + (self: SemVer, that: SemVer): boolean +} = order.isLessThan(Order) + +/** + * Checks whether the first version has lower or equal precedence than the + * second. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const isLessThanOrEqualTo: { + (that: SemVer): (self: SemVer) => boolean + (self: SemVer, that: SemVer): boolean +} = order.isLessThanOrEqualTo(Order) + +/** + * Checks whether the first version has higher precedence than the second. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const isGreaterThan: { + (that: SemVer): (self: SemVer) => boolean + (self: SemVer, that: SemVer): boolean +} = order.isGreaterThan(Order) + +/** + * Checks whether the first version has higher or equal precedence than the + * second. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const isGreaterThanOrEqualTo: { + (that: SemVer): (self: SemVer) => boolean + (self: SemVer, that: SemVer): boolean +} = order.isGreaterThanOrEqualTo(Order) + +/** + * Checks whether a version's precedence is within an inclusive range. + * + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const between: { + (options: { minimum: SemVer; maximum: SemVer }): (self: SemVer) => boolean + (self: SemVer, options: { minimum: SemVer; maximum: SemVer }): boolean +} = order.isBetween(Order) + +/** + * Returns the version with lower precedence, preferring the first on ties. + * + * @stability unstable + * @category ordering + * @since 4.0.6 + */ +export const min: { + (that: SemVer): (self: SemVer) => SemVer + (self: SemVer, that: SemVer): SemVer +} = order.min(Order) + +/** + * Returns the version with higher precedence, preferring the first on ties. + * + * @stability unstable + * @category ordering + * @since 4.0.6 + */ +export const max: { + (that: SemVer): (self: SemVer) => SemVer + (self: SemVer, that: SemVer): SemVer +} = order.max(Order) + +/** + * Constrains a version's precedence to an inclusive range. + * + * @stability unstable + * @category ordering + * @since 4.0.6 + */ +export const clamp: { + (options: { minimum: SemVer; maximum: SemVer }): (self: SemVer) => SemVer + (self: SemVer, options: { minimum: SemVer; maximum: SemVer }): SemVer +} = order.clamp(Order) + +/** + * Replaces the pre-release identifiers of a version and throws `SemVerError` + * for invalid identifiers. + * + * **Details** + * + * Digit-only strings are normalized to numbers. An empty array removes the + * pre-release. Build metadata is preserved. + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const withPrerelease: { + (prerelease: ReadonlyArray): (self: SemVer) => SemVer + (self: SemVer, prerelease: ReadonlyArray): SemVer +} = dual( + 2, + (self: SemVer, prerelease: ReadonlyArray): SemVer => + unchecked(self.major, self.minor, self.patch, prerelease.map(prereleaseIdentifier), self.build) +) + +/** + * Replaces the build metadata of a version and throws `SemVerError` for + * invalid identifiers. + * + * **Details** + * + * An empty array removes the build metadata. Pre-release identifiers are + * preserved. + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const withBuild: { + (build: ReadonlyArray): (self: SemVer) => SemVer + (self: SemVer, build: ReadonlyArray): SemVer +} = dual( + 2, + (self: SemVer, build: ReadonlyArray): SemVer => + unchecked(self.major, self.minor, self.patch, self.prerelease, build.map(buildIdentifier)) +) + +/** + * Removes pre-release identifiers and build metadata from a version. + * + * **Example** (Finalizing a release candidate) + * + * ```ts import.meta.vitest + * import { SemVer } from "effect" + * + * SemVer.format(SemVer.release(SemVer.parseUnsafe("2.0.0-rc.3+sha.5114f85"))) // => "2.0.0" + * ``` + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const release = (self: SemVer): SemVer => + self.prerelease.length === 0 && self.build.length === 0 + ? self + : unchecked(self.major, self.minor, self.patch, [], []) + +/** + * Returns the next major release. + * + * **Details** + * + * Returns the smallest major release with higher precedence, so a pre-release + * of a major version, such as `2.0.0-rc.1`, becomes `2.0.0`. Pre-release + * identifiers and build metadata are removed. + * + * **Example** (Bumping major versions) + * + * ```ts import.meta.vitest + * import { SemVer } from "effect" + * + * SemVer.format(SemVer.bumpMajor(SemVer.parseUnsafe("1.2.3"))) // => "2.0.0" + * SemVer.format(SemVer.bumpMajor(SemVer.parseUnsafe("2.0.0-rc.1"))) // => "2.0.0" + * SemVer.format(SemVer.bumpMajor(SemVer.parseUnsafe("2.1.0-rc.1"))) // => "3.0.0" + * ``` + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bumpMajor = (self: SemVer): SemVer => + unchecked( + self.minor === 0 && self.patch === 0 && self.prerelease.length > 0 ? self.major : increment(self.major, "major"), + 0, + 0, + [], + [] + ) + +/** + * Returns the next minor release. + * + * **Details** + * + * Returns the smallest minor release with higher precedence, so a pre-release + * of a minor version, such as `1.3.0-rc.1`, becomes `1.3.0`. Pre-release + * identifiers and build metadata are removed. + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bumpMinor = (self: SemVer): SemVer => + unchecked( + self.major, + self.patch === 0 && self.prerelease.length > 0 ? self.minor : increment(self.minor, "minor"), + 0, + [], + [] + ) + +/** + * Returns the next patch release. + * + * **Details** + * + * Returns the smallest patch release with higher precedence, so a pre-release + * such as `1.2.3-rc.1` becomes `1.2.3`. Pre-release identifiers and build + * metadata are removed. + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bumpPatch = (self: SemVer): SemVer => + unchecked( + self.major, + self.minor, + self.prerelease.length > 0 ? self.patch : increment(self.patch, "patch"), + [], + [] + ) + +const startPrerelease = (identifier: string | undefined): ReadonlyArray => + identifier === undefined ? [0] : [prereleaseIdentifier(identifier), 0] + +const isSemVerArgs = (args: IArguments) => isSemVer(args[0]) + +/** + * Returns the first pre-release of the next major version. + * + * **Details** + * + * Increments major, resets minor and patch, and starts the pre-release at + * `[identifier, 0]`, or `[0]` without an identifier. Build metadata is removed. + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bumpPremajor: { + (identifier?: string): (self: SemVer) => SemVer + (self: SemVer, identifier?: string): SemVer +} = dual( + isSemVerArgs, + (self: SemVer, identifier?: string): SemVer => + unchecked(increment(self.major, "major"), 0, 0, startPrerelease(identifier), []) +) + +/** + * Returns the first pre-release of the next minor version. + * + * **Details** + * + * Increments minor, resets patch, and starts the pre-release at + * `[identifier, 0]`, or `[0]` without an identifier. Build metadata is removed. + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bumpPreminor: { + (identifier?: string): (self: SemVer) => SemVer + (self: SemVer, identifier?: string): SemVer +} = dual( + isSemVerArgs, + (self: SemVer, identifier?: string): SemVer => + unchecked(self.major, increment(self.minor, "minor"), 0, startPrerelease(identifier), []) +) + +/** + * Returns the first pre-release of the next patch version. + * + * **Details** + * + * Increments patch and starts the pre-release at `[identifier, 0]`, or `[0]` + * without an identifier. Build metadata is removed. + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bumpPrepatch: { + (identifier?: string): (self: SemVer) => SemVer + (self: SemVer, identifier?: string): SemVer +} = dual( + isSemVerArgs, + (self: SemVer, identifier?: string): SemVer => + unchecked(self.major, self.minor, increment(self.patch, "patch"), startPrerelease(identifier), []) +) + +const nextPrerelease = (prerelease: ReadonlyArray): ReadonlyArray => { + for (let i = prerelease.length - 1; i >= 0; i--) { + const identifier = prerelease[i] + if (typeof identifier === "number") { + const next = prerelease.slice() + next[i] = increment(identifier, "pre-release identifier") + return next + } + } + return [...prerelease, 0] +} + +/** + * Returns the next pre-release. + * + * **Details** + * + * - A release starts the pre-release of the next patch, like + * {@link bumpPrepatch}. + * - An identifier that differs from the first pre-release identifier restarts + * the pre-release at `[identifier, 0]`. + * - Otherwise the right-most numeric identifier is incremented, or `0` is + * appended when there is none. + * + * Build metadata is removed. + * + * **Gotchas** + * + * Switching to an identifier that sorts lower, such as from `beta` to `alpha`, + * returns a version with lower precedence. + * + * **Example** (Advancing pre-releases) + * + * ```ts import.meta.vitest + * import { SemVer } from "effect" + * + * SemVer.format(SemVer.bumpPrerelease(SemVer.parseUnsafe("1.2.3"), "alpha")) // => "1.2.4-alpha.0" + * SemVer.format(SemVer.bumpPrerelease(SemVer.parseUnsafe("1.2.4-alpha.0"))) // => "1.2.4-alpha.1" + * SemVer.format(SemVer.bumpPrerelease(SemVer.parseUnsafe("1.2.4-alpha.1"), "beta")) // => "1.2.4-beta.0" + * ``` + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bumpPrerelease: { + (identifier?: string): (self: SemVer) => SemVer + (self: SemVer, identifier?: string): SemVer +} = dual(isSemVerArgs, (self: SemVer, identifier?: string): SemVer => { + if (self.prerelease.length === 0) { + return unchecked(self.major, self.minor, increment(self.patch, "patch"), startPrerelease(identifier), []) + } + if (identifier !== undefined) { + const normalized = prereleaseIdentifier(identifier) + if (self.prerelease[0] !== normalized) { + return unchecked(self.major, self.minor, self.patch, [normalized, 0], []) + } + } + return unchecked(self.major, self.minor, self.patch, nextPrerelease(self.prerelease), []) +}) + +/** + * Names of version changes, matching npm's release types. + * + * @see {@link difference} for classifying the change between two versions + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type ReleaseType = "major" | "premajor" | "minor" | "preminor" | "patch" | "prepatch" | "prerelease" + +/** + * Bumps a version by release type. + * + * **When to use** + * + * Use when the release type is data, such as a CLI argument, configuration, or + * the result of {@link difference}. + * + * **Details** + * + * Dispatches to {@link bumpMajor}, {@link bumpMinor}, {@link bumpPatch}, + * {@link bumpPremajor}, {@link bumpPreminor}, {@link bumpPrepatch}, or + * {@link bumpPrerelease}, with the same semantics. + * + * **Gotchas** + * + * The pre-release `identifier` is ignored for `"major"`, `"minor"`, and + * `"patch"`. + * + * **Example** (Bumping by a release type from input) + * + * ```ts import.meta.vitest + * import { SemVer } from "effect" + * + * const version = SemVer.parseUnsafe("1.2.3") + * + * SemVer.format(SemVer.bump(version, "minor")) // => "1.3.0" + * SemVer.format(SemVer.bump(version, "premajor", "rc")) // => "2.0.0-rc.0" + * ``` + * + * **Example** (Defining custom bump rules) + * + * ```ts import.meta.vitest + * import { SemVer } from "effect" + * + * // Always increment patch, even for pre-releases, and keep build metadata + * const customBump = (version: SemVer.SemVer, releaseType: SemVer.ReleaseType): SemVer.SemVer => + * releaseType === "patch" + * ? SemVer.make({ ...version, patch: version.patch + 1, prerelease: [] }) + * : SemVer.withBuild(SemVer.bump(version, releaseType), version.build) + * + * SemVer.format(customBump(SemVer.parseUnsafe("1.2.3-rc.1+b"), "patch")) // => "1.2.4+b" + * SemVer.format(customBump(SemVer.parseUnsafe("1.2.3-rc.1+b"), "minor")) // => "1.3.0+b" + * ``` + * + * @stability unstable + * @category transforming + * @since 4.0.6 + */ +export const bump: { + (releaseType: ReleaseType, identifier?: string): (self: SemVer) => SemVer + (self: SemVer, releaseType: ReleaseType, identifier?: string): SemVer +} = dual(isSemVerArgs, (self: SemVer, releaseType: ReleaseType, identifier?: string): SemVer => { + switch (releaseType) { + case "major": + return bumpMajor(self) + case "minor": + return bumpMinor(self) + case "patch": + return bumpPatch(self) + case "premajor": + return bumpPremajor(self, identifier) + case "preminor": + return bumpPreminor(self, identifier) + case "prepatch": + return bumpPrepatch(self, identifier) + case "prerelease": + return bumpPrerelease(self, identifier) + } +}) + +/** + * Classifies the change between two versions by release type. + * + * **Details** + * + * Follows node-semver's `diff`, so the result names the bump that produces the + * higher version from the lower one. Moving to a pre-release adds the `pre` + * prefix, and finalizing a pre-release reports the level it was released at: + * `1.0.0-1` to `1.0.0` is `"major"`, `1.1.0-1` to `1.1.0` is `"minor"`, and + * `1.2.3-1` to `1.2.3` is `"patch"`. The result does not depend on argument + * order. + * + * **Gotchas** + * + * Returns `None` for versions with the same precedence, including versions + * that differ only in build metadata. + * + * **Example** (Classifying upgrades) + * + * ```ts import.meta.vitest + * import { Option, SemVer } from "effect" + * + * SemVer.difference(SemVer.parseUnsafe("1.2.3"), SemVer.parseUnsafe("1.3.0")) // => Option.some("minor") + * SemVer.difference(SemVer.parseUnsafe("1.2.3"), SemVer.parseUnsafe("2.0.0-rc.1")) // => Option.some("premajor") + * SemVer.difference(SemVer.parseUnsafe("2.0.0-rc.1"), SemVer.parseUnsafe("2.0.0")) // => Option.some("major") + * SemVer.difference(SemVer.parseUnsafe("1.2.3+a"), SemVer.parseUnsafe("1.2.3+b")) // => Option.none() + * ``` + * + * @see {@link bump} for applying a release type to a version + * @stability unstable + * @category comparisons + * @since 4.0.6 + */ +export const difference: { + (that: SemVer): (self: SemVer) => Option.Option + (self: SemVer, that: SemVer): Option.Option +} = dual(2, (self: SemVer, that: SemVer): Option.Option => { + const comparison = Order(self, that) + if (comparison === 0) return Option.none() + const [low, high] = comparison < 0 ? [self, that] : [that, self] + const highIsPrerelease = high.prerelease.length > 0 + if (low.prerelease.length > 0 && !highIsPrerelease) { + if (low.minor === 0 && low.patch === 0) return Option.some("major") + if (sameCore(low, high)) return Option.some(low.patch === 0 ? "minor" : "patch") + } + if (self.major !== that.major) return Option.some(highIsPrerelease ? "premajor" : "major") + if (self.minor !== that.minor) return Option.some(highIsPrerelease ? "preminor" : "minor") + if (self.patch !== that.patch) return Option.some(highIsPrerelease ? "prepatch" : "patch") + return Option.some("prerelease") +}) diff --git a/packages/effect/src/SemVerRange.ts b/packages/effect/src/SemVerRange.ts new file mode 100644 index 000000000..bafa5352e --- /dev/null +++ b/packages/effect/src/SemVerRange.ts @@ -0,0 +1,703 @@ +/** + * npm-style version ranges over `SemVer` values. + * + * A `SemVerRange` keeps the syntax it was parsed from, such as `^1.2`, `~1.2.3`, + * `1.x`, or `1.2.3 - 2.3.4`. Matching follows node-semver's pre-release rules, + * except that wildcard alternatives preserve the union described in {@link matches}. + * + * @stability unstable + * @since 4.0.6 + */ +import * as Equal from "./Equal.ts" +import { dual } from "./Function.ts" +import * as Hash from "./Hash.ts" +import { type Inspectable, NodeInspectSymbol } from "./Inspectable.ts" +import * as Option from "./Option.ts" +import { type Pipeable, pipeArguments } from "./Pipeable.ts" +import { hasProperty } from "./Predicate.ts" +import * as Result from "./Result.ts" +import { + compareCore, + type Core, + fromInputUnsafe as versionFromInputUnsafe, + type Input as VersionInput, + invalid, + parseComponent, + prereleaseIdentifier, + sameCore, + type SemVer, + type SemVerError, + tryParse +} from "./SemVer.ts" + +const TypeId = "~effect/SemVerRange" + +/** + * Represents an npm-style version range. + * + * **Details** + * + * Ranges are opaque. {@link format} preserves parsed operators such as `^1.2` + * rather than expanding them into bounds. + * + * @see {@link parse} for decoding range strings + * @see {@link matches} for matching versions + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface SemVerRange extends Pipeable, Equal.Equal, Inspectable { + readonly [TypeId]: typeof TypeId +} + +interface PartialVersion { + // The components given before the first wildcard; empty for `*`. + readonly components: ReadonlyArray + // Only a full MAJOR.MINOR.PATCH may carry a pre-release. + readonly prerelease: ReadonlyArray +} + +type Operator = "=" | "<" | "<=" | ">" | ">=" | "~" | "^" + +type Comparator = + | { + readonly _tag: "Comparator" + readonly operator: Operator + readonly version: PartialVersion + } + | { + readonly _tag: "Hyphen" + readonly from: PartialVersion + readonly to: PartialVersion + } + +interface SemVerRangeImpl extends SemVerRange { + // Alternatives separated by `||`, each a set of comparators that must all match. + readonly sets: ReadonlyArray> + matcher?: (version: SemVer) => boolean + prereleaseMatcher?: (version: SemVer) => boolean +} + +/** + * Options controlling how pre-release versions match a range. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface MatchOptions { + /** + * Lets pre-release versions match any comparator set whose bounds contain + * them, and lowers wildcard lower bounds to include pre-releases. + * + * @default false + */ + readonly includePrerelease?: boolean | undefined +} + +const SemVerRangeProto: SemVerRange = { + [TypeId]: TypeId, + [Equal.symbol](this: SemVerRange, that: unknown): boolean { + return isSemVerRange(that) && format(this) === format(that) + }, + [Hash.symbol](this: SemVerRange): number { + return Hash.string(format(this)) + }, + toString(this: SemVerRange) { + return `SemVerRange(${format(this)})` + }, + toJSON(this: SemVerRange) { + return { _id: "SemVerRange", value: format(this) } + }, + [NodeInspectSymbol](this: SemVerRange) { + return this.toJSON() + }, + pipe() { + return pipeArguments(this, arguments) + } +} + +/** + * Checks whether a value is a `SemVerRange`. + * + * @stability unstable + * @category guards + * @since 4.0.6 + */ +export const isSemVerRange = (u: unknown): u is SemVerRange => hasProperty(u, TypeId) + +const isRangeArgs = (args: IArguments) => isSemVerRange(args[0]) + +const wildcard: PartialVersion = { components: [], prerelease: [] } +const operatorPattern = /^(<=|>=|<|>|=|~|\^)?(.*)$/ +const isWildcard = (part: string): boolean => part === "x" || part === "X" || part === "*" + +const componentLabels = ["major", "minor", "patch"] as const + +const parsePartial = (input: string): PartialVersion => { + if (input.length === 0) return invalid("expected a version after the operator") + if (input.includes("+")) { + return invalid(`build metadata is not allowed in ranges, received ${JSON.stringify(input)}`) + } + const dash = input.indexOf("-") + const parts = (dash === -1 ? input : input.slice(0, dash)).split(".") + if (parts.length > 3) return invalid(`expected at most MAJOR.MINOR.PATCH, received ${JSON.stringify(input)}`) + const components: Array = [] + for (let i = 0; i < parts.length; i++) { + if (isWildcard(parts[i])) continue + // A skipped wildcard leaves `components` shorter than the position reached. + if (components.length < i) { + return invalid(`a version component cannot follow a wildcard, received ${JSON.stringify(input)}`) + } + components.push(parseComponent(parts[i], componentLabels[i])) + } + if (dash === -1) return { components, prerelease: [] } + if (components.length < 3) { + return invalid(`a pre-release requires MAJOR.MINOR.PATCH, received ${JSON.stringify(input)}`) + } + return { components, prerelease: input.slice(dash + 1).split(".").map(prereleaseIdentifier) } +} + +const parseComparatorSet = (input: string): ReadonlyArray => { + const tokens = input.split(/\s+/).filter((token) => token.length > 0) + if (tokens.length === 0) return invalid("expected a comparator") + if (tokens.length === 3 && tokens[1] === "-") { + return [{ _tag: "Hyphen", from: parsePartial(tokens[0]), to: parsePartial(tokens[2]) }] + } + const comparators: Array = [] + for (let i = 0; i < tokens.length; i++) { + const [, operator, rest] = operatorPattern.exec(tokens[i])! + // Allow whitespace between an operator and its version, as in `>= 1.2.3`. + const version = rest.length === 0 && operator !== undefined && i + 1 < tokens.length ? tokens[++i] : rest + comparators.push({ _tag: "Comparator", operator: (operator ?? "=") as Operator, version: parsePartial(version) }) + } + return comparators +} + +const makeRange = (sets: ReadonlyArray>): SemVerRange => { + const self = Object.create(SemVerRangeProto) + self.sets = sets + return self +} + +const parseOrThrow = (input: string): SemVerRange => { + if (input.trim().length === 0) return any + return makeRange(input.split("||").map(parseComparatorSet)) +} + +/** + * Parses an npm-style version range. + * + * **Details** + * + * Supports: + * + * - comparators `=`, `<`, `<=`, `>`, `>=`, whitespace-separated as AND; + * - alternatives separated by `||`; + * - X-ranges such as `1.x`, `1.2.*`, `1`, and `*`; + * - tilde (`~1.2.3`), caret (`^1.2.3`), and hyphen (`1.2.3 - 2.3.4`) ranges. + * + * An empty string is equivalent to `*`. Invalid input fails with + * `SemVer.SemVerError`. + * + * **Gotchas** + * + * Versions must follow the strict SemVer grammar apart from wildcards, so a + * `v` prefix, leading zeros, build metadata, and empty `||` alternatives are + * rejected. + * + * **Example** (Parsing and matching a caret range) + * + * ```ts import.meta.vitest + * import { Result, SemVerRange } from "effect" + * + * const range = SemVerRange.parseUnsafe("^1.2.0 || >=3.0.0-rc.1") + * + * SemVerRange.matches(range, "1.9.0") // => true + * SemVerRange.matches(range, "2.0.0") // => false + * SemVerRange.matches(range, "3.0.0-rc.2") // => true + * Result.isFailure(SemVerRange.parse("^v1.2.0")) // => true + * ``` + * + * @see {@link parseUnsafe} for throwing on invalid input + * @see {@link format} for the inverse operation + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const parse = (input: string): Result.Result => + tryParse("SemVer range", input, parseOrThrow) + +/** + * Parses an npm-style version range and throws `SemVer.SemVerError` for + * invalid input. + * + * @see {@link parse} for non-throwing parsing + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const parseUnsafe = (input: string): SemVerRange => Result.getOrThrow(parse(input)) + +/** + * Values accepted by `fromInput`: a range or a range string. + * + * @see {@link fromInput} for converting inputs + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type Input = SemVerRange | string + +/** + * Converts a range or range string into a `SemVerRange`. + * + * **Details** + * + * Ranges are returned unchanged and strings are parsed like {@link parse}. + * Invalid strings fail with `SemVer.SemVerError`. + * + * @see {@link fromInputUnsafe} for throwing on invalid input + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const fromInput = (input: Input): Result.Result => + isSemVerRange(input) ? Result.succeed(input) : parse(input) + +/** + * Converts a range or range string into a `SemVerRange` and throws + * `SemVer.SemVerError` for invalid input. + * + * @see {@link fromInput} for non-throwing conversion + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const fromInputUnsafe = (input: Input): SemVerRange => Result.getOrThrow(fromInput(input)) + +const toPartial = (version: SemVer): PartialVersion => ({ + components: [version.major, version.minor, version.patch], + prerelease: version.prerelease +}) + +const comparator = (operator: Operator, version: VersionInput): Comparator => ({ + _tag: "Comparator", + operator, + version: toPartial(versionFromInputUnsafe(version)) +}) + +const fromComparator = (operator: Operator, version: VersionInput): SemVerRange => + makeRange([[comparator(operator, version)]]) + +/** + * The range matching every release, written `*`. + * + * **Gotchas** + * + * Like every range, `*` only matches pre-releases with `includePrerelease`. + * + * @stability unstable + * @category constants + * @since 4.0.6 + */ +export const any: SemVerRange = makeRange([[{ _tag: "Comparator", operator: "=", version: wildcard }]]) + +/** + * Creates a range matching exactly one version, written `1.2.3`. + * + * **Details** + * + * Build metadata is dropped because ranges cannot express it and it does not + * affect matching, so `1.2.3+a` and `1.2.3+b` both match. Throws `SemVerError` for an invalid `SemVer.Input`. + * + * **Example** (Building ranges from versions) + * + * ```ts import.meta.vitest + * import { SemVer, SemVerRange } from "effect" + * + * const version = SemVer.parseUnsafe("1.2.3+build.5") + * + * SemVerRange.format(SemVerRange.exact(version)) // => "1.2.3" + * SemVerRange.format(SemVerRange.greaterThanOrEqualTo(version)) // => ">=1.2.3" + * SemVerRange.format(SemVerRange.caret("1.2.3")) // => "^1.2.3" + * SemVerRange.matches(SemVerRange.greaterThan(version), "1.2.4") // => true + * ``` + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const exact = (version: VersionInput): SemVerRange => fromComparator("=", version) + +/** + * Creates a range matching versions with higher precedence, written `>1.2.3`. + * + * **Details** + * + * Build metadata is dropped. Throws `SemVerError` for an invalid `SemVer.Input`. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const greaterThan = (version: VersionInput): SemVerRange => fromComparator(">", version) + +/** + * Creates a range matching versions with at least the given precedence, written `>=1.2.3`. + * + * **Details** + * + * Build metadata is dropped. Throws `SemVerError` for an invalid `SemVer.Input`. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const greaterThanOrEqualTo = (version: VersionInput): SemVerRange => fromComparator(">=", version) + +/** + * Creates a range matching versions with lower precedence, written `<1.2.3`. + * + * **Details** + * + * Build metadata is dropped. Throws `SemVerError` for an invalid `SemVer.Input`. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const lessThan = (version: VersionInput): SemVerRange => fromComparator("<", version) + +/** + * Creates a range matching versions with at most the given precedence, written `<=1.2.3`. + * + * **Details** + * + * Build metadata is dropped. Throws `SemVerError` for an invalid `SemVer.Input`. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const lessThanOrEqualTo = (version: VersionInput): SemVerRange => fromComparator("<=", version) + +/** + * Creates a range matching versions compatible with a version, written `^1.2.3`. + * + * **Details** + * + * Allows changes that do not modify the left-most non-zero component: `^1.2.3` + * means `>=1.2.3 <2.0.0-0` and `^0.2.3` means `>=0.2.3 <0.3.0-0`. Build + * metadata is dropped. Throws `SemVerError` for an invalid `SemVer.Input`. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const caret = (version: VersionInput): SemVerRange => fromComparator("^", version) + +/** + * Creates a range matching patch-level changes of a version, written `~1.2.3`. + * + * **Details** + * + * `~1.2.3` means `>=1.2.3 <1.3.0-0`. Build metadata is dropped. + * Throws `SemVerError` for an invalid `SemVer.Input`. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const tilde = (version: VersionInput): SemVerRange => fromComparator("~", version) + +/** + * Creates a range matching versions within inclusive bounds, written + * `>=1.2.3 <=2.0.0`. + * + * **Details** + * + * Matches the same versions as `SemVer.between`, subject to the pre-release + * rule. Build metadata is dropped. Throws `SemVerError` for an invalid `SemVer.Input`. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const between = (options: { readonly minimum: VersionInput; readonly maximum: VersionInput }): SemVerRange => + makeRange([[comparator(">=", options.minimum), comparator("<=", options.maximum)]]) + +const formatPartial = (self: PartialVersion): string => { + if (self.components.length === 0) return "*" + const core = self.components.join(".") + return self.prerelease.length === 0 ? core : `${core}-${self.prerelease.join(".")}` +} + +const formatComparator = (self: Comparator): string => + self._tag === "Hyphen" + ? `${formatPartial(self.from)} - ${formatPartial(self.to)}` + : `${self.operator === "=" ? "" : self.operator}${formatPartial(self.version)}` + +/** + * Formats a range in canonical form. + * + * **Details** + * + * Operators and comparators are kept as parsed. Wildcard spellings are + * normalized by dropping trailing wildcard components (`1.x` and `1.*.*` + * format as `1`, and `x` as `*`), and an explicit `=` operator is omitted. + * + * @see {@link parse} for the inverse operation + * @stability unstable + * @category converting + * @since 4.0.6 + */ +export const format = (self: SemVerRange): string => + (self as SemVerRangeImpl).sets.map((set) => set.map(formatComparator).join(" ")).join(" || ") + +interface Bound extends Core { + readonly operator: "=" | "<" | "<=" | ">" | ">=" +} + +const bound = ( + operator: Bound["operator"], + [major = 0, minor = 0, patch = 0]: ReadonlyArray, + prerelease: ReadonlyArray +): Bound => ({ operator, major, minor, patch, prerelease }) + +// The expansions below follow node-semver's `Range` desugaring. `z` is the +// pre-release a wildcard lower bound carries: `[0]` when pre-releases are +// included, so `>=1.2` admits `1.2.0-alpha`, and empty otherwise. + +// Exclusive upper bound at the first pre-release of `core`, node-semver's `): Bound => bound("<", core, [0]) + +// The lowest version a partial covers. +const floor = (self: PartialVersion, z: ReadonlyArray): Bound => + bound(">=", self.components, self.components.length === 3 ? self.prerelease : z) + +// The first core above every version the first `precision` components cover. +const after = (components: ReadonlyArray, precision: number): ReadonlyArray => { + const next = components.slice(0, precision) + next[precision - 1]++ + return next +} + +const expandCaret = (self: PartialVersion, z: ReadonlyArray): ReadonlyArray => { + const n = self.components.length + if (n === 0) return [] + // The first non-zero component may not change; `^0.0.x` pins the patch. + const precision = self.components.findIndex((component) => component !== 0) + 1 || n + return [floor(self, z), below(after(self.components, precision))] +} + +const expandTilde = (self: PartialVersion, z: ReadonlyArray): ReadonlyArray => { + const n = self.components.length + if (n === 0) return [] + return [floor(self, z), below(after(self.components, Math.min(n, 2)))] +} + +const expandXRange = ( + operator: Bound["operator"], + self: PartialVersion, + z: ReadonlyArray +): ReadonlyArray => { + const n = self.components.length + if (n === 3) return [bound(operator, self.components, self.prerelease)] + if (n === 0) return operator === "<" || operator === ">" ? [below([0, 0, 0])] : [] + switch (operator) { + case "=": + return [floor(self, z), below(after(self.components, n))] + case ">": + return [bound(">=", after(self.components, n), z)] + case ">=": + return [floor(self, z)] + case "<=": + return [below(after(self.components, n))] + case "<": + return [below(self.components)] + } +} + +const expand = (self: Comparator, includePrerelease: boolean): ReadonlyArray => { + const z = includePrerelease ? [0] : [] + if (self._tag === "Hyphen") { + const { from, to } = self + // Unlike `>=`, a full hyphen lower bound without a pre-release also takes `z`. + const lower = from.components.length === 0 + ? [] + : [bound(">=", from.components, from.prerelease.length > 0 ? from.prerelease : z)] + return [...lower, ...expandXRange("<=", to, z)] + } + switch (self.operator) { + case "^": + return expandCaret(self.version, z) + case "~": + return expandTilde(self.version, z) + default: + return expandXRange(self.operator, self.version, z) + } +} + +const testBound = (self: Bound, version: SemVer): boolean => { + const result = compareCore(version, self) + switch (self.operator) { + case "=": + return result === 0 + case "<": + return result < 0 + case "<=": + return result <= 0 + case ">": + return result > 0 + case ">=": + return result >= 0 + } +} + +const compile = (self: SemVerRangeImpl, includePrerelease: boolean): (version: SemVer) => boolean => { + const sets = self.sets.map((set) => set.flatMap((comparator) => expand(comparator, includePrerelease))) + return (version) => + sets.some((bounds) => + bounds.every((bound) => testBound(bound, version)) && + (includePrerelease || version.prerelease.length === 0 || + bounds.some((bound) => bound.prerelease.length > 0 && sameCore(bound, version))) + ) +} + +const matcher = (range: SemVerRange, options: MatchOptions | undefined): (version: SemVer) => boolean => { + const self = range as SemVerRangeImpl + return options?.includePrerelease === true + ? (self.prereleaseMatcher ??= compile(self, true)) + : (self.matcher ??= compile(self, false)) +} + +/** + * Checks whether a range matches a version. + * + * **Details** + * + * Partial versions, tilde, caret, and hyphen ranges expand to bounds following + * node-semver. For example, `^1.2.3` means `>=1.2.3 <2.0.0-0`, `~1.2` means + * `>=1.2.0 <1.3.0-0`, and `<=1.2` means `<1.3.0-0`. Throws `SemVerError` when + * `version` is an invalid `SemVer.Input`. + * + * **Gotchas** + * + * A pre-release version only matches a comparator set that contains a + * comparator with a pre-release on the same major, minor, and patch. So + * `^1.2.3-beta.1` matches `1.2.3-beta.2` but not `1.3.0-rc.1`, and `*` does + * not match any pre-release. Set `includePrerelease` to disable this rule. + * + * Alternatives separated by `||` always form a union. Unlike node-semver, a `*` + * alternative does not discard pre-releases admitted by another alternative. + * + * **Example** (Matching pre-releases) + * + * ```ts import.meta.vitest + * import { SemVerRange } from "effect" + * + * const range = SemVerRange.parseUnsafe("^1.2.3-beta.1") + * + * SemVerRange.matches(range, "1.2.3-beta.2") // => true + * SemVerRange.matches(range, "1.3.0-rc.1") // => false + * SemVerRange.matches(range, "1.3.0-rc.1", { includePrerelease: true }) // => true + * ``` + * + * **Example** (Filtering versions) + * + * ```ts import.meta.vitest + * import { SemVer, SemVerRange } from "effect" + * + * const range = SemVerRange.parseUnsafe("~1.2") + * const versions = ["1.1.0", "1.2.0", "1.2.7", "1.3.0"].map(SemVer.parseUnsafe) + * + * versions.filter((version) => SemVerRange.matches(range, version)).map(SemVer.format) // => ["1.2.0", "1.2.7"] + * ``` + * + * @see {@link maxSatisfying} for selecting the highest matching version + * @stability unstable + * @category predicates + * @since 4.0.6 + */ +export const matches: { + (version: VersionInput, options?: MatchOptions): (self: SemVerRange) => boolean + (self: SemVerRange, version: VersionInput, options?: MatchOptions): boolean +} = dual( + isRangeArgs, + (self: SemVerRange, version: VersionInput, options?: MatchOptions): boolean => + matcher(self, options)(versionFromInputUnsafe(version)) +) + +const select = ( + self: SemVerRange, + versions: Iterable, + options: MatchOptions | undefined, + better: -1 | 1 +): Option.Option => { + const test = matcher(self, options) + let selected: SemVer | undefined + for (const input of versions) { + const version = versionFromInputUnsafe(input) + if (test(version) && (selected === undefined || compareCore(version, selected) === better)) { + selected = version + } + } + return Option.fromUndefinedOr(selected) +} + +/** + * Returns the version with the highest precedence that a range matches. + * + * **Details** + * + * Returns `None` when no version matches. When several versions share the + * highest precedence, the first one is returned. Throws `SemVerError` when a + * version is an invalid `SemVer.Input`. + * + * **Example** (Selecting the newest compatible version) + * + * ```ts import.meta.vitest + * import { Option, SemVer, SemVerRange } from "effect" + * + * const range = SemVerRange.parseUnsafe("^1.2.0") + * + * SemVerRange.maxSatisfying(range, ["1.2.0", "1.4.1", "2.0.0", "1.5.0-rc.1"]).pipe(Option.map(SemVer.format)) // => Option.some("1.4.1") + * ``` + * + * @see {@link minSatisfying} for the lowest matching version + * @stability unstable + * @category filtering + * @since 4.0.6 + */ +export const maxSatisfying: { + (versions: Iterable, options?: MatchOptions): (self: SemVerRange) => Option.Option + (self: SemVerRange, versions: Iterable, options?: MatchOptions): Option.Option +} = dual( + isRangeArgs, + (self: SemVerRange, versions: Iterable, options?: MatchOptions): Option.Option => + select(self, versions, options, 1) +) + +/** + * Returns the version with the lowest precedence that a range matches. + * + * **Details** + * + * Returns `None` when no version matches. When several versions share the + * lowest precedence, the first one is returned. Throws `SemVerError` when a + * version is an invalid `SemVer.Input`. + * + * @see {@link maxSatisfying} for the highest matching version + * @stability unstable + * @category filtering + * @since 4.0.6 + */ +export const minSatisfying: { + (versions: Iterable, options?: MatchOptions): (self: SemVerRange) => Option.Option + (self: SemVerRange, versions: Iterable, options?: MatchOptions): Option.Option +} = dual( + isRangeArgs, + (self: SemVerRange, versions: Iterable, options?: MatchOptions): Option.Option => + select(self, versions, options, -1) +) diff --git a/repos/effect/packages/effect/src/Semaphore.ts b/packages/effect/src/Semaphore.ts similarity index 100% rename from repos/effect/packages/effect/src/Semaphore.ts rename to packages/effect/src/Semaphore.ts diff --git a/repos/effect/packages/effect/src/Sink.ts b/packages/effect/src/Sink.ts similarity index 99% rename from repos/effect/packages/effect/src/Sink.ts rename to packages/effect/src/Sink.ts index 138e26d8e..4f6059286 100644 --- a/repos/effect/packages/effect/src/Sink.ts +++ b/packages/effect/src/Sink.ts @@ -1215,7 +1215,9 @@ export const take = (n: number): Sink, In, In> => { return upstream.pipe( Effect.flatMap((arr) => { if (taken.length + arr.length <= count) { - taken.push(...arr) + for (let i = 0; i < arr.length; i++) { + taken.push(arr[i]) + } if (taken.length === count) { return Cause.done() } @@ -1644,7 +1646,9 @@ export const count: Sink = reduceArray(() => 0, (s, arr) => s + */ export const collect = (): Sink, In> => reduceArray(Arr.empty, (s, arr) => { - s.push(...arr) + for (let i = 0; i < arr.length; i++) { + s.push(arr[i]) + } return s }) diff --git a/repos/effect/packages/effect/src/StandardSchema.ts b/packages/effect/src/StandardSchema.ts similarity index 100% rename from repos/effect/packages/effect/src/StandardSchema.ts rename to packages/effect/src/StandardSchema.ts diff --git a/repos/effect/packages/effect/src/Stdio.ts b/packages/effect/src/Stdio.ts similarity index 100% rename from repos/effect/packages/effect/src/Stdio.ts rename to packages/effect/src/Stdio.ts diff --git a/repos/effect/packages/effect/src/Stream.ts b/packages/effect/src/Stream.ts similarity index 98% rename from repos/effect/packages/effect/src/Stream.ts rename to packages/effect/src/Stream.ts index e102f5f57..1050a5b2c 100644 --- a/repos/effect/packages/effect/src/Stream.ts +++ b/packages/effect/src/Stream.ts @@ -823,8 +823,13 @@ export const sync = (evaluate: LazyArg): Stream => fromChannel(Channel. * @category constructors * @since 2.0.0 */ -export const suspend = (stream: LazyArg>): Stream => - fromChannel(Channel.suspend(() => stream().channel)) +export const suspend = (stream: LazyArg>): Stream => { + const suspended = fromChannel(Channel.suspend(() => stream().channel)) + suspendedStreams.set(suspended, stream) + return suspended +} + +const suspendedStreams = new WeakMap, LazyArg>>() /** * Terminates with the specified error. @@ -1259,9 +1264,10 @@ export const fromPubSubTake = (pubsub: PubSub.PubSub>): St * * **Details** * - * The stream reads from a `ReadableStreamDefaultReader`, maps read failures - * with `onError`, and closes the reader when the stream finalizes. By default - * the reader is canceled; set `releaseLockOnEnd` to release the lock instead. + * The stream reads from a `ReadableStreamDefaultReader` and maps read failures + * with `onError`. Finalization cancels the reader and releases its lock, or + * only releases the lock when `releaseLockOnEnd` is set. A rejected cancel + * hook becomes a defect. * * **Example** (Creating a stream from a ReadableStream) * @@ -1433,6 +1439,11 @@ export interface EventListener { /** * Creates a stream from an event listener. * + * **Gotchas** + * + * Event listeners cannot backpressure the source. When a bounded `bufferSize` + * is full, additional events are dropped rather than suspending the listener. + * * **Example** (Creating a stream from an event listener) * * ```ts import.meta.vitest @@ -1800,6 +1811,11 @@ export const as: { /** * Maps both the failure and success channels of a stream. * + * **Details** + * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * * **Example** (Mapping both the failure and success channels of a stream) * * ```ts import.meta.vitest @@ -2981,14 +2997,30 @@ export const concat: { // a single layer instead of one layer per call const concatParts = new WeakMap, readonly [Stream, Stream]>() -// Yields the channel of each non-`concat` stream in a chain, in order +// Yields the channel of each non-`concat` stream in a chain, in order. A +// `suspend` part is evaluated when it is reached and its result joins the chain function* concatChannels(self: Stream, that: Stream) { const stack = [that, self] while (stack.length > 0) { const stream = stack.pop()! const parts = concatParts.get(stream) - if (parts === undefined) yield stream.channel - else stack.push(parts[1], parts[0]) + if (parts !== undefined) { + stack.push(parts[1], parts[0]) + continue + } + const evaluate = suspendedStreams.get(stream) + if (evaluate === undefined) { + yield stream.channel + continue + } + // Evaluated here rather than in an effect, so retrying an interrupted pull + // cannot evaluate it twice. `Channel.empty` keeps a yield point + try { + stack.push(evaluate()) + } catch (defect) { + stack.push(die(defect)) + } + yield Channel.empty } } @@ -3453,24 +3485,23 @@ export const zipWith: { left: Stream, right: Stream, f: (left: AL, right: AR) => A -): Stream => zipWithArray(left, right, zipArrays(f))) - -const zipArrays = ( - f: (left: AL, right: AR) => A -) => -( - leftArr: Arr.NonEmptyReadonlyArray, - rightArr: Arr.NonEmptyReadonlyArray -) => { - const minLength = Math.min(leftArr.length, rightArr.length) - const result: Arr.NonEmptyArray = [] as any - - for (let i = 0; i < minLength; i++) { - result.push(f(leftArr[i], rightArr[i])) - } - - return [result, leftArr.slice(minLength), rightArr.slice(minLength)] as const -} +): Stream => + suspend(() => { + // Track consumed elements with cursors instead of slicing leftovers + let leftIndex = 0 + let rightIndex = 0 + return zipWithArray(left, right, (leftArr, rightArr) => { + const length = Math.min(leftArr.length - leftIndex, rightArr.length - rightIndex) + const output = Arr.makeBy(length, (i) => f(leftArr[leftIndex + i], rightArr[rightIndex + i])) + leftIndex += length + rightIndex += length + const leftDone = leftIndex === leftArr.length + const rightDone = rightIndex === rightArr.length + if (leftDone) leftIndex = 0 + if (rightDone) rightIndex = 0 + return [output, leftDone ? [] : leftArr, rightDone ? [] : rightArr] + }) + })) /** * Zips two streams by applying a function to non-empty arrays of elements. @@ -3554,31 +3585,23 @@ export const zipWithArray: { ] }) - type State = - | { _tag: "PullBoth" } - | { _tag: "PullLeft"; rightArray: Arr.NonEmptyReadonlyArray } - | { _tag: "PullRight"; leftArray: Arr.NonEmptyReadonlyArray } - let state: State = { _tag: "PullBoth" } + let leftArray: Arr.NonEmptyReadonlyArray | undefined + let rightArray: Arr.NonEmptyReadonlyArray | undefined const pull: Effect.Effect< Arr.NonEmptyReadonlyArray, EL | ER | Cause.Done, RL | RR > = Effect.gen(function*() { - const [left, right] = state._tag === "PullBoth" - ? yield* pullBoth - : state._tag === "PullLeft" - ? [yield* pullLeft, state.rightArray] - : [state.leftArray, yield* pullRight] - const result = f(left, right) - if (Arr.isReadonlyArrayNonEmpty(result[1])) { - state = { _tag: "PullRight", leftArray: result[1] } - } else if (Arr.isReadonlyArrayNonEmpty(result[2])) { - state = { _tag: "PullLeft", rightArray: result[2] } - } else { - state = { _tag: "PullBoth" } - } - return result[0] + const [left, right] = leftArray + ? [leftArray, rightArray ?? (yield* pullRight)] + : rightArray + ? [yield* pullLeft, rightArray] + : yield* pullBoth + const [output, leftoverLeft, leftoverRight] = f(left, right) + leftArray = Arr.isReadonlyArrayNonEmpty(leftoverLeft) ? leftoverLeft : undefined + rightArray = Arr.isReadonlyArrayNonEmpty(leftoverRight) ? leftoverRight : undefined + return output }) return pull @@ -3657,15 +3680,7 @@ export const zipLeft: { ( left: Stream, right: Stream - ): Stream => - zipWithArray(left, right, (leftArr, rightArr) => { - const minLength = Math.min(leftArr.length, rightArr.length) - const output = leftArr.slice(0, minLength) as Arr.NonEmptyArray - const leftoverLeft = leftArr.slice(minLength) - const leftoverRight = rightArr.slice(minLength) - - return [output, leftoverLeft, leftoverRight] as const - }) + ): Stream => zipWith(left, right, (a) => a) ) /** @@ -3699,15 +3714,7 @@ export const zipRight: { ( left: Stream, right: Stream - ): Stream => - zipWithArray(left, right, (leftArr, rightArr) => { - const minLength = Math.min(leftArr.length, rightArr.length) - const output = rightArr.slice(0, minLength) as Arr.NonEmptyArray - const leftoverLeft = leftArr.slice(minLength) - const leftoverRight = rightArr.slice(minLength) - - return [output, leftoverLeft, leftoverRight] as const - }) + ): Stream => zipWith(left, right, (_, a) => a) ) /** @@ -4905,6 +4912,12 @@ export const catchCause: { * * Typed failures and interruptions are not caught. * + * **Gotchas** + * + * `catchDefect` recovers any cause that contains a defect, including causes + * that also contain typed errors. Typed error handlers such as `catch` skip + * those causes, so defects always take precedence. + * * **Example** (Recovering from a defect) * * ```ts import.meta.vitest @@ -5002,6 +5015,13 @@ export { /** * Switches over to the stream produced by the provided function if this one fails. * + * **Gotchas** + * + * Defects take precedence over typed errors. If the cause also contains a + * defect, the handler does not run and the failure is propagated with every + * typed error converted to a defect. An interruption alongside a typed error + * does not prevent recovery. + * * **Example** (Catching stream failures) * * ```ts import.meta.vitest @@ -5218,6 +5238,12 @@ export const tapDefect: { * stream. Non-matching failures propagate downstream, so the error type is * preserved unless the filter narrows it. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Catching matching failures) * * ```ts import.meta.vitest @@ -5260,7 +5286,7 @@ export const catchIf: { ) => Stream, E2 | E3 | (A3 extends unassigned ? E : never), R | R2 | R3> ( self: Stream, - refinement: Refinement, + refinement: Refinement, EB>, f: (e: EB) => Stream, orElse?: ((e: Exclude) => Stream) | undefined ): Stream, E2 | E3 | (A3 extends unassigned ? Exclude : never), R | R2 | R3> @@ -5309,6 +5335,12 @@ export const catchIf: { * Successful filter results are passed to `f`. Failed filter results go to * `orElse` when provided; otherwise the filter failure is re-failed. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * @see {@link catchIf} for predicate or refinement based recovery * @see {@link catchTag} for `_tag` based recovery from one tagged error * @see {@link catchTags} for `_tag` based recovery from multiple tagged errors @@ -5368,6 +5400,12 @@ export const catchFilter: { * Use when you need to handle a specific error case from a stream whose error * type is a tagged union with a readonly `_tag` field. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Catching tagged failures) * * ```ts import.meta.vitest @@ -5476,6 +5514,12 @@ export const catchTag: { /** * Switches to a recovery stream based on matching `_tag` handlers. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Catching tagged failures with handlers) * * ```ts import.meta.vitest @@ -5598,6 +5642,12 @@ export const catchTags: { * * The handler receives the unwrapped reason. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Catching a tagged error reason) * * ```ts import.meta.vitest @@ -5733,6 +5783,12 @@ export const catchReason: { /** * Catches multiple reasons within a tagged error using an object of handlers. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Catching tagged error reasons) * * ```ts import.meta.vitest @@ -5961,6 +6017,11 @@ export const unwrapReason: { /** * Transforms the errors emitted by this stream using `f`. * + * **Details** + * + * Maps every typed error in the cause, preserving defects, interrupts and + * reason order. Unlike typed recovery, mapping does not convert errors to defects. + * * **Example** (Mapping stream errors) * * ```ts import.meta.vitest @@ -6135,6 +6196,12 @@ export const orElseIfEmpty: { /** * Returns a stream that emits a fallback value when this stream fails. * + * **Gotchas** + * + * If the cause contains a defect, the handler is skipped and every typed error + * is converted to a defect, preserving reason order and interrupts. An + * interruption alongside a typed error does not prevent recovery. + * * **Example** (Recovering with a fallback value) * * ```ts import.meta.vitest @@ -6208,6 +6275,13 @@ export const orDie = (self: Stream): Stream => fr * The `log` option controls whether the failure is logged before the stream * terminates. * + * **Gotchas** + * + * Defects take precedence over typed errors. If the cause also contains a + * defect, the failure is not ignored and is propagated with every typed error + * converted to a defect. An interruption alongside a typed error does not + * prevent the error from being ignored. + * * **Example** (Ignoring stream failures) * * ```ts import.meta.vitest @@ -6325,6 +6399,12 @@ export const ignoreCause: < * The schedule is reset as soon as the first element passes through the * stream again. * + * **Gotchas** + * + * Defects take precedence over typed errors. A failure whose cause contains a + * defect is not retried and is propagated with every typed error converted to a + * defect. A typed error alongside an interruption is retried. + * * **Example** (Retrying stream failures) * * ```ts import.meta.vitest @@ -7825,7 +7905,9 @@ export const mapAccum: { for (let index = 0; index < arr.length; index++) { const [newState, values] = f(state, arr[index]) state = newState - acc.push(...values) + for (let i = 0; i < values.length; i++) { + acc.push(values[i]) + } } return [state, Arr.isArrayNonEmpty(acc) ? Arr.of(acc) : emptyArr] }, @@ -9807,10 +9889,10 @@ export const decodeText: < (self: Stream, options?: { readonly encoding?: string | undefined }): Stream => - suspend(() => { - const decoder = new TextDecoder(options?.encoding) - return map(self, (chunk) => decoder.decode(chunk, { stream: true })) - }) + self.channel.pipe( + Channel.pipeTo(Channel.decodeText(options?.encoding)), + fromChannel + ) ) /** @@ -10409,7 +10491,8 @@ export const ensuring: { /** * Provides a layer or context to the stream, removing the corresponding * service requirements. Use `options.local` to build the layer every time; by - * default, layers are shared between provide calls. + * default, a layer already built by an enclosing provide or runtime is reused; + * sibling provide calls build their own copies. * * **Example** (Providing stream requirements) * @@ -11527,7 +11610,7 @@ export const mkArrayBuffer = (self: Stream): Effect.Effe * @category destructors * @since 4.0.0 */ -export const mkUint8Array = (self: Stream): Effect.Effect => +export const mkUint8Array = (self: Stream): Effect.Effect, E, R> => Channel.mkUint8Array(self.channel) /** @@ -11597,15 +11680,18 @@ export const toReadableStreamWith: { return new ReadableStream({ start(controller) { fiber = Effect.runFork(Effect.provideContext( - runForEachArray(self, (chunk) => - latch.whenOpen(Effect.sync(() => { - latch.closeUnsafe() - for (let i = 0; i < chunk.length; i++) { - controller.enqueue(chunk[i]) - } - currentResolve!() - currentResolve = undefined - }))), + Channel.runForEach( + Channel.transformPull(self.channel, (pull) => Effect.succeed(latch.whenOpen(pull))), + (chunk) => + Effect.sync(() => { + latch.closeUnsafe() + for (let i = 0; i < chunk.length; i++) { + controller.enqueue(chunk[i]) + } + currentResolve!() + currentResolve = undefined + }) + ), context )) fiber.addObserver((exit) => { diff --git a/repos/effect/packages/effect/src/String.ts b/packages/effect/src/String.ts similarity index 98% rename from repos/effect/packages/effect/src/String.ts rename to packages/effect/src/String.ts index b4bb0117f..531d23271 100644 --- a/repos/effect/packages/effect/src/String.ts +++ b/packages/effect/src/String.ts @@ -200,7 +200,7 @@ export const toUpperCase = (self: S): Uppercase => self.toU export const toLowerCase = (self: T): Lowercase => self.toLowerCase() as Lowercase /** - * Capitalizes the first character of a string. + * Capitalizes the first character (Unicode code point) of a string. * * **Example** (Capitalizing a string) * @@ -209,6 +209,7 @@ export const toLowerCase = (self: T): Lowercase => self.toL * * pipe("abc", String.capitalize) // => "Abc" * String.capitalize("hello") // => "Hello" + * String.capitalize("𐐨foo") // => "𐐀foo" * ``` * * @stability stable @@ -216,13 +217,12 @@ export const toLowerCase = (self: T): Lowercase => self.toL * @since 2.0.0 */ export const capitalize = (self: T): Capitalize => { - if (self.length === 0) return self as Capitalize - - return (toUpperCase(self[0]) + self.slice(1)) as Capitalize + const [first = ""] = self + return (toUpperCase(first) + self.slice(first.length)) as Capitalize } /** - * Uncapitalizes the first character of a string. + * Uncapitalizes the first character (Unicode code point) of a string. * * **Example** (Uncapitalizing a string) * @@ -231,6 +231,7 @@ export const capitalize = (self: T): Capitalize => { * * pipe("ABC", String.uncapitalize) // => "aBC" * String.uncapitalize("Hello") // => "hello" + * String.uncapitalize("𐐀FOO") // => "𐐨FOO" * ``` * * @stability stable @@ -238,9 +239,8 @@ export const capitalize = (self: T): Capitalize => { * @since 2.0.0 */ export const uncapitalize = (self: T): Uncapitalize => { - if (self.length === 0) return self as Uncapitalize - - return (toLowerCase(self[0]) + self.slice(1)) as Uncapitalize + const [first = ""] = self + return (toLowerCase(first) + self.slice(first.length)) as Uncapitalize } /** diff --git a/repos/effect/packages/effect/src/Struct.ts b/packages/effect/src/Struct.ts similarity index 100% rename from repos/effect/packages/effect/src/Struct.ts rename to packages/effect/src/Struct.ts diff --git a/repos/effect/packages/effect/src/SubscriptionRef.ts b/packages/effect/src/SubscriptionRef.ts similarity index 100% rename from repos/effect/packages/effect/src/SubscriptionRef.ts rename to packages/effect/src/SubscriptionRef.ts diff --git a/repos/effect/packages/effect/src/Symbol.ts b/packages/effect/src/Symbol.ts similarity index 100% rename from repos/effect/packages/effect/src/Symbol.ts rename to packages/effect/src/Symbol.ts diff --git a/repos/effect/packages/effect/src/SynchronizedRef.ts b/packages/effect/src/SynchronizedRef.ts similarity index 100% rename from repos/effect/packages/effect/src/SynchronizedRef.ts rename to packages/effect/src/SynchronizedRef.ts diff --git a/repos/effect/packages/effect/src/Take.ts b/packages/effect/src/Take.ts similarity index 100% rename from repos/effect/packages/effect/src/Take.ts rename to packages/effect/src/Take.ts diff --git a/repos/effect/packages/effect/src/Terminal.ts b/packages/effect/src/Terminal.ts similarity index 100% rename from repos/effect/packages/effect/src/Terminal.ts rename to packages/effect/src/Terminal.ts diff --git a/packages/effect/src/TextDiff.ts b/packages/effect/src/TextDiff.ts new file mode 100644 index 000000000..3c651c964 --- /dev/null +++ b/packages/effect/src/TextDiff.ts @@ -0,0 +1,766 @@ +/** + * The `TextDiff` module computes edit scripts for arrays and text, applies and + * inverts them, and formats line differences as unified diffs. + * + * Scripts are computed with Myers' O(ND) algorithm in linear space, after + * trimming common prefixes and suffixes. Scripts are minimal unless the + * optional `maxEdits` search cap is exceeded. Word diffs, semantic cleanup + * and collaborative editing protocols are out of scope. + * + * @stability experimental + * @since 4.0.6 + */ +import * as Data from "./Data.ts" +import * as Equal from "./Equal.ts" +import { hasProperty } from "./Predicate.ts" +import * as Result from "./Result.ts" + +const TextDiffErrorTypeId = "~effect/TextDiff/TextDiffError" + +/** + * One run of an edit script. + * + * **Details** + * + * `Equal` runs carry items kept from `before`, `Delete` runs carry items + * removed from `before`, and `Insert` runs carry items added from `after`. + * Scripts produced by this module never contain empty runs or two adjacent + * runs with the same tag, and within each change the `Delete` run comes before + * the `Insert` run. + * + * @see {@link diff} to compute an edit script + * @stability experimental + * @category models + * @since 4.0.6 + */ +export type Edit = EditEqual | EditInsert | EditDelete + +/** + * Run of items present in both sequences. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface EditEqual { + readonly _tag: "Equal" + readonly items: ReadonlyArray +} + +/** + * Run of items present only in the `after` sequence. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface EditInsert { + readonly _tag: "Insert" + readonly items: ReadonlyArray +} + +/** + * Run of items present only in the `before` sequence. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface EditDelete { + readonly _tag: "Delete" + readonly items: ReadonlyArray +} + +/** + * Error returned when an edit script does not fit the input it is applied to. + * + * **Example** (Rejecting a script made for other input) + * + * ```ts import.meta.vitest + * import { Result, TextDiff } from "effect" + * + * const edits = TextDiff.diff([1, 2], [1]) + * + * Result.mapError(TextDiff.apply([1], edits), TextDiff.isTextDiffError) // => Result.fail(true) + * ``` + * + * @see {@link apply} and {@link applyText} for the operations that return it + * @see {@link isTextDiffError} for narrowing unknown values + * @stability experimental + * @category errors + * @since 4.0.6 + */ +export class TextDiffError extends Data.TaggedError("TextDiffError")<{ + readonly message: string +}> { + readonly [TextDiffErrorTypeId]: typeof TextDiffErrorTypeId = TextDiffErrorTypeId +} + +/** + * Checks whether a value is a `TextDiffError`. + * + * @stability experimental + * @category guards + * @since 4.0.6 + */ +export const isTextDiffError = (u: unknown): u is TextDiffError => hasProperty(u, TextDiffErrorTypeId) + +/** + * Creates a function that computes a minimal edit script between two arrays + * using a custom equivalence. + * + * **When to use** + * + * Use when items should match by key or by a structural comparison other than + * `Equal.equals`. + * + * **Details** + * + * Unless the `maxEdits` cap is exceeded, the script has the + * fewest possible inserted plus deleted items. `Equal` runs carry the items + * from `before`, not necessarily the exact values from `after`. + * + * **Gotchas** + * + * Without `maxEdits`, work grows with the product of the input size and the + * number of differences, which is quadratic for unrelated inputs. Set + * `maxEdits` for large or untrusted inputs. + * + * **Example** (Matching items by id) + * + * ```ts import.meta.vitest + * import { TextDiff } from "effect" + * + * const diffById = TextDiff.diffWith<{ readonly id: number }>((a, b) => a.id === b.id) + * + * diffById([{ id: 1 }, { id: 2 }], [{ id: 2 }]) // => [{ _tag: "Delete", items: [{ id: 1 }] }, { _tag: "Equal", items: [{ id: 2 }] }] + * ``` + * + * @see {@link diff} for the `Equal.equals` variant + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const diffWith = (isEquivalent: (self: A, that: A) => boolean) => +( + before: ReadonlyArray, + after: ReadonlyArray, + options?: { + /** + * Largest number of inserted plus deleted items searched for in each + * changed region. Common prefixes, suffixes and matching runs are free. + * When a region needs more edits, the search splits it at the furthest + * point it reached and continues on the rest, and a part that still cannot + * be matched becomes a deletion followed by an insertion. The script stays + * valid, and is minimal whenever the whole diff needs at most `maxEdits` + * edits. For a finite cap, work is O((N + M) * (maxEdits + 1)). + * + * @default Infinity + */ + readonly maxEdits?: number | undefined + } +): Array> => editScript(before, after, (i, j) => isEquivalent(before[i], after[j]), options?.maxEdits) + +/** + * Computes a minimal edit script between two arrays, comparing items with + * `Equal.equals`. + * + * **Details** + * + * Unless the `maxEdits` cap is exceeded, the script has the + * fewest possible inserted plus deleted items. The choice is deterministic, + * and each change lists its deletions before its insertions. + * + * **Gotchas** + * + * Without `maxEdits`, work is quadratic for unrelated inputs. Set `maxEdits` + * when diffing large or untrusted arrays. + * + * **Example** (Diffing two arrays) + * + * ```ts import.meta.vitest + * import { TextDiff } from "effect" + * + * TextDiff.diff([1, 2, 3, 4], [1, 3, 4, 5]) // => [{ _tag: "Equal", items: [1] }, { _tag: "Delete", items: [2] }, { _tag: "Equal", items: [3, 4] }, { _tag: "Insert", items: [5] }] + * ``` + * + * @see {@link diffWith} to compare items with a custom equivalence + * @see {@link apply} to replay a script + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const diff = ( + before: ReadonlyArray, + after: ReadonlyArray, + options?: { + /** + * Largest number of inserted plus deleted items searched for in each + * changed region. Common prefixes, suffixes and matching runs are free. + * When a region needs more edits, the search splits it at the furthest + * point it reached and continues on the rest, and a part that still cannot + * be matched becomes a deletion followed by an insertion. The script stays + * valid, and is minimal whenever the whole diff needs at most `maxEdits` + * edits. For a finite cap, work is O((N + M) * (maxEdits + 1)). + * + * @default Infinity + */ + readonly maxEdits?: number | undefined + } +): Array> => diffWith(Equal.equals)(before, after, options) + +/** + * Computes a minimal line-level edit script between two strings. + * + * **Details** + * + * Each line keeps its `\n` terminator, so `\n` versus `\r\n` endings and a + * missing final newline are reported as changes. Joining the items of a run + * gives the exact text it covers. + * + * **Gotchas** + * + * Without `maxEdits`, work is quadratic for unrelated inputs. Set `maxEdits` + * when diffing large or untrusted text. + * + * **Example** (Diffing lines) + * + * ```ts import.meta.vitest + * import { TextDiff } from "effect" + * + * TextDiff.lines("a\nb\n", "a\nc\n") // => [{ _tag: "Equal", items: ["a\n"] }, { _tag: "Delete", items: ["b\n"] }, { _tag: "Insert", items: ["c\n"] }] + * ``` + * + * @see {@link codePoints} for a code point diff + * @see {@link unified} to format a line diff + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const lines = (before: string, after: string, options?: { + /** + * Largest number of inserted plus deleted items searched for in each + * changed region. Common prefixes, suffixes and matching runs are free. + * When a region needs more edits, the search splits it at the furthest + * point it reached and continues on the rest, and a part that still cannot + * be matched becomes a deletion followed by an insertion. The script stays + * valid, and is minimal whenever the whole diff needs at most `maxEdits` + * edits. For a finite cap, work is O((N + M) * (maxEdits + 1)). + * + * @default Infinity + */ + readonly maxEdits?: number | undefined +}): Array> => diffStrings(splitLines(before), splitLines(after), options?.maxEdits) + +/** + * Computes a minimal code point edit script between two strings. + * + * **Details** + * + * Items are Unicode code points, so a surrogate pair is never split between + * runs. Grapheme clusters, such as an emoji with modifiers or a letter with + * combining marks, can still be split. The UTF-16 length of a run is the sum + * of its items' lengths. + * + * **Gotchas** + * + * Without `maxEdits`, work is quadratic for unrelated inputs. Set `maxEdits` + * when diffing large or untrusted text. + * + * **Example** (Diffing code points) + * + * ```ts import.meta.vitest + * import { TextDiff } from "effect" + * + * TextDiff.codePoints("cat", "cut") // => [{ _tag: "Equal", items: ["c"] }, { _tag: "Delete", items: ["a"] }, { _tag: "Insert", items: ["u"] }, { _tag: "Equal", items: ["t"] }] + * ``` + * + * @see {@link lines} for a line diff + * @see {@link applyText} to replay a script on a string + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const codePoints = (before: string, after: string, options?: { + /** + * Largest number of inserted plus deleted items searched for in each + * changed region. Common prefixes, suffixes and matching runs are free. + * When a region needs more edits, the search splits it at the furthest + * point it reached and continues on the rest, and a part that still cannot + * be matched becomes a deletion followed by an insertion. The script stays + * valid, and is minimal whenever the whole diff needs at most `maxEdits` + * edits. For a finite cap, work is O((N + M) * (maxEdits + 1)). + * + * @default Infinity + */ + readonly maxEdits?: number | undefined +}): Array> => diffStrings(Array.from(before), Array.from(after), options?.maxEdits) + +/** + * Applies an edit script to an array. + * + * **Details** + * + * `Equal` and `Delete` runs consume items from `before` by count, `Equal` runs + * copy them to the result, and `Insert` runs add their items. Run contents are + * not compared with `before`, since under a custom equivalence `Equal` items + * can differ from the input. + * + * **Gotchas** + * + * Fails with a {@link TextDiffError} when the runs do not consume exactly + * `before.length` items. + * + * **Example** (Replaying a script) + * + * ```ts import.meta.vitest + * import { Result, TextDiff } from "effect" + * + * const edits = TextDiff.diff([1, 2, 3], [1, 3, 4]) + * + * TextDiff.apply([1, 2, 3], edits) // => Result.succeed([1, 3, 4]) + * ``` + * + * @see {@link applyText} for the string variant + * @see {@link invert} to apply a script in reverse + * @stability experimental + * @category combinators + * @since 4.0.6 + */ +export const apply = ( + before: ReadonlyArray, + edits: ReadonlyArray> +): Result.Result, TextDiffError> => { + const out: Array = [] + let index = 0 + for (const edit of edits) { + if (edit._tag === "Insert") { + for (const item of edit.items) out.push(item) + continue + } + const end = index + edit.items.length + if (end > before.length) return fail("Edit script consumes more items than the input contains") + if (edit._tag === "Equal") { + for (let i = index; i < end; i++) out.push(before[i]) + } + index = end + } + if (index !== before.length) return fail("Edit script consumes fewer items than the input contains") + return Result.succeed(out) +} + +/** + * Applies a text edit script to a string. + * + * **Details** + * + * Works with scripts from both {@link lines} and {@link codePoints}. The + * joined items of each `Equal` and `Delete` run must match `before` at the + * current position, so a script made for different text is rejected rather + * than applied. + * + * **Gotchas** + * + * Fails with a {@link TextDiffError} when a run does not match `before`, or + * when the runs do not cover all of it. + * + * **Example** (Replaying a code point diff) + * + * ```ts import.meta.vitest + * import { Result, TextDiff } from "effect" + * + * const edits = TextDiff.codePoints("cat", "cut") + * + * TextDiff.applyText("cat", edits) // => Result.succeed("cut") + * ``` + * + * @see {@link apply} for the array variant + * @stability experimental + * @category combinators + * @since 4.0.6 + */ +export const applyText = ( + before: string, + edits: ReadonlyArray> +): Result.Result => { + let out = "" + let index = 0 + for (const edit of edits) { + const text = edit.items.join("") + if (edit._tag === "Insert") { + out += text + continue + } + if (!before.startsWith(text, index)) return fail(`Edit script does not match the input at index ${index}`) + if (edit._tag === "Equal") out += text + index += text.length + } + if (index !== before.length) return fail("Edit script covers less text than the input contains") + return Result.succeed(out) +} + +/** + * Inverts an edit script, so it turns `after` back into `before`. + * + * **Details** + * + * Swaps the items of each change's `Delete` and `Insert` runs, keeping + * deletions before insertions. `Equal` runs are unchanged. + * + * **Example** (Undoing a change) + * + * ```ts import.meta.vitest + * import { Result, TextDiff } from "effect" + * + * const edits = TextDiff.diff([1, 2], [1, 3]) + * + * TextDiff.apply([1, 3], TextDiff.invert(edits)) // => Result.succeed([1, 2]) + * ``` + * + * @stability experimental + * @category combinators + * @since 4.0.6 + */ +export const invert = (edits: ReadonlyArray>): Array> => { + const out: Array> = [] + let deleted: ReadonlyArray = [] + let inserted: ReadonlyArray = [] + const flush = () => { + if (inserted.length > 0) out.push({ _tag: "Delete", items: inserted }) + if (deleted.length > 0) out.push({ _tag: "Insert", items: deleted }) + deleted = [] + inserted = [] + } + for (const edit of edits) { + if (edit._tag === "Delete") { + deleted = deleted.concat(edit.items) + } else if (edit._tag === "Insert") { + inserted = inserted.concat(edit.items) + } else { + flush() + out.push(edit) + } + } + flush() + return out +} + +/** + * Formats a line edit script as a unified diff. + * + * **Details** + * + * The script must come from {@link lines}, or otherwise hold one line per + * item. The output follows `diff -u`: hunks whose context would touch or + * overlap are merged, and a final line without a newline is followed by + * `\ No newline at end of file`. When `fromFile` or `toFile` is set, both + * header lines are emitted and an unset name falls back to the other. A script + * without changes produces an empty string. + * + * **Gotchas** + * + * Among equally small diffs, hunks can differ from those of GNU diff or Git, + * which apply extra heuristics. + * + * **Example** (Formatting a unified diff) + * + * ```ts import.meta.vitest + * import { TextDiff } from "effect" + * + * TextDiff.unified(TextDiff.lines("a\nb\n", "a\nc\n")) // => "@@ -1,2 +1,2 @@\n a\n-b\n+c\n" + * ``` + * + * @see {@link lines} to compute a line diff + * @stability experimental + * @category formatting + * @since 4.0.6 + */ +export const unified = (edits: ReadonlyArray>, options?: { + /** + * Number of unchanged lines shown around each change. + * + * @default 3 + */ + readonly context?: number | undefined + /** + * Name shown in the `---` header line. + */ + readonly fromFile?: string | undefined + /** + * Name shown in the `+++` header line. + */ + readonly toFile?: string | undefined +}): string => { + const a: Array = [] + const b: Array = [] + // Group consecutive deletions and insertions between Equal runs. + const changes: Array = [] + let change: Change | undefined + for (const edit of edits) { + if (edit._tag === "Equal") { + change = undefined + for (const line of edit.items) { + a.push(line) + b.push(line) + } + continue + } + if (edit.items.length === 0) continue + if (change === undefined) { + change = { a0: a.length, a1: a.length, b0: b.length, b1: b.length } + changes.push(change) + } + if (edit._tag === "Delete") { + for (const line of edit.items) a.push(line) + change.a1 = a.length + } else { + for (const line of edit.items) b.push(line) + change.b1 = b.length + } + } + if (changes.length === 0) return "" + + const context = Math.max(0, Math.floor(options?.context ?? 3)) + let out = "" + if (options?.fromFile !== undefined || options?.toFile !== undefined) { + out += `--- ${options.fromFile ?? options.toFile}\n+++ ${options.toFile ?? options.fromFile}\n` + } + let start = 0 + while (start < changes.length) { + let end = start + 1 + while (end < changes.length && changes[end].a0 - changes[end - 1].a1 <= 2 * context) end++ + const first = changes[start] + const last = changes[end - 1] + const a0 = Math.max(0, first.a0 - context) + const b0 = first.b0 - (first.a0 - a0) + const a1 = Math.min(a.length, last.a1 + context) + const b1 = last.b1 + (a1 - last.a1) + out += `@@ -${range(a0, a1 - a0)} +${range(b0, b1 - b0)} @@\n` + let line = a0 + for (let c = start; c < end; c++) { + const change = changes[c] + for (; line < change.a0; line++) out += formatLine(" ", a[line]) + for (let k = change.a0; k < change.a1; k++) out += formatLine("-", a[k]) + for (let k = change.b0; k < change.b1; k++) out += formatLine("+", b[k]) + line = change.a1 + } + for (; line < a1; line++) out += formatLine(" ", a[line]) + start = end + } + return out +} + +// ----------------------------------------------------------------------------- +// internal +// ----------------------------------------------------------------------------- + +const fail = (message: string) => Result.fail(new TextDiffError({ message })) + +interface Block { + readonly a: number + readonly b: number + readonly length: number +} + +interface Change { + a0: number + a1: number + b0: number + b1: number +} + +interface Region { + readonly aLo: number + readonly aHi: number + readonly bLo: number + readonly bHi: number +} + +// A diagonal run from (x, y) to (u, v) +interface Snake { + readonly x: number + readonly y: number + readonly u: number + readonly v: number +} + +const splitLines = (text: string): Array => text.match(/[^\n]*\n|[^\n]+$/g) ?? [] + +const range = (start: number, count: number): string => + count === 0 ? `${start},0` : count === 1 ? `${start + 1}` : `${start + 1},${count}` + +const formatLine = (prefix: string, line: string): string => + line.endsWith("\n") ? prefix + line : `${prefix}${line}\n\\ No newline at end of file\n` + +// Interns the tokens so the search compares integers instead of strings +const diffStrings = (a: ReadonlyArray, b: ReadonlyArray, maxEdits?: number) => { + const ids = new Map() + const intern = (token: string) => { + let id = ids.get(token) + if (id === undefined) { + id = ids.size + ids.set(token, id) + } + return id + } + const ai = Int32Array.from(a, intern) + const bi = Int32Array.from(b, intern) + return editScript(a, b, (i, j) => ai[i] === bi[j], maxEdits) +} + +const editScript = ( + a: ReadonlyArray, + b: ReadonlyArray, + equals: (i: number, j: number) => boolean, + maxEdits?: number +): Array> => { + const blocks = matchingBlocks(a.length, b.length, equals, maxEdits) + // A final empty block flushes the trailing changes + blocks.push({ a: a.length, b: b.length, length: 0 }) + const out: Array> = [] + let i = 0 + let j = 0 + for (const block of blocks) { + if (block.a > i) out.push({ _tag: "Delete", items: a.slice(i, block.a) }) + if (block.b > j) out.push({ _tag: "Insert", items: b.slice(j, block.b) }) + if (block.length > 0) out.push({ _tag: "Equal", items: a.slice(block.a, block.a + block.length) }) + i = block.a + block.length + j = block.b + block.length + } + return out +} + +/** + * Returns ordered, merged matching runs for sequences of lengths n and m, + * using the index comparator. Linear-space Myers search splits each region + * at the middle snake of a minimal path, or at the furthest point reached + * when the search cap is exceeded. + */ +const matchingBlocks = ( + n: number, + m: number, + equals: (i: number, j: number) => boolean, + maxEdits = Infinity +): Array => { + const blocks: Array = [] + const push = (a: number, b: number, length: number) => { + if (length === 0) return + const last = blocks[blocks.length - 1] + if (last !== undefined && last.a + last.length === a && last.b + last.length === b) { + blocks[blocks.length - 1] = { a: last.a, b: last.b, length: last.length + length } + } else { + blocks.push({ a, b, length }) + } + } + + // Forward and reverse furthest-reaching x per diagonal, offset by n + m + 1 + const offset = n + m + 1 + const vf = new Int32Array(2 * offset + 1) + const vb = new Int32Array(2 * offset + 1) + // The searches meet after half the edits of a region, so a region needing at + // most `maxEdits` edits is solved within `maxD` steps of each search + const maxD = Math.ceil(Math.max(0, maxEdits) / 2) + + // Returns the middle snake of a minimal path in absolute coordinates, or an + // empty snake at the furthest point reached when the region needs more than + // `maxEdits` edits, or undefined when no point inside the region was reached + const findSplit = (aLo: number, aHi: number, bLo: number, bHi: number): Snake | undefined => { + const N = aHi - aLo + const M = bHi - bLo + const delta = N - M + const odd = (delta & 1) === 1 + const limit = Math.min((N + M + 1) >> 1, maxD) + vf[offset + 1] = 0 + vb[offset + 1] = 0 + for (let d = 0; d <= limit; d++) { + for (let k = -d; k <= d; k += 2) { + let x = k === -d || (k !== d && vf[offset + k - 1] < vf[offset + k + 1]) + ? vf[offset + k + 1] + : vf[offset + k - 1] + 1 + let y = x - k + const x0 = x + const y0 = y + while (x < N && y < M && equals(aLo + x, bLo + y)) { + x++ + y++ + } + vf[offset + k] = x + if (odd && k >= delta - (d - 1) && k <= delta + (d - 1) && x + vb[offset + delta - k] >= N) { + return { x: aLo + x0, y: bLo + y0, u: aLo + x, v: bLo + y } + } + } + for (let k = -d; k <= d; k += 2) { + let x = k === -d || (k !== d && vb[offset + k - 1] < vb[offset + k + 1]) + ? vb[offset + k + 1] + : vb[offset + k - 1] + 1 + let y = x - k + const x0 = x + const y0 = y + while (x < N && y < M && equals(aHi - x - 1, bHi - y - 1)) { + x++ + y++ + } + vb[offset + k] = x + if (!odd && k >= delta - d && k <= delta + d && x + vf[offset + delta - k] >= N) { + return { x: aHi - x, y: bHi - y, u: aHi - x0, v: bHi - y0 } + } + } + } + + // Over the cap: split at the reached point that covers the most items. It + // must lie strictly inside the region so that both parts shrink + let best = 0 + let x = 0 + let y = 0 + for (let k = -limit; k <= limit; k += 2) { + const xf = vf[offset + k] + const yf = xf - k + if (xf <= N && yf >= 0 && yf <= M && xf + yf > best) { + best = xf + yf + x = aLo + xf + y = bLo + yf + } + const xb = vb[offset + k] + const yb = xb - k + if (xb <= N && yb >= 0 && yb <= M && xb + yb > best) { + best = xb + yb + x = aHi - xb + y = bHi - yb + } + } + return best > 0 && best < N + M ? { x, y, u: x, v: y } : undefined + } + + // Regions are processed in order from an explicit stack, since splits over + // the cap can nest deeper than the call stack allows + const stack: Array = [{ aLo: 0, aHi: n, bLo: 0, bHi: m }] + while (stack.length > 0) { + const task = stack.pop()! + if ("length" in task) { + push(task.a, task.b, task.length) + continue + } + let { aHi, aLo, bHi, bLo } = task + let prefix = 0 + while (aLo + prefix < aHi && bLo + prefix < bHi && equals(aLo + prefix, bLo + prefix)) prefix++ + push(aLo, bLo, prefix) + aLo += prefix + bLo += prefix + let suffix = 0 + while (aLo < aHi - suffix && bLo < bHi - suffix && equals(aHi - suffix - 1, bHi - suffix - 1)) suffix++ + aHi -= suffix + bHi -= suffix + stack.push({ a: aHi, b: bHi, length: suffix }) + // With a common prefix and suffix removed, a non-empty side on both ends + // means at least two edits, so both halves below are strictly smaller + const snake = aLo < aHi && bLo < bHi ? findSplit(aLo, aHi, bLo, bHi) : undefined + if (snake !== undefined) { + stack.push( + { aLo: snake.u, aHi, bLo: snake.v, bHi }, + { a: snake.x, b: snake.y, length: snake.u - snake.x }, + { aLo, aHi: snake.x, bLo, bHi: snake.y } + ) + } + } + return blocks +} diff --git a/repos/effect/packages/effect/src/Tracer.ts b/packages/effect/src/Tracer.ts similarity index 100% rename from repos/effect/packages/effect/src/Tracer.ts rename to packages/effect/src/Tracer.ts diff --git a/repos/effect/packages/effect/src/Trie.ts b/packages/effect/src/Trie.ts similarity index 100% rename from repos/effect/packages/effect/src/Trie.ts rename to packages/effect/src/Trie.ts diff --git a/repos/effect/packages/effect/src/Tuple.ts b/packages/effect/src/Tuple.ts similarity index 100% rename from repos/effect/packages/effect/src/Tuple.ts rename to packages/effect/src/Tuple.ts diff --git a/repos/effect/packages/effect/src/TxChunk.ts b/packages/effect/src/TxChunk.ts similarity index 100% rename from repos/effect/packages/effect/src/TxChunk.ts rename to packages/effect/src/TxChunk.ts diff --git a/repos/effect/packages/effect/src/TxDeferred.ts b/packages/effect/src/TxDeferred.ts similarity index 100% rename from repos/effect/packages/effect/src/TxDeferred.ts rename to packages/effect/src/TxDeferred.ts diff --git a/repos/effect/packages/effect/src/TxHashMap.ts b/packages/effect/src/TxHashMap.ts similarity index 100% rename from repos/effect/packages/effect/src/TxHashMap.ts rename to packages/effect/src/TxHashMap.ts diff --git a/repos/effect/packages/effect/src/TxHashSet.ts b/packages/effect/src/TxHashSet.ts similarity index 100% rename from repos/effect/packages/effect/src/TxHashSet.ts rename to packages/effect/src/TxHashSet.ts diff --git a/repos/effect/packages/effect/src/TxPriorityQueue.ts b/packages/effect/src/TxPriorityQueue.ts similarity index 100% rename from repos/effect/packages/effect/src/TxPriorityQueue.ts rename to packages/effect/src/TxPriorityQueue.ts diff --git a/repos/effect/packages/effect/src/TxPubSub.ts b/packages/effect/src/TxPubSub.ts similarity index 100% rename from repos/effect/packages/effect/src/TxPubSub.ts rename to packages/effect/src/TxPubSub.ts diff --git a/repos/effect/packages/effect/src/TxQueue.ts b/packages/effect/src/TxQueue.ts similarity index 100% rename from repos/effect/packages/effect/src/TxQueue.ts rename to packages/effect/src/TxQueue.ts diff --git a/repos/effect/packages/effect/src/TxReentrantLock.ts b/packages/effect/src/TxReentrantLock.ts similarity index 100% rename from repos/effect/packages/effect/src/TxReentrantLock.ts rename to packages/effect/src/TxReentrantLock.ts diff --git a/repos/effect/packages/effect/src/TxRef.ts b/packages/effect/src/TxRef.ts similarity index 100% rename from repos/effect/packages/effect/src/TxRef.ts rename to packages/effect/src/TxRef.ts diff --git a/repos/effect/packages/effect/src/TxSemaphore.ts b/packages/effect/src/TxSemaphore.ts similarity index 100% rename from repos/effect/packages/effect/src/TxSemaphore.ts rename to packages/effect/src/TxSemaphore.ts diff --git a/repos/effect/packages/effect/src/TxSubscriptionRef.ts b/packages/effect/src/TxSubscriptionRef.ts similarity index 100% rename from repos/effect/packages/effect/src/TxSubscriptionRef.ts rename to packages/effect/src/TxSubscriptionRef.ts diff --git a/repos/effect/packages/effect/src/Types.ts b/packages/effect/src/Types.ts similarity index 100% rename from repos/effect/packages/effect/src/Types.ts rename to packages/effect/src/Types.ts diff --git a/repos/effect/packages/effect/src/UndefinedOr.ts b/packages/effect/src/UndefinedOr.ts similarity index 100% rename from repos/effect/packages/effect/src/UndefinedOr.ts rename to packages/effect/src/UndefinedOr.ts diff --git a/repos/effect/packages/effect/src/Unify.ts b/packages/effect/src/Unify.ts similarity index 100% rename from repos/effect/packages/effect/src/Unify.ts rename to packages/effect/src/Unify.ts diff --git a/repos/effect/packages/effect/src/Utils.ts b/packages/effect/src/Utils.ts similarity index 100% rename from repos/effect/packages/effect/src/Utils.ts rename to packages/effect/src/Utils.ts diff --git a/repos/effect/packages/effect/src/Version.ts b/packages/effect/src/Version.ts similarity index 100% rename from repos/effect/packages/effect/src/Version.ts rename to packages/effect/src/Version.ts diff --git a/repos/effect/packages/effect/src/ai/AiError.ts b/packages/effect/src/ai/AiError.ts similarity index 96% rename from repos/effect/packages/effect/src/ai/AiError.ts rename to packages/effect/src/ai/AiError.ts index 8fbe89087..0a84f8a8d 100644 --- a/repos/effect/packages/effect/src/ai/AiError.ts +++ b/packages/effect/src/ai/AiError.ts @@ -341,6 +341,15 @@ export interface ContentPolicyErrorMetadata extends ProviderMetadata {} */ export interface InvalidRequestErrorMetadata extends ProviderMetadata {} +/** + * Provider-specific metadata attached to `InvalidResponseError`. + * + * @stability unstable + * @category configuration + * @since 4.1.0 + */ +export interface InvalidResponseErrorMetadata extends ProviderMetadata {} + /** * Provider-specific metadata attached to `InternalProviderError`. * @@ -835,7 +844,59 @@ export class InternalProviderError extends Schema.Error( } /** - * Error indicating failure to parse or validate LLM output. + * Error indicating the provider response does not conform to the expected format. + * + * **Details** + * + * Invalid response errors are not retryable because retrying does not resolve an + * incompatible provider response format. Optional HTTP context can retain request + * and response details for diagnosis. + * + * @see {@link InvalidOutputError} for retryable model-generated output failures + * @stability unstable + * @category errors + * @since 4.1.0 + */ +export class InvalidResponseError extends Schema.Error( + "effect/ai/AiError/InvalidResponseError" +)({ + _tag: Schema.tag("InvalidResponseError"), + description: Schema.String, + metadata: providerMetadataWithDefaults(), + http: Schema.optional(HttpContext) +}) { + /** + * Marks `InvalidResponseError` as a semantic AI error reason for runtime guards. + * + * @since 4.1.0 + */ + readonly [ReasonTypeId] = ReasonTypeId + + /** + * Invalid response errors are not retryable because the response format is incompatible. + * + * @since 4.1.0 + */ + get isRetryable(): boolean { + return false + } + + /** + * Creates an InvalidResponseError from a provider response schema error. + * + * @since 4.1.0 + */ + static fromSchemaError(error: Schema.SchemaError): InvalidResponseError { + return new InvalidResponseError({ description: error.message }) + } + + override get message(): string { + return `Invalid response: ${this.description}` + } +} + +/** + * Error indicating failure to parse or validate model-generated output. * * **Details** * @@ -903,9 +964,7 @@ export class InvalidOutputError extends Schema.Error( * @since 4.0.0 */ static fromSchemaError(error: Schema.SchemaError): InvalidOutputError { - return new InvalidOutputError({ - description: error.message - }) + return new InvalidOutputError({ description: error.message }) } override get message(): string { @@ -1520,6 +1579,7 @@ export type AiErrorReason = | AuthenticationError | ContentPolicyError | InvalidRequestError + | InvalidResponseError | InternalProviderError | NetworkError | InvalidOutputError @@ -1557,6 +1617,7 @@ export const AiErrorReason: Schema.Union<[ typeof AuthenticationError, typeof ContentPolicyError, typeof InvalidRequestError, + typeof InvalidResponseError, typeof InternalProviderError, typeof NetworkError, typeof InvalidOutputError, @@ -1576,6 +1637,7 @@ export const AiErrorReason: Schema.Union<[ AuthenticationError, ContentPolicyError, InvalidRequestError, + InvalidResponseError, InternalProviderError, NetworkError, InvalidOutputError, diff --git a/repos/effect/packages/effect/src/ai/AnthropicStructuredOutput.ts b/packages/effect/src/ai/AnthropicStructuredOutput.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/AnthropicStructuredOutput.ts rename to packages/effect/src/ai/AnthropicStructuredOutput.ts diff --git a/repos/effect/packages/effect/src/ai/Chat.ts b/packages/effect/src/ai/Chat.ts similarity index 98% rename from repos/effect/packages/effect/src/ai/Chat.ts rename to packages/effect/src/ai/Chat.ts index a68b1e37f..ce0d2cc20 100644 --- a/repos/effect/packages/effect/src/ai/Chat.ts +++ b/packages/effect/src/ai/Chat.ts @@ -18,8 +18,7 @@ import * as Duration from "../Duration.ts" import * as Effect from "../Effect.ts" import * as Layer from "../Layer.ts" import * as Option from "../Option.ts" -import type { PersistenceError } from "../persistence/Persistence.ts" -import { BackingPersistence } from "../persistence/Persistence.ts" +import { BackingPersistence, PersistenceError } from "../persistence/Persistence.ts" import * as Predicate from "../Predicate.ts" import * as Ref from "../Ref.ts" import * as Schema from "../Schema.ts" @@ -926,13 +925,19 @@ export const makePersisted = Effect.fnUntraced(function*(options: { return yield* new ChatNotFoundError({ chatId }) } // Decode the encoded previous history - const history = yield* decodeHistory(previousHistory) + const history = yield* decodeHistory(previousHistory).pipe( + Effect.mapError((cause) => + new PersistenceError({ + message: `Failed to decode persisted history for chat "${chatId}"`, + cause + }) + ) + ) // Hydrate the chat history yield* Ref.set(chat.history, history) // Convert the chat to a persisted chat return yield* toPersisted(chatId, chat, ttl) - }, - Effect.catchTag("SchemaError", Effect.die) + } ) const get = Effect.fnUntraced( diff --git a/repos/effect/packages/effect/src/ai/Decision.ts b/packages/effect/src/ai/Decision.ts similarity index 88% rename from repos/effect/packages/effect/src/ai/Decision.ts rename to packages/effect/src/ai/Decision.ts index 1775bd80b..ba1032bfd 100644 --- a/repos/effect/packages/effect/src/ai/Decision.ts +++ b/packages/effect/src/ai/Decision.ts @@ -1,13 +1,15 @@ /** * Defines decisions that a `DecisionModel` answers over a single input. * Pair an input schema with named classification, rating, or probability - * decisions using `make`, then answer them with `DecisionModel.decide`. + * decisions using `make`, then answer them with `DecisionModel.decide`, or + * with `DecisionModel.decideSettled` to keep valid answers when others fail. * * @see {@link make} for building a definition from an input schema and decisions * * @stability unstable * @since 4.0.0 */ +import type * as Result from "../Result.ts" import type * as Schema from "../Schema.ts" /** @@ -175,6 +177,52 @@ export type Answers> = { readonly [K in keyof Decisions]: Answer } +/** + * Reason and description of the first failed answer validation check. + * + * **Details** + * + * - `"Missing"`: the provider returned no answer for the decision. + * - `"Refused"`: the provider declined to answer the decision. + * - `"Invalid"`: the answer has an invalid kind, value, confidence, or distribution. + * - `"NoDistribution"`: a classify or rate answer is otherwise valid but has + * no probabilities. + * + * Except for refusals, `description` is the message `DecisionModel.decide` + * fails with for the same answer. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface Unanswered { + readonly _tag: "Unanswered" + readonly reason: "Missing" | "Refused" | "NoDistribution" | "Invalid" + readonly description: string +} + +/** + * Validated answer or the reason the decision is unanswered. + * + * @see {@link Unanswered} for the failure reasons + * + * @stability unstable + * @category utility types + * @since 4.0.6 + */ +export type Outcome = Result.Result, Unanswered> + +/** + * Settled results keyed by decision name. + * + * @stability unstable + * @category utility types + * @since 4.0.6 + */ +export type Outcomes> = { + readonly [K in keyof Decisions]: Outcome +} + /** * Input schema and named decisions that run together in one provider call. * diff --git a/packages/effect/src/ai/DecisionModel.ts b/packages/effect/src/ai/DecisionModel.ts new file mode 100644 index 000000000..22e78a100 --- /dev/null +++ b/packages/effect/src/ai/DecisionModel.ts @@ -0,0 +1,617 @@ +/** + * Defines the provider-neutral service for structured decisions. + * `decide` encodes one input as JSON, sends its named decisions in one provider + * call, and validates the answers. Failures are reported as `AiError` values. + * `decideSettled` makes the same call but reports each decision separately, so + * valid answers survive when others are missing, refused, or invalid. + * @stability unstable + * @since 4.0.0 + */ +import * as Context from "../Context.ts" +import * as Effect from "../Effect.ts" +import * as Predicate from "../Predicate.ts" +import * as Result from "../Result.ts" +import * as Schema from "../Schema.ts" +import * as AiError from "./AiError.ts" +import type * as Decision from "./Decision.ts" +import type * as Response from "./Response.ts" + +/** + * Service key for answering decisions about an input. Create implementations + * with `make`. + * + * @see {@link decide} for answering a definition through the current service + * + * @stability unstable + * @category services + * @since 4.0.0 + */ +export const DecisionModel: Context.Service = Context.Service( + "effect/ai/DecisionModel" +) + +/** + * Brand type for `DecisionModel`. + * + * @stability unstable + * @category type IDs + * @since 4.0.0 + */ +export type TypeId = "~effect/ai/DecisionModel" + +/** + * Brand for `DecisionModel` implementations. + * + * @stability unstable + * @category type IDs + * @since 4.0.0 + */ +export const TypeId: TypeId = "~effect/ai/DecisionModel" + +/** + * Provider-reported token usage. Unreported counts are `undefined`. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export class DecisionUsage extends Schema.Class( + "effect/ai/DecisionModel/DecisionUsage" +)({ + inputTokens: Schema.optional(Schema.Finite), + outputTokens: Schema.optional(Schema.Finite) +}) {} + +/** + * Options for answering a decision definition. + * + * @stability unstable + * @category options + * @since 4.0.0 + */ +export interface DecideOptions { + readonly input: Input["Type"] + /** + * Images to include with the input. + */ + readonly images?: ReadonlyArray | undefined +} + +/** + * An image sent to the provider with the encoded input. + * `data` is a base64 string, a byte array, or a URL. + * + * @stability unstable + * @category models + * @since 4.0.1 + */ +export interface Image { + readonly mediaType: string + readonly data: string | Uint8Array | URL +} + +/** + * Provider-specific response metadata keyed by provider name. Provider + * packages augment this interface. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface DecideResponseMetadata extends Response.ProviderMetadata {} + +/** + * Answers for every decision in a definition with usage and response metadata. + * + * @see {@link DecisionUsage} for token usage metadata + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface DecideResponse> { + readonly answers: Decision.Answers + readonly usage: DecisionUsage + /** + * Response id reported by the provider. + * + * @since 4.0.6 + */ + readonly id?: string | undefined + /** + * Model that answered. Can differ from the requested model when the + * provider resolves an alias. + * + * @since 4.0.6 + */ + readonly modelId?: string | undefined + /** + * Provider-specific metadata, such as reported cost. + * + * @since 4.0.6 + */ + readonly metadata: DecideResponseMetadata +} + +/** + * Per-decision outcomes, token usage, and response metadata for the whole + * request. + * + * @see {@link decideSettled} for producing this response + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface DecideSettledResponse> { + readonly outcomes: Decision.Outcomes + readonly usage: DecisionUsage + readonly id?: string | undefined + readonly modelId?: string | undefined + readonly metadata: DecideResponseMetadata +} + +/** + * Provider input options for a decision request. + * `state` is encoded with `Schema.toCodecJson`, not stringified. + * All `decisions` must be answered in one call. + * `images` is omitted when no images are supplied. + * + * @stability unstable + * @category options + * @since 4.0.0 + */ +export interface ProviderOptions { + readonly state: Schema.Json + readonly decisions: Record + readonly images?: ReadonlyArray | undefined +} + +/** + * Provider classify answer. Omit `probabilities` when no distribution is available. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface ProviderClassifyAnswer { + readonly _tag: "Classify" + readonly label: string + readonly probabilities?: Readonly> | undefined + readonly confidence?: number | undefined +} + +/** + * Provider rate answer. The highest probability determines the label, with + * ties going to the first criterion. Omit `probabilities` when unavailable. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface ProviderRateAnswer { + readonly _tag: "Rate" + readonly rating: number + readonly probabilities?: Readonly> | undefined + readonly confidence?: number | undefined +} + +/** + * Provider answer for a probability decision. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface ProviderProbabilityAnswer { + readonly _tag: "Probability" + readonly probability: number +} + +/** + * Provider refusal to answer a decision. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface ProviderRefusalAnswer { + readonly _tag: "Refusal" +} + +/** + * Provider answer for any decision kind. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export type ProviderAnswer = + | ProviderClassifyAnswer + | ProviderRateAnswer + | ProviderProbabilityAnswer + | ProviderRefusalAnswer + +/** + * Provider response for a decision request. + * `answers` is keyed like the requested decisions. Each answer is validated + * against its decision before it is returned to the caller. Omit the key of a + * decision the provider did not answer. `id`, `modelId` and `metadata` are + * passed through; `metadata` defaults to `{}`. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface ProviderResponse { + readonly answers: Readonly> + readonly usage: { + readonly inputTokens: number | undefined + readonly outputTokens: number | undefined + } + readonly id?: string | undefined + readonly modelId?: string | undefined + readonly metadata?: DecideResponseMetadata | undefined +} + +/** + * Decision operations over a definition. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface DecisionModel { + readonly [TypeId]: TypeId + readonly decide: >( + definition: Decision.Definition, + options: DecideOptions + ) => Effect.Effect, AiError.AiError, Input["EncodingServices"]> + readonly decideSettled: >( + definition: Decision.Definition, + options: DecideOptions + ) => Effect.Effect, AiError.AiError, Input["EncodingServices"]> +} + +const unanswered = ( + reason: Decision.Unanswered["reason"], + description: string +): Result.Result => Result.fail({ _tag: "Unanswered", reason, description }) + +const invalid = (description: string) => unanswered("Invalid", description) + +const aiError = (method: "decide" | "decideSettled", reason: AiError.AiErrorReason): AiError.AiError => + AiError.make({ module: "DecisionModel", method, reason }) + +const isFiniteNumber = (value: unknown): value is number => Predicate.isNumber(value) && Number.isFinite(value) + +const isUnitInterval = (value: unknown): value is number => isFiniteNumber(value) && value >= 0 && value <= 1 + +const sumTolerance = 1e-6 + +/** + * `roundingError` is the largest error a provider's rounding adds to a single + * probability (half a unit of its last decimal place). Distributions that sum + * to 1 within `labels.length * roundingError` are rescaled to sum to 1. + */ +const validateDistribution = ( + key: string, + labels: ReadonlyArray, + answer: Record, + roundingError: number +): Result.Result, Decision.Unanswered> => { + const raw = answer.probabilities + if (!Predicate.isObject(raw)) { + return unanswered("NoDistribution", `Provider returned no probabilities for decision "${key}"`) + } + const probabilities: Record = Object.create(null) + let total = 0 + for (const label of labels) { + const value = raw[label] + if (!isUnitInterval(value)) { + return invalid(`Provider returned no probability for label "${label}" of decision "${key}"`) + } + probabilities[label] = value + total += value + } + const drift = Math.abs(total - 1) + if (total === 0 || drift > labels.length * roundingError + sumTolerance) { + return invalid(`Provider returned probabilities that do not sum to 1 for decision "${key}"`) + } + if (drift > sumTolerance) { + for (const label of labels) probabilities[label] /= total + } + return Result.succeed(probabilities) +} + +const validateConfidence = ( + key: string, + answer: Record +): Result.Result => { + if (answer.confidence === undefined) { + return Result.succeed(undefined) + } + return isUnitInterval(answer.confidence) + ? Result.succeed(answer.confidence) + : invalid(`Provider returned confidence outside [0, 1] for decision "${key}"`) +} + +const validateAnswer = ( + key: string, + decision: Decision.Any, + answer: unknown, + roundingError: number +): Decision.Outcome => { + if (!Predicate.isObject(answer)) { + return unanswered("Missing", `Provider returned no answer for decision "${key}"`) + } + if (answer._tag === "Refusal") { + return unanswered("Refused", `Provider refused to answer decision "${key}"`) + } + if (answer._tag !== decision._tag) { + return invalid( + `Provider returned a "${String(answer._tag)}" answer for decision "${key}", expected "${decision._tag}"` + ) + } + switch (decision._tag) { + case "Classify": { + const labels = Object.keys(decision.criteria) + const label = answer.label + if (!Predicate.isString(label) || !labels.includes(label)) { + return invalid(`Provider returned an unknown label for decision "${key}"`) + } + return Result.gen(function*() { + const confidence = yield* validateConfidence(key, answer) + const probabilities = yield* validateDistribution(key, labels, answer, roundingError) + return { label, probabilities, ...(confidence === undefined ? undefined : { confidence }) } + }) + } + case "Rate": { + const levels = decision.criteria.map((level) => level.label) + const rating = answer.rating + if (!isFiniteNumber(rating) || rating < 0 || rating > levels.length - 1) { + return invalid(`Provider returned a rating outside [0, ${levels.length - 1}] for decision "${key}"`) + } + return Result.gen(function*() { + const confidence = yield* validateConfidence(key, answer) + const probabilities = yield* validateDistribution(key, levels, answer, roundingError) + let label = levels[0] + for (let i = 1; i < levels.length; i++) { + if (probabilities[levels[i]] > probabilities[label]) { + label = levels[i] + } + } + return { rating, label, probabilities, ...(confidence === undefined ? undefined : { confidence }) } + }) + } + case "Probability": { + if (!isUnitInterval(answer.probability)) { + return invalid(`Provider returned a probability outside [0, 1] for decision "${key}"`) + } + return Result.succeed({ probability: answer.probability }) + } + } +} + +const settleAnswers = >( + decisions: Decisions, + answers: Readonly>, + roundingError: number +): Decision.Outcomes => { + const outcomes: Record> = Object.create(null) + for (const key of Object.keys(decisions)) { + outcomes[key] = validateAnswer(key, decisions[key], answers[key], roundingError) + } + return outcomes as Decision.Outcomes +} + +const requireAnswers = >( + outcomes: Decision.Outcomes +): Effect.Effect, AiError.AiError> => { + const answers: Record> = Object.create(null) + const refused: Array = [] + let firstUnanswered: Decision.Unanswered | undefined + for (const [key, outcome] of Object.entries(outcomes) as Array<[string, Decision.Outcome]>) { + if (Result.isSuccess(outcome)) { + answers[key] = outcome.success + } else if (outcome.failure.reason === "Refused") { + refused.push(`"${key}"`) + } else { + firstUnanswered ??= outcome.failure + } + } + if (refused.length > 0) { + return Effect.fail(aiError( + "decide", + new AiError.ContentPolicyError({ description: `Provider refused to answer decisions ${refused.join(", ")}` }) + )) + } + if (firstUnanswered !== undefined) { + return Effect.fail(aiError("decide", new AiError.InvalidOutputError({ description: firstUnanswered.description }))) + } + return Effect.succeed(answers as Decision.Answers) +} + +/** + * Creates a `DecisionModel` that encodes inputs as JSON and validates provider answers. + * Answers must cover every decision and use its labels. Distributions must + * sum to 1 within `1e-6`, optional confidence must be in `[0, 1]`, and ratings must be + * in `[0, criteria.length - 1]`. With `decide`, refused decisions fail with + * `AiError.ContentPolicyError` and other unanswered decisions fail with + * `AiError.InvalidOutputError`; `decideSettled` reports them per decision + * instead. Encoding failures use `AiError.InvalidUserInputError`. + * + * **Details** + * + * Set `supportsImages: true` to accept images. Otherwise, nonempty image input + * fails with `AiError.InvalidUserInputError`. + * + * Providers that round each probability to `probabilityPrecision` decimal + * places may return distributions whose sum drifts from 1 by up to half a unit + * of the last place per label. Setting `probabilityPrecision` accepts that + * drift and rescales the distribution to sum to 1; larger drift still fails. + * + * @see {@link DecisionModel} for the service shape returned by this constructor + * @see {@link ProviderOptions} for the input passed to the provider implementation + * @see {@link ProviderResponse} for the provider response contract consumed by this constructor + * + * @stability unstable + * @category constructors + * @since 4.0.0 + */ +export const make = (params: { + readonly decide: (options: ProviderOptions) => Effect.Effect + readonly probabilityPrecision?: number | undefined + readonly supportsImages?: boolean | undefined +}): Effect.Effect => + Effect.sync(() => { + const roundingError = params.probabilityPrecision === undefined ? 0 : 0.5 * 10 ** -params.probabilityPrecision + const settle = >( + method: "decide" | "decideSettled", + definition: Decision.Definition, + options: DecideOptions + ): Effect.Effect, AiError.AiError, Input["EncodingServices"]> => + Schema.encodeEffect(Schema.toCodecJson(definition.input))(options.input).pipe( + Effect.mapError((error) => aiError(method, new AiError.InvalidUserInputError({ description: error.message }))), + Effect.flatMap((state) => { + const images = options.images?.length ? options.images : undefined + if (images !== undefined && params.supportsImages !== true) { + return Effect.fail( + aiError( + method, + new AiError.InvalidUserInputError({ description: "This decision model does not accept images" }) + ) + ) + } + return params.decide({ + state, + decisions: definition.decisions, + ...(images === undefined ? undefined : { images }) + }) + }), + Effect.map((response): DecideSettledResponse => ({ + outcomes: settleAnswers(definition.decisions, response.answers, roundingError), + usage: new DecisionUsage({ + inputTokens: response.usage.inputTokens, + outputTokens: response.usage.outputTokens + }), + ...(response.id === undefined ? undefined : { id: response.id }), + ...(response.modelId === undefined ? undefined : { modelId: response.modelId }), + metadata: response.metadata ?? {} + })) + ) + return DecisionModel.of({ + [TypeId]: TypeId, + decide: (definition, options) => + settle("decide", definition, options).pipe( + Effect.flatMap(({ outcomes, ...response }) => + Effect.map(requireAnswers(outcomes), (answers) => ({ answers, ...response })) + ), + (effect) => Effect.withSpan(effect, "DecisionModel.decide", { captureStackTrace: false }) + ), + decideSettled: (definition, options) => + settle("decideSettled", definition, options).pipe( + (effect) => Effect.withSpan(effect, "DecisionModel.decideSettled", { captureStackTrace: false }) + ) + }) + }) + +/** + * Answers a decision definition using the current `DecisionModel` service. + * Encodes the input with `Schema.toCodecJson`, requiring the schema's encoding services. + * Explicit `undefined` fields become `null`; absent fields stay absent. + * Custom declarations need a JSON codec annotation or encoding fails. + * Returned answers and probability dictionaries have null prototypes. + * Fails with `AiError.ContentPolicyError` if the provider refuses any decision, + * and otherwise with `AiError.InvalidOutputError` for the first unanswered + * decision in definition order. + * + * **Example** (Triaging a ticket) + * + * ```ts + * import { Effect, Schema } from "effect" + * import { Decision, DecisionModel } from "effect/ai" + * + * const TicketTriage = Decision.make({ + * input: Schema.String, + * decisions: { + * urgent: Decision.probability({ + * instructions: "The message is time-sensitive", + * criteria: { false: "No time pressure", true: "Needs action now" } + * }) + * } + * }) + * + * const program = Effect.gen(function*() { + * const { answers, usage } = yield* DecisionModel.decide(TicketTriage, { + * input: "My card was charged twice, please fix this today" + * }) + * return { probability: answers.urgent.probability, usage } + * }) + * ``` + * + * @see {@link DecisionModel} for the service this function requires + * @see {@link decideSettled} for keeping valid answers when others fail + * + * @stability unstable + * @category decisions + * @since 4.0.0 + */ +export const decide = >( + definition: Decision.Definition, + options: DecideOptions +): Effect.Effect, AiError.AiError, DecisionModel | Input["EncodingServices"]> => + DecisionModel.use((model) => model.decide(definition, options)) + +/** + * Answers each decision with a `Result` containing a validated answer or a + * `Decision.Unanswered` reason. Input encoding and provider failures still + * fail the effect. Token usage covers the whole call. + * + * **Details** + * + * Provider response decoding errors fail the whole call. Per-decision reasons + * reflect the first failed validation check; `"NoDistribution"` requires an + * otherwise valid answer. + * + * **Example** (Falling back for unanswered decisions) + * + * ```ts + * import { Effect, Result, Schema } from "effect" + * import { Decision, DecisionModel } from "effect/ai" + * + * const TicketTriage = Decision.make({ + * input: Schema.String, + * decisions: { + * department: Decision.classify({ + * instructions: "Which team should handle this", + * criteria: { billing: "payments", technical: "bugs" } + * }), + * urgent: Decision.probability({ instructions: "The message is time-sensitive" }) + * } + * }) + * + * const program = Effect.gen(function*() { + * const { outcomes } = yield* DecisionModel.decideSettled(TicketTriage, { + * input: "My card was charged twice, please fix this today" + * }) + * const department = Result.isSuccess(outcomes.department) && + * outcomes.department.success.probabilities.billing >= 0.8 + * ? "billing" + * : "needs review" + * const urgent = Result.isSuccess(outcomes.urgent) ? outcomes.urgent.success.probability : undefined + * return { department, urgent } + * }) + * ``` + * + * @see {@link decide} for failing unless every decision is answered + * + * @stability unstable + * @category decisions + * @since 4.0.6 + */ +export const decideSettled = >( + definition: Decision.Definition, + options: DecideOptions +): Effect.Effect, AiError.AiError, DecisionModel | Input["EncodingServices"]> => + DecisionModel.use((model) => model.decideSettled(definition, options)) diff --git a/repos/effect/packages/effect/src/ai/EmbeddingModel.ts b/packages/effect/src/ai/EmbeddingModel.ts similarity index 87% rename from repos/effect/packages/effect/src/ai/EmbeddingModel.ts rename to packages/effect/src/ai/EmbeddingModel.ts index 4d3ac0c6f..b9a8d9865 100644 --- a/repos/effect/packages/effect/src/ai/EmbeddingModel.ts +++ b/packages/effect/src/ai/EmbeddingModel.ts @@ -209,8 +209,17 @@ const invalidProviderResponse = (description: string): AiError.AiError => * provider, while `embedMany([])` returns an empty response without calling the * provider. * + * Concurrent `embed` calls share one `embedMany` call when `batchKey` returns + * equal keys under `Equal.equals`. The batch runs with the first caller's + * context, so return every value `embedMany` reads from that context. Without + * `batchKey`, all callers share one batch. + * * **Gotchas** * + * Context that `batchKey` does not return is not isolated. Per-call + * `FetchHttpClient.RequestInit`, or any service read by an injected HTTP + * client, takes the first caller's value for the whole batch. + * * Provider responses are interpreted positionally and must contain exactly one * result for each requested input. If the provider returns a different number * of results, `embed` and `embedMany` fail with `AiError.InvalidOutputError`. @@ -225,20 +234,23 @@ const invalidProviderResponse = (description: string): AiError.AiError => */ export const make: (params: { readonly embedMany: (options: ProviderOptions) => Effect.Effect + readonly batchKey?: ((context: Context.Context) => unknown) | undefined }) => Effect.Effect = Effect.fnUntraced(function*(params) { - const resolver = RequestResolver.make((entries) => - Effect.flatMap( - params.embedMany({ - inputs: entries.map((entry) => entry.request.input) - }), - (response) => - Effect.map(mapProviderResults(entries.length, response.results), (embeddings) => { - for (let i = 0; i < entries.length; i++) { - entries[i].completeUnsafe(Exit.succeed(embeddings[i])) - } - }) - ) - ).pipe( + const resolver = RequestResolver.makeGrouped({ + key: ({ context }) => params.batchKey?.(context), + resolver: (entries) => + Effect.flatMap( + params.embedMany({ + inputs: entries.map((entry) => entry.request.input) + }), + (response) => + Effect.map(mapProviderResults(entries.length, response.results), (embeddings) => { + for (let i = 0; i < entries.length; i++) { + entries[i].completeUnsafe(Exit.succeed(embeddings[i])) + } + }) + ) + }).pipe( RequestResolver.withSpan("EmbeddingModel.resolver") ) diff --git a/repos/effect/packages/effect/src/ai/IdGenerator.ts b/packages/effect/src/ai/IdGenerator.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/IdGenerator.ts rename to packages/effect/src/ai/IdGenerator.ts diff --git a/repos/effect/packages/effect/src/ai/LanguageModel.ts b/packages/effect/src/ai/LanguageModel.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/LanguageModel.ts rename to packages/effect/src/ai/LanguageModel.ts diff --git a/packages/effect/src/ai/McpClient.ts b/packages/effect/src/ai/McpClient.ts new file mode 100644 index 000000000..ec7c2ad8c --- /dev/null +++ b/packages/effect/src/ai/McpClient.ts @@ -0,0 +1,1211 @@ +/** + * Connects to MCP servers with scoped Effect clients. + * + * Clients discover tools, prompts and resources using the protocol revision selected by + * their transport. `callTool` returns tool-reported errors as results; supplying a schema + * turns them into a `ToolError` reason that retains the original result. Transport and + * protocol failures fail the Effect. Interactive server requests, subscriptions, OAuth + * and retries are not included. + * + * @stability experimental + * @since 4.0.6 + */ +import * as ByteSize from "../ByteSize.ts" +import type * as Cause from "../Cause.ts" +import * as Context from "../Context.ts" +import * as Deferred from "../Deferred.ts" +import * as Duration from "../Duration.ts" +import * as Effect from "../Effect.ts" +import * as Base64 from "../encoding/Base64.ts" +import * as Sse from "../encoding/Sse.ts" +import * as HttpClient from "../http/HttpClient.ts" +import * as HttpClientRequest from "../http/HttpClientRequest.ts" +import type * as HttpClientResponse from "../http/HttpClientResponse.ts" +import * as Layer from "../Layer.ts" +import * as Option from "../Option.ts" +import * as Predicate from "../Predicate.ts" +import type * as ChildProcess from "../process/ChildProcess.ts" +import { ChildProcessSpawner } from "../process/ChildProcessSpawner.ts" +import * as Queue from "../Queue.ts" +import type * as Rpc from "../rpc/Rpc.ts" +import * as Schema from "../Schema.ts" +import type * as Scope from "../Scope.ts" +import * as Stream from "../Stream.ts" +import { BASE64_SENTINEL_PREFIX, BASE64_SENTINEL_SUFFIX } from "./internal/mcpProtocol.ts" +import { + ClosedError, + ConfigurationError, + HttpError, + isMcpClientError, + LimitError, + McpClientError, + type McpClientErrorReason, + ProtocolError, + TimeoutError, + ToolError, + TransportError, + UnsupportedError, + ValidationError +} from "./McpClientError.ts" +import type * as McpProtocol from "./McpProtocol.ts" +import * as McpSchema from "./McpSchema.ts" +import * as Tool from "./Tool.ts" +import * as Toolkit from "./Toolkit.ts" + +// ============================================================================= +// Models +// ============================================================================= + +/** + * Supported MCP wire protocol revisions. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export type ProtocolVersion = "2025-11-25" | "2026-07-28" + +/** + * Service tag for applications that provide one default MCP connection. + * + * @stability experimental + * @category services + * @since 4.0.6 + */ +export const McpClient: Context.Service = Context.Service("effect/ai/McpClient") + +/** + * A scoped connection to one MCP server. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface McpClient { + readonly [TypeId]: typeof TypeId + readonly protocolVersion: ProtocolVersion + readonly serverInfo: McpSchema.Implementation | undefined + readonly instructions?: string | undefined + /** + * Returns every tool page as one snapshot, bounded by the deadline, 100 pages and 10,000 tools. + * `timeout` overrides the client's discovery deadline. + */ + readonly listTools: ( + options?: { readonly timeout?: Duration.Input | undefined } + ) => Effect.Effect, McpClientError> + /** Returns every prompt page as one snapshot with the same bounds as `listTools`. */ + readonly listPrompts: ( + options?: { readonly timeout?: Duration.Input | undefined } + ) => Effect.Effect, McpClientError> + /** Gets the messages for a discovered prompt. Unbounded unless `timeout` is supplied. */ + readonly getPrompt: ( + params: { + readonly prompt: McpSchema.Prompt + readonly arguments?: typeof McpSchema.GetPrompt.payloadSchema.Type["arguments"] + }, + options?: { readonly timeout?: Duration.Input | undefined } + ) => Effect.Effect + /** Returns every resource page as one snapshot with the same bounds as `listTools`. */ + readonly listResources: ( + options?: { readonly timeout?: Duration.Input | undefined } + ) => Effect.Effect, McpClientError> + /** Reads the complete contents of a resource URI. Unbounded unless `timeout` is supplied. */ + readonly readResource: ( + params: { readonly uri: string }, + options?: { readonly timeout?: Duration.Input | undefined } + ) => Effect.Effect + /** + * Calls a discovered tool, optionally decoding with a schema. Errors retain the full result. + * Unbounded unless `timeout` is supplied. + */ + readonly callTool: { + ( + params: { + readonly tool: McpSchema.Tool + readonly schema?: undefined + readonly arguments?: typeof McpSchema.CallTool.payloadSchema.Type["arguments"] + }, + options?: { readonly timeout?: Duration.Input | undefined } + ): Effect.Effect + ( + params: { + readonly tool: McpSchema.Tool + readonly schema: S + readonly arguments?: typeof McpSchema.CallTool.payloadSchema.Type["arguments"] + }, + options?: { readonly timeout?: Duration.Input | undefined } + ): Effect.Effect + ( + params: { + readonly tool: McpSchema.Tool + readonly schema?: S | undefined + readonly arguments?: typeof McpSchema.CallTool.payloadSchema.Type["arguments"] + }, + options?: { readonly timeout?: Duration.Input | undefined } + ): Effect.Effect + } +} + +/** + * Returns whether a value is an MCP client. + * + * @stability experimental + * @category guards + * @since 4.0.6 + */ +export const isMcpClient = (value: unknown): value is McpClient => Predicate.hasProperty(value, TypeId) + +// ============================================================================= +// Constructors +// ============================================================================= + +/** + * Constructs and connects a client using the supplied MCP transport service and its protocol adapter. + * + * **Details** + * + * `name`, `version` and the optional `title`, `description`, `websiteUrl` and + * `icons` identify the client to the server. `timeout` defaults to 60 seconds + * and bounds initialization and each complete `listTools`, `listPrompts` or + * `listResources` scan. `callTool`, `getPrompt` and `readResource` have no + * deadline unless their own `timeout` option is supplied. + * Schema decoding requires the schema's services from the caller. + * + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const make: (options: { + readonly name: string + readonly version: string + readonly title?: string | undefined + readonly description?: string | undefined + readonly websiteUrl?: string | undefined + readonly icons?: ReadonlyArray | undefined + readonly timeout?: Duration.Input | undefined +}) => Effect.Effect< + McpClient, + McpClientError, + Scope.Scope | Transport +> = Effect.fnUntraced(function*(options: Options): Effect.fn.Return< + McpClient, + McpClientError, + Scope.Scope | Transport +> { + const transport = yield* Transport + const protocol = transport.protocol + const version = protocol.protocolVersion + const modern = version === "2026-07-28" + const timeout = options.timeout ?? DEFAULT_TIMEOUT + const closed = yield* Deferred.make() + yield* Effect.addFinalizer(() => + Deferred.fail(closed, fail(new ClosedError({ description: "MCP client scope closed" }))) + ) + let nextId = 1 + + const clientInfo = encodeImplementation({ + name: options.name, + title: options.title, + version: options.version, + description: options.description, + websiteUrl: options.websiteUrl, + icons: options.icons + }) + + // Modern requests carry the client identity in every request. + // https://modelcontextprotocol.io/specification/2026-07-28/basic/index + const meta = { + "io.modelcontextprotocol/protocolVersion": version, + "io.modelcontextprotocol/clientInfo": clientInfo, + "io.modelcontextprotocol/clientCapabilities": {} + } + + const request = Effect.fnUntraced(function*( + method: string, + params: Record, + tool?: McpSchema.Tool + ): Effect.fn.Return { + const rpc = protocol.clientRpcs.requests.get(method) as Rpc.AnyWithProps | undefined + if (rpc === undefined) { + return yield* fail(new UnsupportedError({ description: `Unsupported MCP method: ${method}` })) + } + + const codecs = protocol.payloadCodecs(rpc) + const payload = yield* codecs.decode(modern ? { ...params, _meta: meta } : params).pipe( + Effect.flatMap(codecs.encode), + Effect.mapError(validationError) + ) + + const response = yield* transport.request( + { jsonrpc: "2.0", id: nextId++, method, params: payload as Readonly> }, + tool + ) + if ("error" in response) { + return yield* fail( + new ProtocolError({ + code: response.error.code, + data: response.error.data, + description: response.error.message + }) + ) + } + + let result: unknown = response.result + // Older modern peers may omit the complete discriminator. + if (modern && isRecord(result) && result.resultType === undefined) result = { ...result, resultType: "complete" } + if (method === "tools/list" && isRecord(result) && Array.isArray(result.tools)) { + result = { ...result, tools: result.tools.filter((tool) => !requiresTasks(tool)) } + } + + const validated = yield* decodeUnknown(rpc.successSchema, result) + if (isRecord(result) && result.resultType === "input_required") { + return yield* fail(new UnsupportedError({ description: "MCP input continuation is not supported" })) + } + const publicRpc = publicRpcs.get(method) + return (publicRpc === undefined ? validated : yield* decodeUnknown(publicRpc.successSchema, result)) as A + }) + + // Initialization and discovery default to `timeout`; other operations are bounded only on request. + const withDeadline = ( + effect: Effect.Effect, + operation: string, + duration: Duration.Input | undefined + ) => { + const open = Effect.raceFirst(effect, Deferred.await(closed)) + return duration === undefined ? open : open.pipe( + Effect.timeoutOrElse({ + duration, + orElse: () => Effect.fail(fail(new TimeoutError({ operation }))) + }) + ) + } + + const initialize = Effect.gen(function*() { + if (!modern) { + return yield* request("initialize", { + protocolVersion: version, + capabilities: {}, + clientInfo + }) + } + const discover = request("server/discover", {}) + // Retry once when the server rejects a version that it reports as supported. + return yield* discover.pipe(Effect.catchIf((error) => { + const reason = error.reason + if (reason._tag !== "ProtocolError" && reason._tag !== "HttpError") return false + const rejection = Option.getOrUndefined(decodeVersionRejection(reason.data)) + return reason.code === McpSchema.UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE && rejection?.requested === version && + rejection.supported.includes(version) + }, () => discover)) + }) + + const { capabilities, instructions, serverInfo } = yield* withDeadline( + Effect.gen(function*() { + const result = yield* initialize + if (modern ? !result.supportedVersions?.includes(version) : result.protocolVersion !== version) { + return yield* fail( + new UnsupportedError({ description: "Server does not support the explicitly selected protocol version" }) + ) + } + const identity = modern ? result._meta?.["io.modelcontextprotocol/serverInfo"] : result.serverInfo + const capabilities = yield* decodeServerCapabilities(result.capabilities) + const serverInfo = identity === undefined ? undefined : yield* decodeImplementation(identity) + if (!modern) yield* transport.notify({ jsonrpc: "2.0", method: "notifications/initialized" }) + return { capabilities, instructions: result.instructions, serverInfo } + }).pipe( + Effect.mapError((cause) => + Schema.isSchemaError(cause) + ? fail(new ProtocolError({ description: "Invalid MCP initialization result", cause })) + : cause + ) + ), + "initialization", + timeout + ) + + // Retrieval requires the capability; discovery treats a missing one as empty. + const call = ( + kind: Capability, + method: string, + params: Record, + tool?: McpSchema.Tool + ): Effect.Effect => + capabilities[kind] === undefined + ? Effect.fail(fail(new UnsupportedError({ description: `Server does not advertise ${kind}` }))) + : request(method, params, tool) + + const list = (kind: K) => + Effect.fnUntraced( + function*(_options?: CallOptions) { + if (capabilities[kind] === undefined) return [] + const items: Array = [] + const cursors = new Set() + let cursor: string | undefined + for (let page = 0; page < MAX_DISCOVERY_PAGES; page++) { + const result = yield* request>(`${kind}/list`, cursor === undefined ? {} : { cursor }) + if (items.length + result[kind].length > MAX_DISCOVERY_ITEMS) { + return yield* limitError(`MCP ${kind} discovery exceeded its item limit`) + } + items.push(...result[kind]) + if (result.nextCursor === undefined) return items + if (cursors.has(result.nextCursor)) { + return yield* limitError(`MCP ${kind} discovery repeated a cursor`) + } + cursors.add(cursor = result.nextCursor) + } + return yield* limitError(`MCP ${kind} discovery exceeded its page limit`) + }, + (effect, callOptions) => withDeadline(effect, `${kind} discovery`, callOptions?.timeout ?? timeout) + ) + + const listTools = list<"tools", McpSchema.Tool>("tools") + + const callTool = Effect.fnUntraced( + function*( + params: { + readonly tool: McpSchema.Tool + readonly schema?: S | undefined + readonly arguments?: typeof McpSchema.CallTool.payloadSchema.Type["arguments"] + }, + _options?: CallOptions + ) { + const result = yield* call( + "tools", + "tools/call", + { name: params.tool.name, ...(params.arguments === undefined ? {} : { arguments: params.arguments }) }, + params.tool + ) + if (params.schema === undefined) return result + if (result.isError === true) { + return yield* fail(new ToolError({ result })) + } + return yield* Schema.decodeUnknownEffect(params.schema)(result.structuredContent).pipe( + Effect.mapError((cause) => + fail(new ValidationError({ description: "MCP structured result validation failed", cause })) + ) + ) + }, + (effect, _params, callOptions) => withDeadline(effect, "tools/call", callOptions?.timeout) + ) + + return { + [TypeId]: TypeId, + protocolVersion: version, + serverInfo, + instructions, + listTools: (callOptions) => + listTools(callOptions).pipe(Effect.map((tools) => tools.filter((tool) => transport.isCallable(tool)))), + listPrompts: list("prompts"), + listResources: list("resources"), + getPrompt: (params, callOptions) => + withDeadline( + call("prompts", "prompts/get", { + name: params.prompt.name, + ...(params.arguments === undefined ? {} : { arguments: params.arguments }) + }), + "prompts/get", + callOptions?.timeout + ), + readResource: (params, callOptions) => + withDeadline( + call("resources", "resources/read", { uri: params.uri }), + "resources/read", + callOptions?.timeout + ), + callTool: callTool as McpClient["callTool"] + } +}) + +// ============================================================================= +// Toolkit +// ============================================================================= + +/** + * Discovers remote tools and binds their handlers using the standard Toolkit implementation. + * + * **Details** + * + * Pass an optional prefix such as `"reports__"` to qualify model tool names. + * Remote JSON Schemas are advertised without local schema import. Results become + * text; tool-reported and operational failures fail the Effect. Use `Tool.make` + * or `Tool.dynamic` with `Toolkit.make` for custom validation, approval or results. + * Build a new toolkit to discover changes between model turns. + * + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const toolkit: (client: McpClient, prefix?: string) => Effect.Effect< + Toolkit.WithHandler< + Record< + string, + Tool.Tool + > + >, + McpClientError +> = Effect.fnUntraced(function*(client: McpClient, prefix = "") { + const descriptors = yield* client.listTools() + const tools: Array< + Tool.Tool + > = [] + const handlers: Record Effect.Effect> = Object.create(null) + + for (const descriptor of descriptors) { + const name = `${prefix}${descriptor.name}` + if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name) || Object.hasOwn(handlers, name)) { + return yield* fail(new ConfigurationError({ description: `Invalid or duplicate MCP model tool name: ${name}` })) + } + tools.push( + Tool.dynamic(name, { + parameters: descriptor.inputSchema, + description: descriptor.description, + success: Schema.String, + failure: McpClientError + }).annotate(Tool.Strict, false) + ) + handlers[name] = Effect.fnUntraced(function*(params: unknown) { + const args = yield* decodeToolArguments(params).pipe( + Effect.mapError((cause) => + fail(new ValidationError({ description: "MCP tool arguments must be a JSON object", cause })) + ) + ) + const result = yield* client.callTool({ tool: descriptor, arguments: args }) + if (result.isError === true) { + return yield* fail(new ToolError({ result })) + } + return yield* toText(result) + }) + } + + const toolkit = Toolkit.make(...tools) + return yield* toolkit.pipe(Effect.provideContext(yield* toolkit.toHandlers(handlers))) +}) + +// ============================================================================= +// Transports +// ============================================================================= + +/** + * Service supplied by the built-in MCP transport constructors and layers. + * + * **Details** + * + * Transport construction owns cleanup through its Effect scope. + * + * @stability experimental + * @category services + * @since 4.0.6 + */ +export const Transport: Context.Service = Context.Service( + "effect/ai/McpClient/Transport" +) + +/** + * Exchanges JSON-RPC messages with one MCP server using its protocol adapter. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Transport { + readonly [TransportTypeId]: typeof TransportTypeId + readonly protocol: McpProtocol.ProtocolAdapter + /** Sends a request and waits for its response. `tool` supplies header routing for tool calls. */ + readonly request: ( + message: McpSchema.JsonRpcRequest, + tool?: McpSchema.Tool + ) => Effect.Effect + /** Sends a notification without waiting for a reply. */ + readonly notify: (message: McpSchema.JsonRpcNotification) => Effect.Effect + /** Returns whether the transport can route a call to the tool. */ + readonly isCallable: (tool: McpSchema.Tool) => boolean +} + +/** + * Starts a scoped subprocess using newline-delimited JSON-RPC. + * + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const stdio: (options: { + readonly command: ChildProcess.Command + readonly protocol: McpProtocol.ProtocolAdapter + readonly maxMessageBytes?: ByteSize.Input | undefined +}) => Effect.Effect = Effect.fnUntraced( + function*(options: { + readonly command: ChildProcess.Command + readonly protocol: Protocol + readonly maxMessageBytes?: ByteSize.Input | undefined + }): Effect.fn.Return { + const maxBytes = yield* messageLimit(options.maxMessageBytes) + const spawner = yield* ChildProcessSpawner + const process = yield* spawner.spawn(options.command).pipe( + Effect.mapError((cause) => fail(new TransportError({ description: "Cannot spawn MCP server", cause }))) + ) + + const pending = new Map>() + const failure = yield* Deferred.make() + let failed: McpClientError | undefined + + const close = (error: McpClientError) => + Effect.suspend(() => { + failed ??= error + return Deferred.fail(failure, failed) + }) + + const input = yield* Queue.unbounded() + + const write = Effect.fnUntraced(function*(message: McpSchema.JsonRpcMessage) { + if (failed !== undefined) return yield* failed + const json = yield* encodeMessage(message, maxBytes) + yield* Queue.offer(input, json + "\n") + }) + + const receive = Effect.fnUntraced(function*(message: McpSchema.JsonRpcMessage) { + if (isRequest(message)) return yield* write(replyToServer(message)) + if (!isResponse(message)) return + // Without an ID the error cannot be correlated, so no pending request can be trusted. + if (message.id === undefined || message.id === null) { + return yield* fail(new ProtocolError({ description: "Server returned an uncorrelated JSON-RPC error" })) + } + const response = pending.get(message.id) + if (response !== undefined) yield* Deferred.succeed(response, message) + }) + + yield* Stream.fromQueue(input).pipe( + Stream.encodeText, + Stream.run(process.stdin), + Effect.catch((cause) => close(fail(new TransportError({ description: "MCP stdin failed", cause })))), + Effect.forkScoped + ) + yield* Stream.runDrain(process.stderr).pipe(Effect.ignore, Effect.forkScoped) + const drained = yield* Deferred.make() + let buffer = "" + yield* process.stdout.pipe( + Stream.decodeText(), + Stream.runForEach(Effect.fnUntraced(function*(chunk) { + buffer += chunk + let start = 0 + let index: number + while ((index = buffer.indexOf("\n", start)) >= 0) { + const line = buffer.slice(start, index) + start = index + 1 + if (line.trim() !== "") yield* receive(yield* parseMessage(line, maxBytes)) + } + buffer = buffer.slice(start) + if (buffer.length > maxBytes) { + return yield* limitError("MCP message exceeds maxMessageBytes") + } + })), + Effect.mapError((cause) => + isMcpClientError(cause) + ? cause + : fail(new TransportError({ description: "MCP stdout failed", cause })) + ), + Effect.andThen(Effect.fail(fail(new TransportError({ description: "MCP server closed stdout" })))), + Effect.catch(close), + Effect.ensuring(Deferred.succeed(drained, undefined)), + Effect.forkScoped + ) + // Output written just before exit is delivered before pending requests fail. + yield* process.exitCode.pipe( + Effect.map((code) => `MCP process exited (${code})`), + Effect.orElseSucceed(() => "MCP process exited"), + Effect.tap(() => Deferred.await(drained).pipe(Effect.timeoutOption(CLOSE_TIMEOUT))), + Effect.flatMap((description) => close(fail(new TransportError({ description })))), + Effect.forkScoped + ) + + // Close stdin, then escalate to SIGTERM and SIGKILL if the server does not exit. + // https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#stdio + yield* Effect.addFinalizer(Effect.fnUntraced(function*() { + yield* close(fail(new ClosedError({ description: "MCP transport closed" }))) + yield* Queue.end(input) + const exited = yield* process.exitCode.pipe(Effect.timeoutOption(CLOSE_TIMEOUT), Effect.option) + if (Option.isNone(exited) || Option.isNone(exited.value)) { + yield* process.kill({ killSignal: "SIGTERM", forceKillAfter: Duration.toMillis(CLOSE_TIMEOUT) }).pipe( + Effect.ignore + ) + } + })) + + return { + [TransportTypeId]: TransportTypeId, + protocol: options.protocol, + isCallable: () => true, + notify: write, + request: Effect.fnUntraced(function*(message) { + const response = yield* Deferred.make() + pending.set(message.id, response) + + return yield* write(message).pipe( + Effect.andThen(Deferred.await(response)), + Effect.raceFirst(Deferred.await(failure)), + Effect.onInterrupt(() => + message.method === "initialize" + ? Effect.void + : Deferred.isDone(response).pipe( + Effect.flatMap((done) => done ? Effect.void : write(cancelled(message.id)).pipe(Effect.ignore)) + ) + ), + Effect.ensuring(Effect.sync(() => pending.delete(message.id))) + ) + }) + } + } +) + +/** + * Provides a connected MCP client over stdio using the child process spawner. + * + * **Details** + * + * The layer scope owns the client and subprocess. The options are those of + * {@link make} and {@link stdio}. + * + * @see {@link layerHttp} + * @stability experimental + * @category layers + * @since 4.0.6 + */ +export const layerStdio: (options: { + readonly name: string + readonly version: string + readonly title?: string | undefined + readonly description?: string | undefined + readonly websiteUrl?: string | undefined + readonly icons?: ReadonlyArray | undefined + readonly timeout?: Duration.Input | undefined + readonly command: ChildProcess.Command + readonly protocol: McpProtocol.ProtocolAdapter + readonly maxMessageBytes?: ByteSize.Input | undefined +}) => Layer.Layer< + McpClient, + McpClientError, + ChildProcessSpawner +> = (options) => Layer.effect(McpClient)(make(options)).pipe(Layer.provide(Layer.effect(Transport)(stdio(options)))) + +/** + * Creates a Streamable HTTP transport with request-owned JSON or SSE responses. + * + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const http: (options: { + readonly url: string | URL + readonly protocol: McpProtocol.ProtocolAdapter + readonly headers?: Readonly> | undefined + readonly maxMessageBytes?: ByteSize.Input | undefined +}) => Effect.Effect = Effect.fnUntraced( + function*(options: { + readonly url: string | URL + readonly protocol: Protocol + readonly headers?: Readonly> | undefined + readonly maxMessageBytes?: ByteSize.Input | undefined + }): Effect.fn.Return { + const maxBytes = yield* messageLimit(options.maxMessageBytes) + const client = HttpClient.withScope(yield* HttpClient.HttpClient) + const modern = options.protocol.protocolVersion === "2026-07-28" + const closed = yield* Deferred.make() + let failed: McpClientError | undefined + // Legacy servers may assign a session during initialization. + let session: string | undefined + + const baseHeaders = { + ...options.headers, + accept: "application/json, text/event-stream", + "mcp-protocol-version": options.protocol.protocolVersion + } + const headers = (): Record => + session === undefined ? baseHeaders : { ...baseHeaders, "mcp-session-id": session } + + const httpError = Effect.fnUntraced(function*(response: HttpClientResponse.HttpClientResponse) { + // Error bodies may carry a JSON-RPC error without an ID, e.g. an unsupported protocol version. + const remote = mediaType(response) === "application/json" + ? yield* readBody(response, maxBytes).pipe( + Effect.flatMap((text) => parseMessage(text, maxBytes)), + Effect.map((message) => "error" in message ? message.error : undefined), + Effect.orElseSucceed(() => undefined) + ) + : undefined + return yield* fail( + new HttpError({ + status: response.status, + code: remote?.code, + data: remote?.data, + description: remote?.message, + wwwAuthenticate: response.headers["www-authenticate"], + retryAfter: response.headers["retry-after"] + }) + ) + }) + + // Posts one message and returns the successful response, tracking legacy sessions. + const send = Effect.fnUntraced(function*(message: McpSchema.JsonRpcMessage, tool?: McpSchema.Tool) { + if (failed !== undefined) return yield* failed + const body = yield* encodeMessage(message, maxBytes) + const request = HttpClientRequest.post(options.url).pipe( + HttpClientRequest.setHeaders({ ...headers(), ...(modern ? yield* routingHeaders(message, tool) : {}) }), + HttpClientRequest.bodyText(body, "application/json") + ) + const response = yield* client.execute(request).pipe( + // Clients using `filterStatusOk` surface error responses as failures. + Effect.catch((cause) => + cause.response === undefined + ? Effect.fail(fail(new TransportError({ description: "MCP HTTP request failed", cause }))) + : Effect.succeed(cause.response) + ) + ) + + // A legacy session ends when the server no longer recognizes its ID. + if (session !== undefined && response.status === 404) { + failed ??= fail(new ClosedError({ sessionExpired: true })) + return yield* failed + } + if (response.status < 200 || response.status >= 300) return yield* httpError(response) + if (!modern) session = response.headers["mcp-session-id"] ?? session + return response + }) + + const exchange = (effect: Effect.Effect) => + effect.pipe( + Effect.mapError((cause) => + isMcpClientError(cause) + ? cause + : fail(new TransportError({ description: "Cannot read MCP response", cause })) + ), + Effect.raceFirst(Deferred.await(closed)), + Effect.scoped + ) + + const notify = (message: McpSchema.JsonRpcNotification | McpSchema.JsonRpcResponse) => + exchange(Effect.asVoid(send(message))) + + const readResponse = Effect.fnUntraced(function*( + response: HttpClientResponse.HttpClientResponse, + request: McpSchema.JsonRpcRequest + ) { + const match = Effect.fnUntraced(function*(message: McpSchema.JsonRpcMessage) { + if (isRequest(message)) { + yield* notify(replyToServer(message)) + return undefined + } + if (!isResponse(message)) return undefined + // A response on this exchange belongs to its request; an error may omit the ID. + if (message.id !== request.id && !("error" in message && message.id == null)) { + return yield* fail(new ProtocolError({ description: "HTTP MCP response id does not match its request" })) + } + return message + }) + + switch (mediaType(response)) { + case "application/json": { + const message = yield* match(yield* parseMessage(yield* readBody(response, maxBytes), maxBytes)) + if (message === undefined) break + return message + } + case "text/event-stream": { + const events: Array = [] + const parser = Sse.makeParser((event) => { + // Resumption is unsupported, so retry directives and event IDs are ignored. + if (event._tag === "Event" && event.data !== "") events.push(event.data) + }, { maxEventSize: maxBytes }) + const message = yield* response.stream.pipe( + Stream.decodeText(), + Stream.flatMap((chunk) => { + const error = parser.feed(chunk) + const parsed = Stream.fromIterable(events.splice(0)) + // Events before an oversized one can still complete the request. + return error === undefined ? parsed : parsed.pipe( + Stream.concat(Stream.fail(limitError("MCP SSE event exceeds maxMessageBytes"))) + ) + }), + Stream.mapEffect((data) => parseMessage(data, maxBytes).pipe(Effect.flatMap(match))), + Stream.filter(Predicate.isNotUndefined), + Stream.runHead + ) + if (Option.isNone(message)) break + return message.value + } + default: { + return yield* fail(new ProtocolError({ description: "Unsupported MCP response Content-Type" })) + } + } + return yield* fail(new TransportError({ description: "MCP response ended before the terminal response" })) + }) + + yield* Effect.addFinalizer(Effect.fnUntraced(function*() { + failed ??= fail(new ClosedError({ description: "MCP HTTP transport closed" })) + yield* Deferred.fail(closed, failed) + if (session === undefined || failed.reason._tag !== "ClosedError" || failed.reason.sessionExpired) return + // Explicitly terminate a legacy session. + // https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management + yield* client.execute(HttpClientRequest.delete(options.url).pipe(HttpClientRequest.setHeaders(headers()))).pipe( + Effect.scoped, + Effect.timeoutOption(CLOSE_TIMEOUT), + Effect.ignore + ) + })) + + return { + [TransportTypeId]: TransportTypeId, + protocol: options.protocol, + isCallable: (tool) => !modern || headerParameters(tool) !== undefined, + notify, + request: (message, tool) => + exchange( + send(message, tool).pipe( + Effect.flatMap((response) => + response.status === 202 || response.status === 204 + ? fail(new ProtocolError({ description: "MCP request received no response" })) + : readResponse(response, message) + ) + ) + ).pipe( + // Modern servers treat a closed response stream as cancellation. + Effect.onInterrupt(() => + modern || message.method === "initialize" + ? Effect.void + : notify(cancelled(message.id)).pipe(Effect.timeoutOption(CLOSE_TIMEOUT), Effect.ignore) + ) + ) + } + } +) + +/** + * Provides a connected MCP client over Streamable HTTP using the HTTP client. + * + * **Details** + * + * The layer scope owns the client and connection. The options are those of + * {@link make} and {@link http}. + * + * **Example** (Providing an HTTP client connection) + * + * ```ts import.meta.vitest + * import { Layer } from "effect" + * import { McpClient, McpProtocol } from "effect/ai" + * import { FetchHttpClient } from "effect/http" + * + * const ClientLayer = McpClient.layerHttp({ + * name: "report-workflow", + * version: "1.0.0", + * protocol: McpProtocol.v2026_07_28, + * url: "http://localhost:3000/mcp" + * }).pipe(Layer.provide(FetchHttpClient.layer)) + * ``` + * + * @see {@link layerStdio} + * @stability experimental + * @category layers + * @since 4.0.6 + */ +export const layerHttp: (options: { + readonly name: string + readonly version: string + readonly title?: string | undefined + readonly description?: string | undefined + readonly websiteUrl?: string | undefined + readonly icons?: ReadonlyArray | undefined + readonly timeout?: Duration.Input | undefined + readonly url: string | URL + readonly protocol: McpProtocol.ProtocolAdapter + readonly headers?: Readonly> | undefined + readonly maxMessageBytes?: ByteSize.Input | undefined +}) => Layer.Layer< + McpClient, + McpClientError, + HttpClient.HttpClient +> = (options) => Layer.effect(McpClient)(make(options)).pipe(Layer.provide(Layer.effect(Transport)(http(options)))) + +// ============================================================================= +// Internal +// ============================================================================= + +const TypeId = "~effect/ai/McpClient" as const +const TransportTypeId = "~effect/ai/McpClient/Transport" as const +const DEFAULT_MAX_MESSAGE_BYTES = ByteSize.mebibytes(16) +const DEFAULT_TIMEOUT = Duration.seconds(60) +const CLOSE_TIMEOUT = Duration.seconds(5) +const MAX_DISCOVERY_PAGES = 100 +const MAX_DISCOVERY_ITEMS = 10_000 +const HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/ +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +type Protocol = McpProtocol.ProtocolAdapter + +type Options = Parameters[0] + +type Capability = "tools" | "prompts" | "resources" + +interface CallOptions { + readonly timeout?: Duration.Input | undefined +} + +interface InitializeResult { + readonly protocolVersion?: string + readonly supportedVersions?: ReadonlyArray + readonly capabilities: unknown + readonly serverInfo?: unknown + readonly instructions?: string + readonly _meta?: Record +} + +type ListResult = Record> & { readonly nextCursor?: string } +const fail = (reason: McpClientErrorReason) => new McpClientError({ reason }) +const limitError = (description: string) => fail(new LimitError({ description })) + +const validationError = (cause: Schema.SchemaError) => + fail(new ValidationError({ description: "Invalid MCP payload", cause })) + +const isRecord = Predicate.isReadonlyObject +const decodeMessage = Schema.decodeUnknownEffect(McpSchema.JsonRpcMessage, { onExcessProperty: "error" }) + +const decodeVersionRejection = Schema.decodeUnknownOption(Schema.Struct({ + requested: Schema.String, + supported: Schema.Array(Schema.String) +})) + +const decodeServerCapabilities = Schema.decodeUnknownEffect(McpSchema.ServerCapabilities) +const decodeImplementation = Schema.decodeUnknownEffect(McpSchema.Implementation) +const encodeImplementation = Schema.encodeSync(McpSchema.Implementation) + +// The protocol payload codec validates argument values; the toolkit only checks for an object. +const decodeToolArguments = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown)) + +const decodeUnknown = (schema: Schema.Constraint, value: unknown) => + Schema.decodeUnknownEffect(schema as Schema.ConstraintCodec)(value).pipe( + Effect.mapError(validationError) + ) + +const publicRpcs: ReadonlyMap = + McpSchema.ClientRequestRpcs.requests + +// Tools that require task-augmented execution cannot be called without the tasks extension. +const requiresTasks = (tool: unknown) => + isRecord(tool) && isRecord(tool.execution) && tool.execution.taskSupport === "required" + +const messageLimit = (input: ByteSize.Input = DEFAULT_MAX_MESSAGE_BYTES) => { + const bytes = Option.flatMap(ByteSize.fromInput(input), ByteSize.toNumber) + return Option.isSome(bytes) && bytes.value > 0 + ? Effect.succeed(bytes.value) + : Effect.fail(fail(new ConfigurationError({ description: "maxMessageBytes must be a positive safe integer" }))) +} + +const encodeMessage = Effect.fnUntraced(function*(message: McpSchema.JsonRpcMessage, maxBytes: number) { + const json = yield* Effect.try({ + try: () => JSON.stringify(message), + catch: (cause) => fail(new TransportError({ description: "Cannot encode MCP message", cause })) + }) + if (encoder.encode(json).length > maxBytes) { + return yield* limitError("MCP message exceeds maxMessageBytes") + } + return json +}) + +const parseMessage = Effect.fnUntraced(function*(text: string, maxBytes: number) { + if (encoder.encode(text).length > maxBytes) { + return yield* limitError("MCP message exceeds maxMessageBytes") + } + const json = yield* Effect.try({ + try: () => JSON.parse(text) as unknown, + catch: (cause) => fail(new ProtocolError({ description: "Invalid MCP JSON", cause })) + }) + return yield* decodeMessage(json).pipe( + Effect.mapError((cause) => fail(new ProtocolError({ description: "Invalid JSON-RPC message", cause }))) + ) +}) + +const isRequest = (message: McpSchema.JsonRpcMessage): message is McpSchema.JsonRpcRequest => + "method" in message && "id" in message + +const isResponse = (message: McpSchema.JsonRpcMessage): message is McpSchema.JsonRpcResponse => !("method" in message) + +// The client advertises no capabilities, so it answers pings and declines every other server request. +const replyToServer = (request: McpSchema.JsonRpcRequest): McpSchema.JsonRpcResponse => + request.method === "ping" + ? { jsonrpc: "2.0", id: request.id, result: {} } + : { + jsonrpc: "2.0", + id: request.id, + error: { code: McpSchema.METHOD_NOT_FOUND_ERROR_CODE, message: `Unsupported MCP method: ${request.method}` } + } + +const cancelled = (id: McpSchema.RequestId): McpSchema.JsonRpcNotification => ({ + jsonrpc: "2.0", + method: "notifications/cancelled", + params: { requestId: id } +}) + +// ============================================================================= +// Internal: Streamable HTTP +// ============================================================================= + +const mediaType = (response: HttpClientResponse.HttpClientResponse) => + response.headers["content-type"]?.split(";", 1)[0].trim().toLowerCase() + +const readBody = (response: HttpClientResponse.HttpClientResponse, maxBytes: number) => + response.stream.pipe( + Stream.mapError((cause) => fail(new TransportError({ description: "Cannot read MCP response", cause }))), + Stream.limitBytes(maxBytes, () => Stream.fail(limitError("MCP response exceeds maxMessageBytes"))), + Stream.mkUint8Array, + Effect.map((bytes) => decoder.decode(bytes)) + ) + +// Values that are not plain visible ASCII are sent Base64 encoded. +// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#custom-headers-from-tool-parameters +const headerValue = (value: string) => + /^[\x20-\x7e]*$/.test(value) && value.trim() === value && + !(value.startsWith(BASE64_SENTINEL_PREFIX) && value.endsWith(BASE64_SENTINEL_SUFFIX)) + ? value + : `${BASE64_SENTINEL_PREFIX}${Base64.encode(value)}${BASE64_SENTINEL_SUFFIX}` + +interface HeaderParameter { + readonly path: ReadonlyArray + readonly header: string + readonly type: string +} + +// JSON Schema keywords whose values are schemas, maps of schemas or arrays of schemas. +const SCHEMA_KEYWORDS = new Set([ + "additionalProperties", + "unevaluatedProperties", + "propertyNames", + "items", + "additionalItems", + "unevaluatedItems", + "contains", + "not", + "if", + "then", + "else", + "contentSchema" +]) + +const SCHEMA_MAP_KEYWORDS = new Set([ + "properties", + "patternProperties", + "$defs", + "definitions", + "dependentSchemas", + "dependencies" +]) + +const SCHEMA_ARRAY_KEYWORDS = new Set(["allOf", "anyOf", "oneOf", "prefixItems", "items"]) + +// Collects `x-mcp-header` annotations along chains of `properties` keys, or +// returns undefined when an annotation is invalid or found anywhere else. +// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extension +const headerParameters = (tool: McpSchema.Tool): ReadonlyArray | undefined => { + const parameters: Array = [] + const names = new Set() + + // `path` is undefined once the traversal leaves a properties-only chain. + const visit = (schema: unknown, path: ReadonlyArray | undefined): boolean => { + if (!isRecord(schema)) return true + const name = schema["x-mcp-header"] + if (name !== undefined) { + if ( + path === undefined || path.length === 0 || typeof name !== "string" || !HEADER_TOKEN.test(name) || + names.has(name.toLowerCase()) || !["string", "integer", "boolean"].includes(schema.type as string) + ) return false + names.add(name.toLowerCase()) + parameters.push({ path, header: `mcp-param-${name.toLowerCase()}`, type: schema.type as string }) + } + return Object.entries(schema).every(([keyword, value]) => { + if (SCHEMA_MAP_KEYWORDS.has(keyword) && isRecord(value)) { + return Object.entries(value).every(([key, child]) => + visit(child, keyword === "properties" && path !== undefined ? [...path, key] : undefined) + ) + } + if (Array.isArray(value)) { + return !SCHEMA_ARRAY_KEYWORDS.has(keyword) || value.every((child) => visit(child, undefined)) + } + return !SCHEMA_KEYWORDS.has(keyword) || visit(value, undefined) + }) + } + + return visit(tool.inputSchema, []) ? parameters : undefined +} + +const routingHeaders = Effect.fnUntraced(function*(message: McpSchema.JsonRpcMessage, tool?: McpSchema.Tool) { + if (!("method" in message)) return {} + const headers: Record = { "mcp-method": message.method } + const params = message.params ?? {} + const name = message.method === "resources/read" ? params.uri : params.name + if ( + typeof name === "string" && + (message.method === "tools/call" || message.method === "prompts/get" || message.method === "resources/read") + ) { + headers["mcp-name"] = headerValue(name) + } + if (message.method !== "tools/call") return headers + + const parameters = tool === undefined ? undefined : headerParameters(tool) + if (parameters === undefined) { + return yield* fail(new UnsupportedError({ description: "Tool has invalid x-mcp-header annotations" })) + } + for (const { header, path, type } of parameters) { + let value: unknown = params.arguments + for (const key of path) value = isRecord(value) && Object.hasOwn(value, key) ? value[key] : undefined + if (value === undefined || value === null) continue + if (type === "integer" ? !Number.isSafeInteger(value) : typeof value !== type) { + return yield* fail(new ValidationError({ description: `Invalid value for MCP header parameter ${header}` })) + } + headers[header] = headerValue(String(value)) + } + return headers +}) + +// ============================================================================= +// Internal: Toolkit +// ============================================================================= + +const toText = (result: McpSchema.CallToolResult): Effect.Effect => { + const unsupported = (content: string) => + Effect.fail( + fail(new UnsupportedError({ description: `MCP ${content} requires an application text converter`, result })) + ) + const parts: Array = [] + for (const block of result.content) { + switch (block.type) { + case "text": { + parts.push(block.text) + break + } + case "resource_link": { + parts.push( + `${block.title ?? block.name}: ${block.uri}${block.description === undefined ? "" : `\n${block.description}`}` + ) + break + } + case "resource": { + if (!("text" in block.resource)) return unsupported("binary resources") + parts.push(block.resource.text) + break + } + default: { + return unsupported(`${block.type} content`) + } + } + } + return Effect.succeed( + parts.length === 0 && result.structuredContent !== undefined + ? JSON.stringify(result.structuredContent) + : parts.join("\n") + ) +} diff --git a/packages/effect/src/ai/McpClientError.ts b/packages/effect/src/ai/McpClientError.ts new file mode 100644 index 000000000..a9a702b38 --- /dev/null +++ b/packages/effect/src/ai/McpClientError.ts @@ -0,0 +1,329 @@ +/** + * Errors produced by `McpClient` operations and transports. + * + * Every failure is an `McpClientError` whose `reason` is one of the reason + * classes in this module: transport, protocol, HTTP, unsupported feature, + * closed connection, timeout, limit, configuration, validation or + * tool-reported errors. + * + * @stability experimental + * @since 4.0.6 + */ +import * as Predicate from "../Predicate.ts" +import * as Schema from "../Schema.ts" +import * as McpSchema from "./McpSchema.ts" + +const TypeId = "~effect/ai/McpClientError" as const +const ReasonTypeId = "~effect/ai/McpClientError/Reason" as const + +/** + * The connection to the server failed: a process or HTTP request could not be + * started, written or read, or ended before the response. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class TransportError extends Schema.Error("effect/ai/McpClientError/TransportError")({ + _tag: Schema.tag("TransportError"), + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()) +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + return this.description ?? "MCP transport failed" + } +} + +/** + * The server returned a JSON-RPC error, or sent a message that is not valid + * JSON-RPC for the selected protocol. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class ProtocolError extends Schema.Error("effect/ai/McpClientError/ProtocolError")({ + _tag: Schema.tag("ProtocolError"), + code: Schema.optional(Schema.Int), + data: Schema.optional(Schema.Unknown), + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()) +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + const message = this.description ?? "MCP protocol error" + return this.code === undefined ? message : `${message} (code ${this.code})` + } +} + +/** + * The server answered an HTTP request with a non-success status. A JSON-RPC + * error in the response body supplies `code`, `data` and `description`. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class HttpError extends Schema.Error("effect/ai/McpClientError/HttpError")({ + _tag: Schema.tag("HttpError"), + status: Schema.Int, + code: Schema.optional(Schema.Int), + data: Schema.optional(Schema.Unknown), + description: Schema.optional(Schema.String), + wwwAuthenticate: Schema.optional(Schema.String), + retryAfter: Schema.optional(Schema.String) +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + const message = `MCP HTTP status ${this.status}` + return this.description === undefined ? message : `${message}: ${this.description}` + } +} + +/** + * The server or the selected protocol does not support the requested + * operation, or a tool result cannot be converted for a language model. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class UnsupportedError extends Schema.Error("effect/ai/McpClientError/UnsupportedError")({ + _tag: Schema.tag("UnsupportedError"), + description: Schema.String, + result: Schema.optional(McpSchema.CallToolResult) +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + return this.description + } +} + +/** + * The client, its transport or its legacy HTTP session is closed. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class ClosedError extends Schema.Error("effect/ai/McpClientError/ClosedError")({ + _tag: Schema.tag("ClosedError"), + sessionExpired: Schema.optional(Schema.Boolean), + description: Schema.optional(Schema.String) +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + return this.sessionExpired === true ? "MCP session expired" : this.description ?? "MCP connection closed" + } +} + +/** + * An operation did not complete within its deadline. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class TimeoutError extends Schema.Error("effect/ai/McpClientError/TimeoutError")({ + _tag: Schema.tag("TimeoutError"), + operation: Schema.String +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + return `MCP ${this.operation} deadline exceeded` + } +} + +/** + * A message or a discovery scan exceeded a client limit. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class LimitError extends Schema.Error("effect/ai/McpClientError/LimitError")({ + _tag: Schema.tag("LimitError"), + description: Schema.String +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + return this.description + } +} + +/** + * Client or toolkit options are invalid. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class ConfigurationError + extends Schema.Error("effect/ai/McpClientError/ConfigurationError")({ + _tag: Schema.tag("ConfigurationError"), + description: Schema.String, + cause: Schema.optional(Schema.Defect()) + }) +{ + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + return this.description + } +} + +/** + * A request payload, tool argument or result failed schema validation. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class ValidationError extends Schema.Error("effect/ai/McpClientError/ValidationError")({ + _tag: Schema.tag("ValidationError"), + description: Schema.String, + cause: Schema.optional(Schema.Defect()) +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + return this.description + } +} + +/** + * A tool reported an execution error with `isError`. The complete result is + * retained. + * + * @stability experimental + * @category reasons + * @since 4.0.6 + */ +export class ToolError extends Schema.Error("effect/ai/McpClientError/ToolError")({ + _tag: Schema.tag("ToolError"), + result: McpSchema.CallToolResult +}) { + readonly [ReasonTypeId] = ReasonTypeId + + override get message(): string { + const text = this.result.content.find((block) => block.type === "text") + return text === undefined ? "MCP tool reported an error" : `MCP tool reported an error: ${text.text}` + } +} + +/** + * Union of MCP client and transport failure reasons. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export type McpClientErrorReason = + | TransportError + | ProtocolError + | HttpError + | UnsupportedError + | ClosedError + | TimeoutError + | LimitError + | ConfigurationError + | ValidationError + | ToolError + +/** + * Schema for the reasons carried by an `McpClientError`. + * + * @stability experimental + * @category schemas + * @since 4.0.6 + */ +export const McpClientErrorReason: Schema.Union<[ + typeof TransportError, + typeof ProtocolError, + typeof HttpError, + typeof UnsupportedError, + typeof ClosedError, + typeof TimeoutError, + typeof LimitError, + typeof ConfigurationError, + typeof ValidationError, + typeof ToolError +]> = Schema.Union([ + TransportError, + ProtocolError, + HttpError, + UnsupportedError, + ClosedError, + TimeoutError, + LimitError, + ConfigurationError, + ValidationError, + ToolError +]) + +/** + * Error wrapping an MCP client or transport failure reason. + * + * **Details** + * + * Use `Effect.catchReason` or `Effect.catchReasons` to recover from selected + * reasons. The error's `message` and `cause` come from its reason. + * + * **Example** (Recovering from a deadline failure) + * + * ```ts import.meta.vitest + * import { Effect } from "effect" + * import { McpClientError } from "effect/ai" + * + * const operation = Effect.fail(new McpClientError.McpClientError({ + * reason: new McpClientError.TimeoutError({ operation: "tools/call" }) + * })) + * const recovered = operation.pipe( + * Effect.catchReason("McpClientError", "TimeoutError", () => Effect.succeed("deferred")) + * ) + * + * await Effect.runPromise(recovered) // => "deferred" + * ``` + * + * @stability experimental + * @category errors + * @since 4.0.6 + */ +export class McpClientError extends Schema.Error("effect/ai/McpClientError/McpClientError")({ + _tag: Schema.tag("McpClientError"), + reason: McpClientErrorReason +}) { + readonly [TypeId] = TypeId + + override readonly cause = this.reason + + override get message(): string { + return this.reason.message + } +} + +/** + * Returns whether a value is an `McpClientError`. + * + * @stability experimental + * @category guards + * @since 4.0.6 + */ +export const isMcpClientError = (u: unknown): u is McpClientError => Predicate.hasProperty(u, TypeId) + +/** + * Returns whether a value is one of the `McpClientError` reasons. + * + * @stability experimental + * @category guards + * @since 4.0.6 + */ +export const isMcpClientErrorReason = (u: unknown): u is McpClientErrorReason => Predicate.hasProperty(u, ReasonTypeId) diff --git a/repos/effect/packages/effect/src/ai/McpProtocol.ts b/packages/effect/src/ai/McpProtocol.ts similarity index 98% rename from repos/effect/packages/effect/src/ai/McpProtocol.ts rename to packages/effect/src/ai/McpProtocol.ts index e06a892a5..b46b02bac 100644 --- a/repos/effect/packages/effect/src/ai/McpProtocol.ts +++ b/packages/effect/src/ai/McpProtocol.ts @@ -106,9 +106,9 @@ export interface TransportPolicy { */ export interface StatelessRuntimeProfile { readonly protocolVersion: string - readonly clientCapabilities: Schema.JsonObject + readonly clientCapabilities: Readonly> readonly clientInfo?: McpSchema.Implementation | undefined - readonly requestMetadata: Schema.JsonObject + readonly requestMetadata: Readonly> } /** diff --git a/repos/effect/packages/effect/src/ai/McpSchema.ts b/packages/effect/src/ai/McpSchema.ts similarity index 95% rename from repos/effect/packages/effect/src/ai/McpSchema.ts rename to packages/effect/src/ai/McpSchema.ts index 02fa72449..f15d16d1e 100644 --- a/repos/effect/packages/effect/src/ai/McpSchema.ts +++ b/packages/effect/src/ai/McpSchema.ts @@ -94,6 +94,10 @@ export const optional = ( }) ) +// Objects are checked one level deep. Incoming payloads come from JSON.parse and +// outgoing ones are serialized through Schema.toCodecJson. +const UnknownObject = Schema.Record(Schema.String, Schema.Unknown) + // ============================================================================= // Common // ============================================================================= @@ -119,6 +123,109 @@ export const RequestId: Schema.Union<[ */ export type RequestId = typeof RequestId.Type +/** + * A JSON-RPC request. + * + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const JsonRpcRequest = Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: RequestId, + method: Schema.String, + params: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)) +}) + +/** + * A JSON-RPC request. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type JsonRpcRequest = typeof JsonRpcRequest.Type + +/** + * A JSON-RPC notification without a request identifier. + * + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const JsonRpcNotification = Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + method: Schema.String, + params: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)) +}) + +/** + * A JSON-RPC notification without a request identifier. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type JsonRpcNotification = typeof JsonRpcNotification.Type & { readonly id?: never } + +/** + * A JSON-RPC response carrying a result or remote error. + * + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const JsonRpcResponse = Schema.Union([ + Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: RequestId, + result: Schema.Unknown + }), + Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.optional(Schema.Union([RequestId, Schema.Null])), + error: Schema.Struct({ + code: Schema.Int, + message: Schema.String, + data: Schema.optional(Schema.Unknown) + }) + }) +]) + +/** + * A JSON-RPC response carrying exactly one result or remote error. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type JsonRpcResponse = + | (typeof JsonRpcResponse.members[0]["Type"] & { readonly error?: never }) + | (typeof JsonRpcResponse.members[1]["Type"] & { readonly result?: never }) + +/** + * Schema for JSON-RPC messages exchanged by MCP peers. + * + * **Details** + * + * Only the envelope is validated. `params`, `result` and error `data` are left + * for the method's own schema to decode. + * + * @stability unstable + * @category schemas + * @since 4.0.6 + */ +export const JsonRpcMessage = Schema.Union([JsonRpcRequest, JsonRpcNotification, JsonRpcResponse]) + +/** + * JSON-RPC messages exchanged by MCP peers. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse + /** * Schema for MCP progress tokens that associate progress notifications with the * original request. @@ -165,7 +272,7 @@ export class RequestMeta extends Schema.Opaque()(Schema.Struct({ */ progressToken: optional(ProgressToken) }), - [Schema.JsonObject] + [UnknownObject] )) })) {} @@ -186,7 +293,7 @@ export class ResultMeta extends Schema.Opaque()(Schema.Struct({ * This result property is reserved by the protocol to allow clients and * servers to attach additional metadata to their responses. */ - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -206,7 +313,7 @@ export class NotificationMeta extends Schema.Opaque()(Schema.S * This parameter name is reserved by MCP to allow clients and servers to * attach additional metadata to their notifications. */ - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -400,7 +507,7 @@ export class ClientCapabilities extends Schema.Class( * Optional extensions capabilities advertised by the client. * Keys are extension identifiers following / (e.g. "io.modelcontextprotocol/ui"). */ - extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Json)), + extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Unknown)), /** * Present if the client supports listing roots. */ @@ -464,7 +571,7 @@ export class ServerCapabilities extends Schema.Opaque()(Sche * Optional extensions capabilities advertised by the server. * Keys are extension identifiers following / (e.g. "io.modelcontextprotocol/ui"). */ - extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Json)), + extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Unknown)), /** * Present if the server supports sending log messages to the client. */ @@ -623,6 +730,16 @@ export const PARSE_ERROR_CODE = -32700 as const */ export const HEADER_MISMATCH_ERROR_CODE = -32020 as const +/** + * Represents the MCP error code returned when a server does not support the + * protocol version requested by a client. + * + * @stability unstable + * @category constants + * @since 4.0.6 + */ +export const UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022 as const + /** * Represents an MCP/JSON-RPC error for invalid JSON that could not be parsed. * @@ -987,7 +1104,7 @@ export class Resource extends Schema.Class( * This parameter name is reserved by MCP to allow clients and servers to * attach additional metadata to resources. */ - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) }) {} /** @@ -1038,7 +1155,7 @@ export class ResourceTemplate extends Schema.Class( /** * Optional additional metadata for the client. */ - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) }) {} /** @@ -1060,7 +1177,7 @@ export class ResourceContents extends Schema.Opaque()(Schema.S /** * Optional additional metadata for the client. */ - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -1319,7 +1436,7 @@ export class Prompt extends Schema.Class( * Icons that clients can display for this prompt. */ icons: optional(Schema.Array(Icon)), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) }) {} /** @@ -1339,7 +1456,7 @@ export class TextContent extends Schema.Opaque()(Schema.Struct({ * Optional annotations for the client. */ annotations: optional(Annotations), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -1364,7 +1481,7 @@ export class ImageContent extends Schema.Opaque()(Schema.Struct({ * Optional annotations for the client. */ annotations: optional(Annotations), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -1389,7 +1506,7 @@ export class AudioContent extends Schema.Opaque()(Schema.Struct({ * Optional annotations for the client. */ annotations: optional(Annotations), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -1411,7 +1528,7 @@ export class EmbeddedResource extends Schema.Opaque()(Schema.S * Optional annotations for the client. */ annotations: optional(Annotations), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -1629,16 +1746,17 @@ export class ToolAnnotations extends Schema.Opaque()(Schema.Str * * **Details** * - * Property definitions and additional root keywords are constrained to JSON - * values. The open root supports generated keywords such as `$defs`. + * Property schemas may be objects or booleans. The open root supports + * generated keywords such as `$defs`. Nested values are not validated when + * decoding. * * @stability unstable * @category tools * @since 4.0.0 */ -export type ToolJson = Schema.JsonObject & { +export type ToolJson = { readonly [x: string]: unknown } & { readonly type: "object" - readonly properties?: Readonly> | undefined + readonly properties?: Readonly> | undefined readonly required?: ReadonlyArray | undefined } @@ -1652,10 +1770,10 @@ export type ToolJson = Schema.JsonObject & { export const ToolJson: Schema.Codec = Schema.StructWithRest( Schema.Struct({ type: Schema.Literal("object"), - properties: optional(Schema.Record(Schema.String, Schema.JsonObject)), + properties: optional(Schema.Record(Schema.String, Schema.Union([UnknownObject, Schema.Boolean]))), required: optional(Schema.Array(Schema.String)) }), - [Schema.JsonObject] + [UnknownObject] ) /** @@ -1669,7 +1787,7 @@ export const ToolJson: Schema.Codec = Schema.StructWithRest( * @category tools * @since 4.0.0 */ -export type ToolOutputJson = Schema.JsonObject +export type ToolOutputJson = { readonly [x: string]: unknown } /** * Schema for {@link ToolOutputJson}. @@ -1678,7 +1796,7 @@ export type ToolOutputJson = Schema.JsonObject * @category tools * @since 4.0.0 */ -export const ToolOutputJson: Schema.Codec = Schema.JsonObject +export const ToolOutputJson: Schema.Codec = UnknownObject /** * Schema for the definition of a tool the client can call. @@ -1723,7 +1841,7 @@ export class Tool extends Schema.Class( * This parameter name is reserved by MCP to allow clients and servers to * attach additional metadata to resources. */ - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) }) {} /** @@ -1775,7 +1893,7 @@ export class CallToolResult extends Schema.Class("@effect/ai/Mcp /** * An optional JSON value containing the structured result of the tool call. */ - structuredContent: optional(Schema.Json), + structuredContent: optional(Schema.Unknown), /** * Whether the tool call ended in an error. * @@ -1955,7 +2073,7 @@ export class ToolUseContent extends Schema.Class("@effect/ai/Mcp * Arguments supplied to the tool. */ input: Schema.Record(Schema.String, Schema.Unknown), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) }) {} /** @@ -1978,12 +2096,12 @@ export class ToolResultContent extends Schema.Class("@effect/ /** * Optional structured result returned by the tool. */ - structuredContent: optional(Schema.Json), + structuredContent: optional(Schema.Unknown), /** * Whether tool execution ended in an error. */ isError: optional(Schema.Boolean), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) }) {} /** @@ -2020,7 +2138,7 @@ export type SamplingMessageContentBlock = typeof SamplingMessageContentBlock.Typ export class SamplingMessage extends Schema.Opaque()(Schema.Struct({ role: Role, content: Schema.Union([SamplingMessageContentBlock, Schema.Array(SamplingMessageContentBlock)]), - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) })) {} /** @@ -2141,7 +2259,7 @@ export class CreateMessageResult extends Schema.Class( )({ role: Role, content: Schema.Union([SamplingMessageContentBlock, Schema.Array(SamplingMessageContentBlock)]), - _meta: optional(Schema.JsonObject), + _meta: optional(UnknownObject), /** * The name of the model that generated the message. */ @@ -2360,7 +2478,7 @@ export class Root extends Schema.Class( /** * Optional additional metadata associated with the root. */ - _meta: optional(Schema.JsonObject) + _meta: optional(UnknownObject) }) {} /** @@ -2799,9 +2917,9 @@ export type ElicitResult = typeof ElicitResult.Type * @since 4.0.0 */ export type McpInputRequest = - | { readonly method: "roots/list"; readonly params?: Schema.JsonObject | undefined } - | { readonly method: "sampling/createMessage"; readonly params: Schema.JsonObject } - | { readonly method: "elicitation/create"; readonly params: Schema.JsonObject } + | { readonly method: "roots/list"; readonly params?: Readonly> | undefined } + | { readonly method: "sampling/createMessage"; readonly params: Readonly> } + | { readonly method: "elicitation/create"; readonly params: Readonly> } /** * Version-neutral response supplied by an MCP client for a prior input request. @@ -2810,7 +2928,7 @@ export type McpInputRequest = * @category models * @since 4.0.0 */ -export type McpInputResponse = Schema.JsonObject +export type McpInputResponse = Readonly> /** * Indicates that an MCP operation requires additional keyed input from the client. @@ -2970,7 +3088,7 @@ export class McpRequestContext extends Context.Service - | Schema.JsonObject + | Readonly> | undefined readonly inputResponses?: Readonly> | undefined readonly requestState?: string | undefined @@ -2994,7 +3112,7 @@ export class McpServerClient extends Context.Service> | undefined readonly getClient: Effect.Effect< McpReverseClient, never, diff --git a/repos/effect/packages/effect/src/ai/McpServer.ts b/packages/effect/src/ai/McpServer.ts similarity index 91% rename from repos/effect/packages/effect/src/ai/McpServer.ts rename to packages/effect/src/ai/McpServer.ts index a44d32e6f..7cf92985a 100644 --- a/repos/effect/packages/effect/src/ai/McpServer.ts +++ b/packages/effect/src/ai/McpServer.ts @@ -424,214 +424,210 @@ export class McpServer extends Context.Service - Effect.gen(function*() { - const existingIndex = tools.findIndex(({ tool }) => tool.name === options.tool.name) - if (existingIndex === -1) { - tools.push(options) - } else { - tools[existingIndex] = options - } - const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen) - yield* internalCore.tools.register({ - descriptor: new McpTool({ - ...options.tool, - title: options.tool.title ?? options.tool.annotations?.title - }), - isVisible: (profile) => - enabledWhen === undefined || enabledWhen( - { - protocolVersion: profile.protocolVersion, - capabilities: profile.clientCapabilities, - clientInfo: profile.clientInfo - } - ), - handle: (call, invocation) => - provideInvocationContext(options.handle(call.arguments), invocation).pipe( - Effect.catchTags({ - InternalError: (error) => - Effect.fail( - new McpCore.ToolExecutionError({ - name: options.tool.name, - message: error.message - }) - ), - InvalidParams: (error) => - Effect.fail( - new (invocation.requestContext.requestState === undefined && - invocation.requestContext.inputResponses === undefined - ? McpCore.InvalidToolInput - : McpCore.InvalidToolContinuation)({ - name: options.tool.name, - message: error.message - }) - ) - }), - Effect.map((result) => - Predicate.isTagged(result, "InputRequired") - ? McpCore.OperationOutcome.InputRequired(result) - : McpCore.OperationOutcome.Complete(result) - ) + addTool: Effect.fnUntraced(function*(options) { + const existingIndex = tools.findIndex(({ tool }) => tool.name === options.tool.name) + if (existingIndex === -1) { + tools.push(options) + } else { + tools[existingIndex] = options + } + const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen) + yield* internalCore.tools.register({ + descriptor: new McpTool({ + ...options.tool, + title: options.tool.title ?? options.tool.annotations?.title + }), + isVisible: (profile) => + enabledWhen === undefined || enabledWhen( + { + protocolVersion: profile.protocolVersion, + capabilities: profile.clientCapabilities, + clientInfo: profile.clientInfo + } + ), + handle: (call, invocation) => + provideInvocationContext(options.handle(call.arguments), invocation).pipe( + Effect.catchTags({ + InternalError: (error) => + Effect.fail( + new McpCore.ToolExecutionError({ + name: options.tool.name, + message: error.message + }) + ), + InvalidParams: (error) => + Effect.fail( + new (invocation.requestContext.requestState === undefined && + invocation.requestContext.inputResponses === undefined + ? McpCore.InvalidToolInput + : McpCore.InvalidToolContinuation)({ + name: options.tool.name, + message: error.message + }) + ) + }), + Effect.map((result) => + Predicate.isTagged(result, "InputRequired") + ? McpCore.OperationOutcome.InputRequired(result) + : McpCore.OperationOutcome.Complete(result) ) - }) - yield* notifications.client["notifications/tools/list_changed"]({}) - }), - callTool: (request) => - Effect.gen(function*() { - const client = yield* McpServerClient - const result = yield* internalCore.tools.call( - request, - McpProtocolInternal.invocationFromClient(client) - ).pipe( - Effect.mapError((error) => - new InvalidParams({ - message: error._tag === "ToolNotFound" - ? `Tool '${error.name}' not found` - : error.message - }) ) + }) + yield* notifications.client["notifications/tools/list_changed"]({}) + }), + callTool: Effect.fnUntraced(function*(request) { + const client = yield* McpServerClient + const result = yield* internalCore.tools.call( + request, + McpProtocolInternal.invocationFromClient(client) + ).pipe( + Effect.mapError((error) => + new InvalidParams({ + message: error._tag === "ToolNotFound" + ? `Tool '${error.name}' not found` + : error.message + }) ) - if (result._tag === "InputRequired") { - return yield* new InvalidParams({ message: "Client input is not supported by this MCP protocol" }) - } - return result.value - }), + ) + if (result._tag === "InputRequired") { + return yield* new InvalidParams({ message: "Client input is not supported by this MCP protocol" }) + } + return result.value + }), get resources() { return resources }, get resourceTemplates() { return resourceTemplates }, - addResource: (options) => - Effect.gen(function*() { - const existingIndex = resources.findIndex(({ resource }) => resource.uri === options.resource.uri) - if (existingIndex === -1) { - resources.push(options) - } else { - resources[existingIndex] = options - } - yield* internalCore.resources.register({ - descriptor: options.resource, - isVisible: (profile) => { - const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen) - return enabledWhen === undefined || enabledWhen({ - protocolVersion: profile.protocolVersion, - capabilities: profile.clientCapabilities, - clientInfo: profile.clientInfo - }) - }, - read: (invocation) => provideInvocationContext(options.handle, invocation) - }) - yield* notifications.client["notifications/resources/list_changed"]({}) - }), - addResourceTemplate: ({ annotations, completions, handle, routerPath, template }) => - Effect.gen(function*() { - const existingIndex = resourceTemplates.findIndex(({ template: current }) => - current.uriTemplate === template.uriTemplate + addResource: Effect.fnUntraced(function*(options) { + const existingIndex = resources.findIndex(({ resource }) => resource.uri === options.resource.uri) + if (existingIndex === -1) { + resources.push(options) + } else { + resources[existingIndex] = options + } + yield* internalCore.resources.register({ + descriptor: options.resource, + isVisible: (profile) => { + const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen) + return enabledWhen === undefined || enabledWhen({ + protocolVersion: profile.protocolVersion, + capabilities: profile.clientCapabilities, + clientInfo: profile.clientInfo + }) + }, + read: (invocation) => provideInvocationContext(options.handle, invocation) + }) + yield* notifications.client["notifications/resources/list_changed"]({}) + }), + addResourceTemplate: Effect.fnUntraced(function*({ annotations, completions, handle, routerPath, template }) { + const existingIndex = resourceTemplates.findIndex(({ template: current }) => + current.uriTemplate === template.uriTemplate + ) + if (existingIndex === -1) { + resourceTemplates.push({ template, annotations }) + } else { + resourceTemplates[existingIndex] = { template, annotations } + } + const templateMatcher = makeUriMatcher() + templateMatcher.add(routerPath, true) + yield* internalCore.resources.registerTemplate({ + descriptor: template, + isVisible: (profile) => { + const enabledWhen = Context.getOrUndefined(annotations, EnabledWhen) + return enabledWhen === undefined || enabledWhen({ + protocolVersion: profile.protocolVersion, + capabilities: profile.clientCapabilities, + clientInfo: profile.clientInfo + }) + }, + match: (uri) => { + const match = templateMatcher.find(uri) + if (match === undefined) { + return undefined + } + const params: Array = [] + for (const key of Object.keys(match.params)) { + params[Number(key)] = match.params[key]! + } + return params + }, + read: (uri, params, invocation) => provideInvocationContext(handle(uri, Array.from(params)), invocation) + }) + for (const [param, handle] of Object.entries(completions)) { + yield* internalCore.completions.register( + `resource/${template.uriTemplate}/${param}`, + (request, invocation) => + provideInvocationContext(handle(request.argument.value, request.context), invocation).pipe( + Effect.map((result) => ({ + values: result.completion.values, + total: result.completion.total, + hasMore: result.completion.hasMore, + metadata: result._meta + })) + ) ) - if (existingIndex === -1) { - resourceTemplates.push({ template, annotations }) - } else { - resourceTemplates[existingIndex] = { template, annotations } - } - const templateMatcher = makeUriMatcher() - templateMatcher.add(routerPath, true) - yield* internalCore.resources.registerTemplate({ - descriptor: template, - isVisible: (profile) => { - const enabledWhen = Context.getOrUndefined(annotations, EnabledWhen) - return enabledWhen === undefined || enabledWhen({ - protocolVersion: profile.protocolVersion, - capabilities: profile.clientCapabilities, - clientInfo: profile.clientInfo - }) - }, - match: (uri) => { - const match = templateMatcher.find(uri) - if (match === undefined) { - return undefined - } - const params: Array = [] - for (const key of Object.keys(match.params)) { - params[Number(key)] = match.params[key]! - } - return params - }, - read: (uri, params, invocation) => provideInvocationContext(handle(uri, Array.from(params)), invocation) - }) - for (const [param, handle] of Object.entries(completions)) { - yield* internalCore.completions.register( - `resource/${template.uriTemplate}/${param}`, - (request, invocation) => - provideInvocationContext(handle(request.argument.value, request.context), invocation).pipe( - Effect.map((result) => ({ - values: result.completion.values, - total: result.completion.total, - hasMore: result.completion.hasMore, - metadata: result._meta - })) - ) - ) - } - yield* notifications.client["notifications/resources/list_changed"]({}) - }), - findResource: (uri) => - Effect.gen(function*() { - const client = yield* McpServerClient - return yield* internalCore.resources.read( - uri, - McpProtocolInternal.invocationFromClient(client) - ).pipe( - Effect.catchTag("ResourceNotFound", (error) => - Effect.fail(new InvalidParams({ message: `Resource '${error.uri}' not found` }))) + } + yield* notifications.client["notifications/resources/list_changed"]({}) + }), + findResource: Effect.fnUntraced(function*(uri) { + const client = yield* McpServerClient + return yield* internalCore.resources.read( + uri, + McpProtocolInternal.invocationFromClient(client) + ).pipe( + Effect.catchTag( + "ResourceNotFound", + (error) => Effect.fail(new InvalidParams({ message: `Resource '${error.uri}' not found` })) ) - }), + ) + }), get prompts() { return prompts }, - addPrompt: (options) => - Effect.gen(function*() { - const existingIndex = prompts.findIndex(({ prompt }) => prompt.name === options.prompt.name) - if (existingIndex === -1) { - prompts.push(options) - } else { - prompts[existingIndex] = options - } - yield* internalCore.prompts.register({ - descriptor: options.prompt, - isVisible: (profile) => { - const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen) - return enabledWhen === undefined || enabledWhen({ - protocolVersion: profile.protocolVersion, - capabilities: profile.clientCapabilities, - clientInfo: profile.clientInfo - }) - }, - get: (params, invocation) => - provideInvocationContext(options.handle(params), invocation).pipe( - Effect.map((result) => - Predicate.isTagged(result, "InputRequired") - ? McpCore.OperationOutcome.InputRequired(result) - : McpCore.OperationOutcome.Complete(result) - ) + addPrompt: Effect.fnUntraced(function*(options) { + const existingIndex = prompts.findIndex(({ prompt }) => prompt.name === options.prompt.name) + if (existingIndex === -1) { + prompts.push(options) + } else { + prompts[existingIndex] = options + } + yield* internalCore.prompts.register({ + descriptor: options.prompt, + isVisible: (profile) => { + const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen) + return enabledWhen === undefined || enabledWhen({ + protocolVersion: profile.protocolVersion, + capabilities: profile.clientCapabilities, + clientInfo: profile.clientInfo + }) + }, + get: (params, invocation) => + provideInvocationContext(options.handle(params), invocation).pipe( + Effect.map((result) => + Predicate.isTagged(result, "InputRequired") + ? McpCore.OperationOutcome.InputRequired(result) + : McpCore.OperationOutcome.Complete(result) ) - }) - for (const [param, handle] of Object.entries(options.completions)) { - yield* internalCore.completions.register( - `prompt/${options.prompt.name}/${param}`, - (request, invocation) => - provideInvocationContext(handle(request.argument.value, request.context), invocation).pipe( - Effect.map((result) => ({ - values: result.completion.values, - total: result.completion.total, - hasMore: result.completion.hasMore, - metadata: result._meta - })) - ) ) - } - yield* notifications.client["notifications/prompts/list_changed"]({}) - }), + }) + for (const [param, handle] of Object.entries(options.completions)) { + yield* internalCore.completions.register( + `prompt/${options.prompt.name}/${param}`, + (request, invocation) => + provideInvocationContext(handle(request.argument.value, request.context), invocation).pipe( + Effect.map((result) => ({ + values: result.completion.values, + total: result.completion.total, + hasMore: result.completion.hasMore, + metadata: result._meta + })) + ) + ) + } + yield* notifications.client["notifications/prompts/list_changed"]({}) + }), getPromptResult: Effect.fnUntraced(function*({ arguments: params, name }) { const client = yield* McpServerClient const outcome = yield* internalCore.prompts.get( @@ -916,18 +912,15 @@ const runWithRuntime = Effect.fnUntraced(function*( const isInitialize = rpc._tag.endsWith("/initialize") if (!isInitialize && !session) { const fiber = Fiber.getCurrent()! - const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest) - if (httpRequest) { - appendPreResponseHandlerUnsafe( - httpRequest, - () => - Effect.succeed( - HttpServerResponse.empty({ - status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404 - }) - ) - ) - } + appendPreResponseHandlerUnsafe( + fiber.context, + () => + Effect.succeed( + HttpServerResponse.empty({ + status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404 + }) + ) + ) return Effect.die(new Error(`Mcp-Session-Id does not exist`)) } // RPC middleware erases the correlation between the initialize tag @@ -966,7 +959,7 @@ const runWithRuntime = Effect.fnUntraced(function*( clientInfo: profile.clientInfo, initializePayload, requestMetadata: Predicate.isReadonlyObject(payload) && Predicate.isReadonlyObject(payload._meta) - ? payload._meta as Schema.JsonObject + ? payload._meta as Readonly> : undefined, getClient: RcMap.get( clients, @@ -1033,7 +1026,7 @@ const runWithRuntime = Effect.fnUntraced(function*( ? Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest) : undefined if (httpRequest !== undefined && request._tag !== "Eof") { - appendPreResponseHandlerUnsafe(httpRequest, (_, response) => + appendPreResponseHandlerUnsafe(fiber.context, (_, response) => Effect.succeed( response.status === 200 && response.body._tag === "Uint8Array" && @@ -1095,7 +1088,7 @@ const runWithRuntime = Effect.fnUntraced(function*( ) } if (httpRequest !== undefined && session !== undefined) { - appendPreResponseHandlerUnsafe(httpRequest, (_, res) => + appendPreResponseHandlerUnsafe(fiber.context, (_, res) => Effect.succeed( runtime.resolveRequest(clientId, headers) === undefined ? HttpServerResponse.empty({ status: 404 }) @@ -1111,17 +1104,15 @@ const runWithRuntime = Effect.fnUntraced(function*( const isClientNotification = selectedProtocol.clientNotificationRpcs.requests.has(request.tag) if (rpc && isClientNotification && request.isNotification) { if (!session && selectedProtocol.runtime._tag === "Stateful") { - if (httpRequest) { - appendPreResponseHandlerUnsafe( - httpRequest, - () => - Effect.succeed( - HttpServerResponse.empty({ - status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404 - }) - ) - ) - } + appendPreResponseHandlerUnsafe( + fiber.context, + () => + Effect.succeed( + HttpServerResponse.empty({ + status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404 + }) + ) + ) return } const decode = selectedProtocol.payloadCodecs(rpc).decode(request.payload) @@ -2192,10 +2183,16 @@ const isParameterValidationError = ( ): error is AiError.AiError & { readonly reason: AiError.ToolParameterValidationError } => AiError.isAiError(error) && error.reason._tag === "ToolParameterValidationError" -// MCP requires an object root, so a top-level `$ref` is inlined. +// MCP requires an object root. A non-recursive root is compiled inline rather +// than as a definition; a recursive root keeps its definition and the top-level +// `$ref` is inlined. const toolJsonSchema = (schema: Schema.Constraint, strict: boolean): JsonSchema.JsonSchema => { + const root = SchemaAST.getContextOwner(SchemaAST.getLastEncoding(Schema.toCodecJson(schema).ast)) const document = InternalStructuredOutput.resolveTopLevelReference( - Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" }) + Schema.toJsonSchemaDocument(schema, { + onExcessProperty: strict ? "error" : "ignore", + referencePolicy: ({ ast, identifier, occurrences }) => ast === root && occurrences === 1 ? undefined : identifier + }) ) return Object.keys(document.definitions).length === 0 ? document.schema diff --git a/repos/effect/packages/effect/src/ai/Model.ts b/packages/effect/src/ai/Model.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/Model.ts rename to packages/effect/src/ai/Model.ts diff --git a/repos/effect/packages/effect/src/ai/OpenAiStructuredOutput.ts b/packages/effect/src/ai/OpenAiStructuredOutput.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/OpenAiStructuredOutput.ts rename to packages/effect/src/ai/OpenAiStructuredOutput.ts diff --git a/repos/effect/packages/effect/src/ai/Prompt.ts b/packages/effect/src/ai/Prompt.ts similarity index 97% rename from repos/effect/packages/effect/src/ai/Prompt.ts rename to packages/effect/src/ai/Prompt.ts index 45f30c9f9..150075810 100644 --- a/repos/effect/packages/effect/src/ai/Prompt.ts +++ b/packages/effect/src/ai/Prompt.ts @@ -465,9 +465,15 @@ export interface FilePartEncoded extends BasePartEncoded<"file", FilePartOptions */ readonly fileName?: string | undefined /** - * File data as base64 string of data, a byte array, or a URL. + * File data as a base64 string, a byte array, a URL, or the tagged JSON + * form that encoding produces for byte arrays and URLs. */ - readonly data: string | Uint8Array | URL + readonly data: + | string + | Uint8Array + | URL + | { readonly _tag: "Uint8Array"; readonly base64: string } + | { readonly _tag: "URL"; readonly href: string } } /** @@ -491,7 +497,18 @@ export const FilePart: Schema.Struct<{ readonly type: Schema.Literal<"file"> readonly mediaType: Schema.String readonly fileName: Schema.optional - readonly data: Schema.Union + readonly data: Schema.Union< + readonly [ + Schema.String, + Schema.decodeTo< + Schema.Uint8Array, + Schema.TaggedStruct<"Uint8Array", { readonly base64: Schema.Uint8ArrayFromBase64 }> + >, + Schema.decodeTo>, + Schema.Uint8Array, + Schema.URL + ] + > readonly "~effect/ai/Prompt/Part": Schema.withDecodingDefaultKey> readonly options: Schema.withDecodingDefault< Schema.$Record< @@ -504,7 +521,27 @@ export const FilePart: Schema.Struct<{ type: Schema.Literal("file"), mediaType: Schema.String, fileName: Schema.optional(Schema.String), - data: Schema.Union([Schema.String, Schema.Uint8Array, Schema.URL]) + // The tagged members come first so that encoding produces JSON-safe data, + // while the raw members keep decoding byte arrays and URLs directly + data: Schema.Union([ + Schema.String, + Schema.TaggedStruct("Uint8Array", { base64: Schema.Uint8ArrayFromBase64 }).pipe(Schema.decodeTo( + Schema.Uint8Array, + SchemaTransformation.transform({ + decode: ({ base64 }) => base64, + encode: (base64) => ({ _tag: "Uint8Array", base64 }) as const + }) + )), + Schema.TaggedStruct("URL", { href: Schema.URLFromString }).pipe(Schema.decodeTo( + Schema.URL, + SchemaTransformation.transform({ + decode: ({ href }) => href, + encode: (href) => ({ _tag: "URL", href }) as const + }) + )), + Schema.Uint8Array, + Schema.URL + ]) }).annotate({ identifier: "FilePart" }) /** @@ -1407,36 +1444,7 @@ export const UserMessage: Schema.Struct<{ never, never >, - Schema.$Array< - Schema.Union< - readonly [ - Schema.Struct<{ - readonly type: Schema.Literal<"text"> - readonly text: Schema.String - readonly "~effect/ai/Prompt/Part": Schema.withDecodingDefaultKey> - readonly options: Schema.withDecodingDefault< - Schema.$Record< - Schema.String, - Schema.NullOr> - > - > - }>, - Schema.Struct<{ - readonly type: Schema.Literal<"file"> - readonly mediaType: Schema.String - readonly fileName: Schema.optional - readonly data: Schema.Union - readonly "~effect/ai/Prompt/Part": Schema.withDecodingDefaultKey> - readonly options: Schema.withDecodingDefault< - Schema.$Record< - Schema.String, - Schema.NullOr> - > - > - }> - ] - > - > + Schema.$Array> ] > readonly "~effect/ai/Prompt/Message": Schema.withDecodingDefaultKey> diff --git a/repos/effect/packages/effect/src/ai/Response.ts b/packages/effect/src/ai/Response.ts similarity index 95% rename from repos/effect/packages/effect/src/ai/Response.ts rename to packages/effect/src/ai/Response.ts index ed3e15077..deb29613b 100644 --- a/repos/effect/packages/effect/src/ai/Response.ts +++ b/packages/effect/src/ai/Response.ts @@ -170,6 +170,21 @@ const toolPartSchemas = (tool: Tool.Any) => { return parts } +type AnyToolParts = ToolCallPart | ToolResultPart + +const toolPartSchemasFor = ( + toolkit: Toolkit.Any | Toolkit.WithHandler | undefined +): ReadonlyArray => { + if (toolkit === undefined) { + return [ + makeToolCallPart(Schema.String, Schema.Unknown), + makeToolResultPart(Schema.String, Schema.Unknown, Schema.Unknown) + ] + } + const parts = Object.values(toolkit.tools as Record).map(toolPartSchemas) + return [...parts.map((part) => part.toolCall), ...parts.map((part) => part.toolResult)] +} + /** * Creates a Schema for all response parts based on a toolkit. * @@ -178,6 +193,9 @@ const toolPartSchemas = (tool: Tool.Any) => { * Generates a schema that includes all possible response parts, with tool call * and tool result parts dynamically created based on the provided toolkit. * + * When no toolkit is provided, tool call and tool result parts accept any tool + * name, with parameters and results decoded as `unknown`. + * * **Example** (Building a response parts schema) * * ```ts import.meta.vitest @@ -193,28 +211,25 @@ const toolPartSchemas = (tool: Tool.Any) => { * * const allPartsSchema = Response.AllParts(myToolkit) * Schema.isSchema(allPartsSchema) // => true + * + * const anyToolPartsSchema = Response.AllParts() + * Schema.isSchema(anyToolPartsSchema) // => true * ``` * * @stability unstable * @category schemas * @since 4.0.0 */ -export const AllParts = >( - toolkit: T -): Schema.Codec< - AllParts : Toolkit.WithHandlerTools>, - AllPartsEncoded, - Tool.ResultDecodingServices[keyof Toolkit.Tools]>, - Tool.ResultEncodingServices[keyof Toolkit.Tools]> -> => { - const toolCalls: Array = [] - const toolResults: Array = [] - for (const tool of Object.values(toolkit.tools as Record)) { - const parts = toolPartSchemas(tool) - toolCalls.push(parts.toolCall) - toolResults.push(parts.toolResult) - } - return Schema.Union([ +export const AllParts: { + >(toolkit: T): Schema.Codec< + AllParts : Toolkit.WithHandlerTools>, + AllPartsEncoded, + Tool.ResultDecodingServices[keyof Toolkit.Tools]>, + Tool.ResultEncodingServices[keyof Toolkit.Tools]> + > + (): Schema.Codec | AnyToolParts, AllPartsEncoded> +} = (toolkit?: Toolkit.Any | Toolkit.WithHandler) => + Schema.Union([ TextPart, TextStartPart, TextDeltaPart, @@ -233,10 +248,8 @@ export const AllParts = >( ResponseMetadataPart, FinishPart, ErrorPart, - ...toolCalls, - ...toolResults + ...toolPartSchemasFor(toolkit) ]) as any -} // ============================================================================= // Parts @@ -289,26 +302,25 @@ export type PartEncoded = /** * Creates a Schema for non-streaming response parts based on a toolkit. * + * **Details** + * + * When no toolkit is provided, tool call and tool result parts accept any tool + * name, with parameters and results decoded as `unknown`. + * * @stability unstable * @category schemas * @since 4.0.0 */ -export const Part = >( - toolkit: T -): Schema.Codec< - Part : Toolkit.WithHandlerTools>, - PartEncoded, - Tool.ResultDecodingServices[keyof Toolkit.Tools]>, - Tool.ResultEncodingServices[keyof Toolkit.Tools]> -> => { - const toolCalls: Array = [] - const toolResults: Array = [] - for (const tool of Object.values(toolkit.tools as Record)) { - const parts = toolPartSchemas(tool) - toolCalls.push(parts.toolCall) - toolResults.push(parts.toolResult) - } - return Schema.Union([ +export const Part: { + >(toolkit: T): Schema.Codec< + Part : Toolkit.WithHandlerTools>, + PartEncoded, + Tool.ResultDecodingServices[keyof Toolkit.Tools]>, + Tool.ResultEncodingServices[keyof Toolkit.Tools]> + > + (): Schema.Codec | AnyToolParts, PartEncoded> +} = (toolkit?: Toolkit.Any | Toolkit.WithHandler) => + Schema.Union([ TextPart, ReasoningPart, ToolApprovalRequestPart, @@ -317,10 +329,8 @@ export const Part = >( UrlSourcePart, ResponseMetadataPart, FinishPart, - ...toolCalls, - ...toolResults + ...toolPartSchemasFor(toolkit) ]) as any -} // ============================================================================= // Stream Parts @@ -386,26 +396,25 @@ export type StreamPartEncoded = /** * Creates a Schema for streaming response parts based on a toolkit. * + * **Details** + * + * When no toolkit is provided, tool call and tool result parts accept any tool + * name, with parameters and results decoded as `unknown`. + * * @stability unstable * @category schemas * @since 4.0.0 */ -export const StreamPart = >( - toolkit: T -): Schema.Codec< - StreamPart : Toolkit.WithHandlerTools>, - StreamPartEncoded, - Tool.ResultDecodingServices[keyof Toolkit.Tools]>, - Tool.ResultEncodingServices[keyof Toolkit.Tools]> -> => { - const toolCalls: Array = [] - const toolResults: Array = [] - for (const tool of Object.values(toolkit.tools as Record)) { - const parts = toolPartSchemas(tool) - toolCalls.push(parts.toolCall) - toolResults.push(parts.toolResult) - } - return Schema.Union([ +export const StreamPart: { + >(toolkit: T): Schema.Codec< + StreamPart : Toolkit.WithHandlerTools>, + StreamPartEncoded, + Tool.ResultDecodingServices[keyof Toolkit.Tools]>, + Tool.ResultEncodingServices[keyof Toolkit.Tools]> + > + (): Schema.Codec | AnyToolParts, StreamPartEncoded> +} = (toolkit?: Toolkit.Any | Toolkit.WithHandler) => + Schema.Union([ TextStartPart, TextDeltaPart, TextEndPart, @@ -422,10 +431,8 @@ export const StreamPart = >( ResponseMetadataPart, FinishPart, ErrorPart, - ...toolCalls, - ...toolResults + ...toolPartSchemasFor(toolkit) ]) as any -} // ============================================================================= // utility types @@ -1522,15 +1529,17 @@ export const ToolCallPart: = ( name: Name, params: Params -) => +) => makeToolCallPart(Schema.Literal(name), params) as any + +const makeToolCallPart = (name: Schema.Top, params: Schema.Constraint) => Schema.Struct({ ...BasePart.fields, type: Schema.Literal("tool-call"), id: Schema.String, - name: Schema.Literal(name), + name, params, providerExecuted: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))) - }).annotate({ identifier: "ToolCallPart" }) as any + }).annotate({ identifier: "ToolCallPart" }) /** * Constructs a new tool call part. @@ -1739,14 +1748,19 @@ export const ToolResultPart = < ToolResultPartEncoded, Success["DecodingServices"] | Failure["DecodingServices"], Success["EncodingServices"] | Failure["EncodingServices"] -> => { +> => + makeToolResultPart(Schema.Literal(name), success, failure).annotate({ + identifier: `ToolResultPart(${name})` + }) as any + +const makeToolResultPart = (name: Schema.Top, success: Schema.Constraint, failure: Schema.Constraint) => { const makeSchema = (result: Schema.Constraint, isFailure: boolean) => { const encodedResult = Schema.toEncoded(result) const Common = { id: Schema.String, type: Schema.Literal("tool-result"), isFailure: Schema.Literal(isFailure), - name: Schema.Literal(name) + name } const Decoded = Schema.Struct({ ...Common, @@ -1779,9 +1793,7 @@ export const ToolResultPart = < }) )) } - return Schema.Union([makeSchema(success, false), makeSchema(failure, true)]).annotate({ - identifier: `ToolResultPart(${name})` - }) + return Schema.Union([makeSchema(success, false), makeSchema(failure, true)]) } /** diff --git a/repos/effect/packages/effect/src/ai/ResponseIdTracker.ts b/packages/effect/src/ai/ResponseIdTracker.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/ResponseIdTracker.ts rename to packages/effect/src/ai/ResponseIdTracker.ts diff --git a/repos/effect/packages/effect/src/ai/Telemetry.ts b/packages/effect/src/ai/Telemetry.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/Telemetry.ts rename to packages/effect/src/ai/Telemetry.ts diff --git a/repos/effect/packages/effect/src/ai/Tokenizer.ts b/packages/effect/src/ai/Tokenizer.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/Tokenizer.ts rename to packages/effect/src/ai/Tokenizer.ts diff --git a/repos/effect/packages/effect/src/ai/Tool.ts b/packages/effect/src/ai/Tool.ts similarity index 99% rename from repos/effect/packages/effect/src/ai/Tool.ts rename to packages/effect/src/ai/Tool.ts index b19465535..b1abfea67 100644 --- a/repos/effect/packages/effect/src/ai/Tool.ts +++ b/packages/effect/src/ai/Tool.ts @@ -115,7 +115,9 @@ export type DynamicTypeId = "~effect/ai/Tool/Dynamic" * execution will be returned in the error channel of the calling effect. * * If set to `"return"`, errors that occur during tool call handler execution - * will be captured and returned as part of the tool call result. + * will be captured and returned as part of the tool call result. Like + * `Effect.catch`, only the first typed failure is returned, and a cause + * containing a defect is raised as a defect. * * @stability unstable * @category models @@ -246,7 +248,9 @@ export interface Tool< * effect. * * If set to `"return"`, errors that occur during tool call handler execution - * will be captured and returned as part of the tool call result. + * will be captured and returned as part of the tool call result. Like + * `Effect.catch`, only the first typed failure is returned, and a cause + * containing a defect is raised as a defect. */ readonly failureMode: FailureMode @@ -1295,7 +1299,9 @@ export const make = < * execution will be returned in the error channel of the calling effect. * * If set to `"return"`, errors that occur during tool call handler execution - * will be captured and returned as part of the tool call result. + * will be captured and returned as part of the tool call result. Like + * `Effect.catch`, only the first typed failure is returned, and a cause + * containing a defect is raised as a defect. */ readonly failureMode?: Mode /** @@ -1534,7 +1540,9 @@ export const providerDefined = < * execution will be returned in the error channel of the calling effect. * * If set to `"return"`, errors that occur during tool call handler execution - * will be captured and returned as part of the tool call result. + * will be captured and returned as part of the tool call result. Like + * `Effect.catch`, only the first typed failure is returned, and a cause + * containing a defect is raised as a defect. */ readonly failureMode?: Mode | undefined } diff --git a/repos/effect/packages/effect/src/ai/Toolkit.ts b/packages/effect/src/ai/Toolkit.ts similarity index 87% rename from repos/effect/packages/effect/src/ai/Toolkit.ts rename to packages/effect/src/ai/Toolkit.ts index c017c35ad..5872416af 100644 --- a/repos/effect/packages/effect/src/ai/Toolkit.ts +++ b/packages/effect/src/ai/Toolkit.ts @@ -125,7 +125,8 @@ export interface HandlerContext { * **Details** * * Preliminary results are streamed to the caller before the handler completes, - * enabling real-time progress updates for lengthy operations. + * enabling real-time progress updates for lengthy operations. Results are + * buffered without limit unless the caller provides `PreliminaryBuffer`. */ readonly preliminary: (result: Tool.Success) => Effect.Effect } @@ -296,9 +297,60 @@ export const FailureOrigin = Context.Reference("effect/ai/To defaultValue: () => "result" }) +/** + * Limits how many preliminary results `Toolkit.handle` buffers per tool call. + * + * **Details** + * + * Defaults to `"unbounded"`. Provide it as a positive integer around `handle`, + * or around a program that calls it, and `HandlerContext.preliminary` suspends + * once that many results are unconsumed. Any other value is a defect. + * + * The bound only applies to the stream returned by `handle`. Consume that + * stream while the handler runs; waiting for the handler before reading it + * deadlocks once the buffer fills. `LanguageModel` moves results into its own + * buffers, so the bound does not limit memory end to end there. + * + * **Example** (Bounding preliminary results) + * + * ```ts import.meta.vitest + * import { Effect, Schema, Stream } from "effect" + * import { Tool, Toolkit } from "effect/ai" + * + * const toolkit = Toolkit.make(Tool.make("Progress", { success: Schema.Number })) + * + * const program = Effect.gen(function*() { + * const handlers = yield* toolkit + * const stream = yield* handlers.handle("Progress", {}).pipe( + * Effect.provideService(Toolkit.PreliminaryBuffer, 16) + * ) + * const results = yield* Stream.runCollect(stream) + * return results.length + * }).pipe( + * Effect.provide(toolkit.toLayer({ + * Progress: Effect.fnUntraced(function*(_, ctx) { + * for (let i = 0; i < 64; i++) yield* ctx.preliminary(i) + * return 64 + * }) + * })) + * ) + * + * await Effect.runPromise(program) // => 65 + * ``` + * + * @stability unstable + * @category services + * @since 4.1.0 + */ +export const PreliminaryBuffer = Context.Reference("effect/ai/Toolkit/PreliminaryBuffer", { + defaultValue: () => "unbounded" +}) + const failureCause = (error: E, origin: Tool.FailureOrigin): Cause.Cause => Cause.annotate(Cause.fail(error), Context.make(FailureOrigin, origin)) +const failureOrigin = (error: unknown): Tool.FailureOrigin => Schema.isSchemaError(error) ? "result" : "handler" + const Proto = { ...Effectable.Prototype({ label: "Toolkit", @@ -402,11 +454,15 @@ const Proto = { } const decodedParams = decodedParamsResult.success + const capacity = yield* PreliminaryBuffer + if (capacity !== "unbounded" && !(Number.isInteger(capacity) && capacity > 0)) { + return yield* Effect.die(`Tool "${name}": PreliminaryBuffer must be a positive integer, got ${capacity}`) + } const queue = yield* Queue.make<{ readonly result: any readonly isFailure: boolean readonly preliminary: boolean - }, Cause.Done>() + }, Cause.Done>(capacity === "unbounded" ? undefined : { capacity }) const context: HandlerContext = { toolCallId, preliminary: (result) => @@ -448,18 +504,30 @@ const Proto = { } return Stream.fromQueue(queue).pipe( - Stream.catch((error) => { - const normalizedError = normalizeError(error) - const failureOrigin: Tool.FailureOrigin = Schema.isSchemaError(error) ? "result" : "handler" - return tool.failureMode === "error" - ? Stream.failCause(failureCause(normalizedError, failureOrigin)) - : Stream.succeed({ result: normalizedError, isFailure: true, failureOrigin, preliminary: false }) - }), + tool.failureMode === "error" + ? Stream.catchCause((cause) => + Stream.failCause(Cause.map( + Cause.fromReasons(cause.reasons.map((reason) => + Cause.isFailReason(reason) + ? reason.annotate(Context.make(FailureOrigin, failureOrigin(reason.error)), { overwrite: true }) + : reason + )), + normalizeError + )) + ) + : Stream.catch((error) => + Stream.succeed({ + result: normalizeError(error), + isFailure: true, + failureOrigin: failureOrigin(error), + preliminary: false + }) + ), Stream.mapEffect(Effect.fnUntraced(function*(output) { const encodedResult = yield* encodeResult(output.result, output.isFailure) return { ...output, encodedResult } })), - Stream.onEnd(Fiber.interrupt(fiber)) + Stream.ensuring(Fiber.interrupt(fiber)) ) satisfies Stream.Stream, any> }) diff --git a/repos/effect/packages/effect/src/ai/index.ts b/packages/effect/src/ai/index.ts similarity index 90% rename from repos/effect/packages/effect/src/ai/index.ts rename to packages/effect/src/ai/index.ts index 30524cc1d..a85512133 100644 --- a/repos/effect/packages/effect/src/ai/index.ts +++ b/packages/effect/src/ai/index.ts @@ -53,6 +53,18 @@ export * as IdGenerator from "./IdGenerator.ts" */ export * as LanguageModel from "./LanguageModel.ts" +/** + * @stability experimental + * @since 4.0.6 + */ +export * as McpClient from "./McpClient.ts" + +/** + * @stability experimental + * @since 4.0.6 + */ +export * as McpClientError from "./McpClientError.ts" + /** * @stability unstable * @since 4.0.0 diff --git a/repos/effect/packages/effect/src/ai/internal/codec-transformer.ts b/packages/effect/src/ai/internal/codec-transformer.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/internal/codec-transformer.ts rename to packages/effect/src/ai/internal/codec-transformer.ts diff --git a/repos/effect/packages/effect/src/ai/internal/mcpCore.ts b/packages/effect/src/ai/internal/mcpCore.ts similarity index 93% rename from repos/effect/packages/effect/src/ai/internal/mcpCore.ts rename to packages/effect/src/ai/internal/mcpCore.ts index a7d200d33..b384c43c0 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpCore.ts +++ b/packages/effect/src/ai/internal/mcpCore.ts @@ -7,7 +7,6 @@ import * as Arr from "../../Array.ts" import * as Data from "../../Data.ts" import * as Effect from "../../Effect.ts" -import type * as Schema from "../../Schema.ts" import type * as McpProtocol from "../McpProtocol.ts" import * as McpSchema from "../McpSchema.ts" @@ -56,7 +55,7 @@ export interface NegotiatedProtocolProfile>) | undefined } @@ -66,7 +65,7 @@ export interface NegotiatedProtocolProfile> | undefined + readonly experimental?: Readonly>>> | undefined readonly logging?: boolean | undefined readonly completions?: boolean | undefined readonly prompts?: Readonly<{ readonly listChanged?: boolean | undefined }> | undefined @@ -77,7 +76,7 @@ export interface CanonicalServerCapabilities { }> | undefined readonly tools?: Readonly<{ readonly listChanged?: boolean | undefined }> | undefined - readonly extensions?: Schema.JsonObject | undefined + readonly extensions?: Readonly> | undefined } /** @@ -303,7 +302,7 @@ export interface CompletionResult { readonly values: ReadonlyArray readonly total?: number | undefined readonly hasMore?: boolean | undefined - readonly metadata?: Schema.JsonObject | undefined + readonly metadata?: Readonly> | undefined } /** @@ -333,7 +332,7 @@ export type ClientNotification = Data.TaggedEnum<{ readonly progress: number readonly total?: number | undefined readonly message?: string | undefined - readonly metadata?: Schema.JsonObject | undefined + readonly metadata?: Readonly> | undefined } RootsChanged: {} }> @@ -349,7 +348,7 @@ export const ClientNotification = Data.taggedEnum() export interface Cancellation { readonly requestId: string | number readonly reason?: string | undefined - readonly metadata?: Schema.JsonObject | undefined + readonly metadata?: Readonly> | undefined } /** @@ -359,25 +358,25 @@ export type ServerNotification = Data.TaggedEnum<{ Cancelled: { readonly requestId: string | number readonly reason?: string | undefined - readonly metadata?: Schema.JsonObject | undefined + readonly metadata?: Readonly> | undefined } Progress: { readonly progressToken: string | number readonly progress: number readonly total?: number | undefined readonly message?: string | undefined - readonly metadata?: Schema.JsonObject | undefined + readonly metadata?: Readonly> | undefined } LoggingMessage: { readonly level: McpSchema.LoggingLevel readonly logger?: string | undefined readonly data: unknown - readonly metadata?: Schema.JsonObject | undefined + readonly metadata?: Readonly> | undefined } - ResourceUpdated: { readonly uri: string; readonly metadata?: Schema.JsonObject | undefined } - ResourcesChanged: { readonly metadata?: Schema.JsonObject | undefined } - ToolsChanged: { readonly metadata?: Schema.JsonObject | undefined } - PromptsChanged: { readonly metadata?: Schema.JsonObject | undefined } + ResourceUpdated: { readonly uri: string; readonly metadata?: Readonly> | undefined } + ResourcesChanged: { readonly metadata?: Readonly> | undefined } + ToolsChanged: { readonly metadata?: Readonly> | undefined } + PromptsChanged: { readonly metadata?: Readonly> | undefined } ElicitationComplete: { readonly elicitationId: string } }> diff --git a/repos/effect/packages/effect/src/ai/internal/mcpProtocol.ts b/packages/effect/src/ai/internal/mcpProtocol.ts similarity index 96% rename from repos/effect/packages/effect/src/ai/internal/mcpProtocol.ts rename to packages/effect/src/ai/internal/mcpProtocol.ts index 13072ba62..dccd8b047 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpProtocol.ts +++ b/packages/effect/src/ai/internal/mcpProtocol.ts @@ -19,8 +19,10 @@ import * as PublicMcpSchema from "../McpSchema.ts" import * as McpCore from "./mcpCore.ts" const LEGACY_RESOURCE_NOT_FOUND_ERROR_CODE = -32002 -const BASE64_SENTINEL_PREFIX = "=?base64?" -const BASE64_SENTINEL_SUFFIX = "?=" +/** @internal */ +export const BASE64_SENTINEL_PREFIX = "=?base64?" +/** @internal */ +export const BASE64_SENTINEL_SUFFIX = "?=" // A leading U+FEFF belongs to the header value and must survive comparison with the JSON body. const routingHeaderDecoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }) @@ -120,6 +122,24 @@ export const requireCompleteOperation = ( ? Effect.succeed(outcome.value) : Effect.fail(new McpCore.UnsupportedByProtocol({ protocolVersion, feature: "Client input" })) +/** + * Projects boolean property schemas to equivalent objects for pre-June adapters. + * Their dated tool descriptors require object-valued direct property schemas. + * + * @internal + */ +export const projectLegacyToolInputSchema = (inputSchema: PublicMcpSchema.ToolJson) => ({ + ...inputSchema, + properties: inputSchema.properties === undefined + ? undefined + : Object.fromEntries>>( + Object.entries(inputSchema.properties).map(([name, schema]) => [ + name, + typeof schema === "boolean" ? schema ? {} : { not: {} } : schema + ]) + ) +}) + /** * Unquotes an exact text mirror when the protocol drops string `structuredContent`. * diff --git a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2024_11_05.ts b/packages/effect/src/ai/internal/mcpProtocol/v2024_11_05.ts similarity index 99% rename from repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2024_11_05.ts rename to packages/effect/src/ai/internal/mcpProtocol/v2024_11_05.ts index 6204a3396..123486db3 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2024_11_05.ts +++ b/packages/effect/src/ai/internal/mcpProtocol/v2024_11_05.ts @@ -257,7 +257,7 @@ export const protocol = McpProtocol.make({ McpSchema.Tool.make({ name: tool.name, description: tool.description, - inputSchema: tool.inputSchema + inputSchema: McpProtocol.projectLegacyToolInputSchema(tool.inputSchema) }) ) }) diff --git a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_03_26.ts b/packages/effect/src/ai/internal/mcpProtocol/v2025_03_26.ts similarity index 99% rename from repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_03_26.ts rename to packages/effect/src/ai/internal/mcpProtocol/v2025_03_26.ts index 156c817a4..66558306f 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_03_26.ts +++ b/packages/effect/src/ai/internal/mcpProtocol/v2025_03_26.ts @@ -279,7 +279,7 @@ export const protocol = McpProtocol.make({ McpSchema.Tool.make({ name: tool.name, description: tool.description, - inputSchema: tool.inputSchema, + inputSchema: McpProtocol.projectLegacyToolInputSchema(tool.inputSchema), annotations: tool.title === undefined && tool.annotations === undefined ? undefined : McpSchema.ToolAnnotations.make({ diff --git a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_06_18.ts b/packages/effect/src/ai/internal/mcpProtocol/v2025_06_18.ts similarity index 98% rename from repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_06_18.ts rename to packages/effect/src/ai/internal/mcpProtocol/v2025_06_18.ts index 2318e22de..1ab071906 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_06_18.ts +++ b/packages/effect/src/ai/internal/mcpProtocol/v2025_06_18.ts @@ -122,10 +122,10 @@ const projectResourceContents = ( } const projectStructuredContent = ( - content: Schema.Json | undefined -): Schema.JsonObject | undefined => content === undefined || isJsonObject(content) ? content : undefined + content: unknown +): Readonly> | undefined => content === undefined || isObject(content) ? content : undefined -const isJsonObject = (value: Schema.Json): value is Schema.JsonObject => Predicate.isReadonlyObject(value) +const isObject = (value: unknown): value is Readonly> => Predicate.isReadonlyObject(value) /** * @internal diff --git a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_11_25.ts b/packages/effect/src/ai/internal/mcpProtocol/v2025_11_25.ts similarity index 98% rename from repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_11_25.ts rename to packages/effect/src/ai/internal/mcpProtocol/v2025_11_25.ts index e6e984675..487acb57e 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2025_11_25.ts +++ b/packages/effect/src/ai/internal/mcpProtocol/v2025_11_25.ts @@ -112,10 +112,10 @@ const projectContent = Effect.fnUntraced(function*(content: PublicMcpSchema.Cont }) const projectStructuredContent = ( - content: Schema.Json | undefined -): Schema.JsonObject | undefined => content === undefined || isJsonObject(content) ? content : undefined + content: unknown +): Readonly> | undefined => content === undefined || isObject(content) ? content : undefined -const isJsonObject = (value: Schema.Json): value is Schema.JsonObject => Predicate.isReadonlyObject(value) +const isObject = (value: unknown): value is Readonly> => Predicate.isReadonlyObject(value) /** * @internal diff --git a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts b/packages/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts similarity index 97% rename from repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts rename to packages/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts index 604c35688..11fe397ca 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts +++ b/packages/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts @@ -26,7 +26,7 @@ import * as McpCore from "../mcpCore.ts" import * as McpProtocol from "../mcpProtocol.ts" import * as McpSchema from "../mcpSchema/v2026_07_28.ts" -const InputResponses = Schema.Record(Schema.String, Schema.JsonObject) +const InputResponses = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)) const decodeRequestMetadata = Schema.decodeUnknownEffect(McpSchema.RequestMetaObject) const decodeCancellation = Schema.decodeUnknownEffect(McpSchema.CancelledNotification.payloadSchema) const isJson = Schema.is(Schema.Json) @@ -92,8 +92,8 @@ export const profileFromRequestMetadata = Effect.fnUntraced(function*(metadata: const resultMetadata = ( value: ObjectWithUndefined, - serverInfo: Schema.JsonObject -): Schema.JsonObject => { + serverInfo: Readonly> +): Readonly> => { const metadata: unknown = value._meta return { ...(Predicate.isReadonlyObject(metadata) ? metadata : {}), @@ -103,7 +103,7 @@ const resultMetadata = ( const projectInputRequired = ( outcome: McpCore.InputRequiredFields, - serverInfo: Schema.JsonObject + serverInfo: Readonly> ) => omitUndefined({ _meta: { "io.modelcontextprotocol/serverInfo": serverInfo }, @@ -138,8 +138,8 @@ export const projectCallToolOutcome = Effect.fnUntraced(function*( const requiredCapabilitiesForInputRequests = ( inputRequests: McpCore.InputRequiredFields["inputRequests"], capabilities: McpCore.NegotiatedProtocolProfile["clientCapabilities"] -): Record => { - const required: Record> = {} +): Record>> => { + const required: Record> = {} for (const request of Object.values(inputRequests ?? {})) { Match.value(request).pipe( Match.when({ method: "roots/list" }, () => { @@ -264,16 +264,14 @@ const projectError = (error: ProtocolError): McpSchema.McpError => { }) } -const failWithHttpStatus = Effect.fnUntraced(function*(status: number, error: McpProtocol.ProtocolError) { - const request = yield* Effect.serviceOption(HttpServerRequest.HttpServerRequest) - if (Option.isSome(request)) { +const failWithHttpStatus = (status: number, error: McpProtocol.ProtocolError) => + Effect.withFiber((fiber) => { appendPreResponseHandlerUnsafe( - request.value, + fiber.context, (_request, response) => Effect.succeed(HttpServerResponse.setStatus(response, status)) ) - } - return yield* error -}) + return Effect.fail(error) + }) /** * @internal @@ -327,7 +325,7 @@ export const makeHandlers = ( // Older adapters accept arbitrary JSON; this binding requires extension settings objects. const extensions = context.serverInfo.extensions === undefined ? undefined : Record.filter( context.serverInfo.extensions, - (value): value is Schema.JsonObject => Predicate.isReadonlyObject(value) + (value): value is Readonly> => Predicate.isReadonlyObject(value) ) const getDiscovery = Effect.map(context.registrationPresence, (presence) => { const capabilities: Types.Mutable = { completions: {}, logging: {} } diff --git a/repos/effect/packages/effect/src/ai/internal/mcpProtocolRegistry.ts b/packages/effect/src/ai/internal/mcpProtocolRegistry.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/internal/mcpProtocolRegistry.ts rename to packages/effect/src/ai/internal/mcpProtocolRegistry.ts diff --git a/repos/effect/packages/effect/src/ai/internal/mcpRuntime.ts b/packages/effect/src/ai/internal/mcpRuntime.ts similarity index 99% rename from repos/effect/packages/effect/src/ai/internal/mcpRuntime.ts rename to packages/effect/src/ai/internal/mcpRuntime.ts index 095b74153..de8066655 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpRuntime.ts +++ b/packages/effect/src/ai/internal/mcpRuntime.ts @@ -31,7 +31,6 @@ const MCP_METHOD_HEADER = "mcp-method" const MCP_NAME_HEADER = "mcp-name" const PROTOCOL_VERSION_METADATA_KEY = "io.modelcontextprotocol/protocolVersion" const CLIENT_CAPABILITIES_METADATA_KEY = "io.modelcontextprotocol/clientCapabilities" -const UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022 const asRecord = (input: unknown): Record | undefined => typeof input === "object" && input !== null ? input as Record : undefined @@ -236,7 +235,7 @@ export const make = Effect.fnUntraced(function*( const registry = yield* McpProtocolRegistry.make(protocols) const sessionTerminationListeners = new Set<(binding: RequestBinding) => Effect.Effect>() const unsupportedProtocolVersion = (requested: string) => ({ - code: UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE, + code: PublicMcpSchema.UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE, message: `Unsupported protocol version '${requested}'`, data: { supported: protocolVersions, requested } }) @@ -345,7 +344,7 @@ export const make = Effect.fnUntraced(function*( const selected = selectStatefulProtocol(registry.protocols, offeredVersion) if (selected === undefined) { return yield* new McpProtocol.ProtocolError({ - code: UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE, + code: PublicMcpSchema.UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE, message: `initialize is not supported by the configured MCP protocols (requested '${offeredVersion}')` }) } @@ -568,7 +567,7 @@ export const make = Effect.fnUntraced(function*( const sessionId = crypto.randomUUID() stateful.registerHttp(sessionId, registration) appendPreResponseHandlerUnsafe( - httpRequest, + fiber.context, (_request, response) => Effect.succeed(HttpServerResponse.setHeaders(response, { [MCP_SESSION_ID_HEADER]: sessionId, diff --git a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2024_11_05.ts b/packages/effect/src/ai/internal/mcpSchema/v2024_11_05.ts similarity index 94% rename from repos/effect/packages/effect/src/ai/internal/mcpSchema/v2024_11_05.ts rename to packages/effect/src/ai/internal/mcpSchema/v2024_11_05.ts index ed117834f..a1b5645d2 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2024_11_05.ts +++ b/packages/effect/src/ai/internal/mcpSchema/v2024_11_05.ts @@ -29,7 +29,14 @@ export const optional = ( encode: SchemaGetter.transformOptional(Option.flatMap(Option.fromUndefinedOr)) }) ) -const JsonObject = Schema.JsonObject + +/** + * Object checked one level deep. Payloads come from `JSON.parse` or are serialized + * through `Schema.toCodecJson`, so deep JSON validation would be redundant. + * + * @internal + */ +export const UnknownObject = Schema.Record(Schema.String, Schema.Unknown) /** * @internal @@ -65,7 +72,7 @@ export const RequestMeta = Schema.Struct({ Schema.Struct({ progressToken: optional(ProgressToken) }), - [Schema.JsonObject] + [UnknownObject] )) }) @@ -73,14 +80,14 @@ export const RequestMeta = Schema.Struct({ * @internal */ export const NotificationMeta = Schema.Struct({ - _meta: optional(JsonObject) + _meta: optional(UnknownObject) }) /** * @internal */ export const ResultMeta = Schema.Struct({ - _meta: optional(JsonObject) + _meta: optional(UnknownObject) }) /** @@ -111,21 +118,21 @@ export const Implementation = Schema.Struct({ * @internal */ export const ClientCapabilities = Schema.Struct({ - extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Json)), - experimental: optional(Schema.Record(Schema.String, JsonObject)), + extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Unknown)), + experimental: optional(Schema.Record(Schema.String, UnknownObject)), roots: optional(Schema.Struct({ listChanged: optional(Schema.Boolean) })), - sampling: optional(JsonObject) + sampling: optional(UnknownObject) }) /** * @internal */ export const ServerCapabilities = Schema.Struct({ - extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Json)), - experimental: optional(Schema.Record(Schema.String, JsonObject)), - logging: optional(JsonObject), + extensions: optional(Schema.Record(Schema.TemplateLiteral([Schema.String, "/", Schema.String]), Schema.Unknown)), + experimental: optional(Schema.Record(Schema.String, UnknownObject)), + logging: optional(UnknownObject), prompts: optional(Schema.Struct({ listChanged: optional(Schema.Boolean) })), @@ -284,7 +291,7 @@ export const PromptMessage = Schema.Struct({ */ export const ToolInputSchema = Schema.Struct({ type: Schema.Literal("object"), - properties: optional(Schema.Record(Schema.String, JsonObject)), + properties: optional(Schema.Record(Schema.String, UnknownObject)), required: optional(Schema.Array(Schema.String)) }) @@ -582,7 +589,7 @@ export class CallTool extends Rpc.make("tools/call", { payload: { ...RequestMeta.fields, name: Schema.String, - arguments: optional(JsonObject) + arguments: optional(UnknownObject) } }) {} @@ -610,7 +617,7 @@ export class CreateMessage extends Rpc.make("sampling/createMessage", { temperature: optional(Schema.Finite), maxTokens: Schema.Finite, stopSequences: optional(Schema.Array(Schema.String)), - metadata: optional(JsonObject) + metadata: optional(UnknownObject) } }) {} diff --git a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_03_26.ts b/packages/effect/src/ai/internal/mcpSchema/v2025_03_26.ts similarity index 97% rename from repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_03_26.ts rename to packages/effect/src/ai/internal/mcpSchema/v2025_03_26.ts index a8b158748..285b54757 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_03_26.ts +++ b/packages/effect/src/ai/internal/mcpSchema/v2025_03_26.ts @@ -21,6 +21,8 @@ export const protocolVersion = "2025-03-26" const optional = Previous.optional +const UnknownObject = Previous.UnknownObject + /** * @internal */ @@ -186,7 +188,7 @@ export class CallTool extends Rpc.make("tools/call", { payload: { ...Previous.RequestMeta.fields, name: Schema.String, - arguments: optional(Schema.JsonObject) + arguments: optional(UnknownObject) } }) {} @@ -205,7 +207,7 @@ export class CreateMessage extends Rpc.make("sampling/createMessage", { temperature: optional(Schema.Finite), maxTokens: Schema.Finite, stopSequences: optional(Schema.Array(Schema.String)), - metadata: optional(Schema.JsonObject) + metadata: optional(UnknownObject) } }) {} diff --git a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_06_18.ts b/packages/effect/src/ai/internal/mcpSchema/v2025_06_18.ts similarity index 97% rename from repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_06_18.ts rename to packages/effect/src/ai/internal/mcpSchema/v2025_06_18.ts index 6fa204820..a7b37eaf6 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_06_18.ts +++ b/packages/effect/src/ai/internal/mcpSchema/v2025_06_18.ts @@ -19,8 +19,8 @@ export * from "./v2025_03_26.ts" export const protocolVersion = "2025-06-18" const optional = Previous.optional -const JsonObject = Schema.JsonObject -const Meta = optional(JsonObject) +const UnknownObject = Previous.UnknownObject +const Meta = optional(UnknownObject) /** * @internal @@ -218,10 +218,10 @@ export class Initialize extends Rpc.make("initialize", { const ToolJsonSchema = Schema.StructWithRest( Schema.Struct({ type: Schema.Literal("object"), - properties: optional(Schema.Record(Schema.String, JsonObject)), + properties: optional(Schema.Record(Schema.String, Schema.Union([UnknownObject, Schema.Boolean]))), required: optional(Schema.Array(Schema.String)) }), - [Schema.JsonObject] + [UnknownObject] ) /** @@ -243,7 +243,7 @@ export const Tool = Schema.Struct({ export const CallToolResult = Schema.Struct({ ...Previous.ResultMeta.fields, content: Schema.Array(ContentBlock), - structuredContent: optional(JsonObject), + structuredContent: optional(UnknownObject), isError: optional(Schema.Boolean) }) @@ -273,7 +273,7 @@ export class CreateMessage extends Rpc.make("sampling/createMessage", { temperature: optional(Schema.Finite), maxTokens: Schema.Finite, stopSequences: optional(Schema.Array(Schema.String)), - metadata: optional(JsonObject) + metadata: optional(UnknownObject) } }) {} @@ -433,7 +433,7 @@ export class CallTool extends Rpc.make("tools/call", { payload: { ...Previous.RequestMeta.fields, name: Schema.String, - arguments: optional(JsonObject) + arguments: optional(UnknownObject) } }) {} diff --git a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_11_25.ts b/packages/effect/src/ai/internal/mcpSchema/v2025_11_25.ts similarity index 97% rename from repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_11_25.ts rename to packages/effect/src/ai/internal/mcpSchema/v2025_11_25.ts index dcaf30116..3c2247fc9 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2025_11_25.ts +++ b/packages/effect/src/ai/internal/mcpSchema/v2025_11_25.ts @@ -19,8 +19,8 @@ export * from "./v2025_06_18.ts" export const protocolVersion = "2025-11-25" const optional = Previous.optional -const JsonObject = Schema.JsonObject -const Meta = optional(JsonObject) +const UnknownObject = Previous.UnknownObject +const Meta = optional(UnknownObject) /** * @internal @@ -48,12 +48,12 @@ export const Implementation = Schema.Struct({ export const ClientCapabilities = Schema.Struct({ ...Previous.ClientCapabilities.fields, sampling: optional(Schema.Struct({ - context: optional(JsonObject), - tools: optional(JsonObject) + context: optional(UnknownObject), + tools: optional(UnknownObject) })), elicitation: optional(Schema.Struct({ - form: optional(JsonObject), - url: optional(JsonObject) + form: optional(UnknownObject), + url: optional(UnknownObject) })) }) @@ -182,7 +182,7 @@ export const ToolUseContent = Schema.Struct({ type: Schema.Literal("tool_use"), id: Schema.String, name: Schema.String, - input: JsonObject, + input: UnknownObject, _meta: Meta }) @@ -193,7 +193,7 @@ export const ToolResultContent = Schema.Struct({ type: Schema.Literal("tool_result"), toolUseId: Schema.String, content: Schema.Array(ContentBlock), - structuredContent: optional(JsonObject), + structuredContent: optional(UnknownObject), isError: optional(Schema.Boolean), _meta: Meta }) diff --git a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2026_07_28.ts b/packages/effect/src/ai/internal/mcpSchema/v2026_07_28.ts similarity index 92% rename from repos/effect/packages/effect/src/ai/internal/mcpSchema/v2026_07_28.ts rename to packages/effect/src/ai/internal/mcpSchema/v2026_07_28.ts index b38c929e3..da6419729 100644 --- a/repos/effect/packages/effect/src/ai/internal/mcpSchema/v2026_07_28.ts +++ b/packages/effect/src/ai/internal/mcpSchema/v2026_07_28.ts @@ -16,7 +16,9 @@ export * from "./v2025_11_25.ts" export const protocolVersion = "2026-07-28" const optional = Previous.optional -const Meta = optional(Schema.JsonObject) + +const UnknownObject = Previous.UnknownObject +const Meta = optional(UnknownObject) /** * @internal @@ -44,19 +46,19 @@ export const Implementation = Previous.Implementation */ export const ClientCapabilities = Schema.StructWithRest( Schema.Struct({ - experimental: optional(Schema.Record(Schema.String, Schema.JsonObject)), + experimental: optional(Schema.Record(Schema.String, UnknownObject)), roots: optional(Schema.Struct({})), sampling: optional(Schema.Struct({ - context: optional(Schema.JsonObject), - tools: optional(Schema.JsonObject) + context: optional(UnknownObject), + tools: optional(UnknownObject) })), elicitation: optional(Schema.Struct({ - form: optional(Schema.JsonObject), - url: optional(Schema.JsonObject) + form: optional(UnknownObject), + url: optional(UnknownObject) })), - extensions: optional(Schema.Record(Schema.String, Schema.JsonObject)) + extensions: optional(Schema.Record(Schema.String, UnknownObject)) }), - [Schema.JsonObject] + [UnknownObject] ) /** @@ -64,18 +66,18 @@ export const ClientCapabilities = Schema.StructWithRest( */ export const ServerCapabilities = Schema.StructWithRest( Schema.Struct({ - experimental: optional(Schema.Record(Schema.String, Schema.JsonObject)), - logging: optional(Schema.JsonObject), - completions: optional(Schema.JsonObject), + experimental: optional(Schema.Record(Schema.String, UnknownObject)), + logging: optional(UnknownObject), + completions: optional(UnknownObject), prompts: optional(Schema.Struct({ listChanged: optional(Schema.Boolean) })), resources: optional(Schema.Struct({ subscribe: optional(Schema.Boolean), listChanged: optional(Schema.Boolean) })), tools: optional(Schema.Struct({ listChanged: optional(Schema.Boolean) })), - extensions: optional(Schema.Record(Schema.String, Schema.JsonObject)) + extensions: optional(Schema.Record(Schema.String, UnknownObject)) }), - [Schema.JsonObject] + [UnknownObject] ) /** @@ -89,7 +91,7 @@ export const RequestMetaObject = Schema.StructWithRest( "io.modelcontextprotocol/clientCapabilities": ClientCapabilities, "io.modelcontextprotocol/logLevel": optional(LoggingLevel) }), - [Schema.JsonObject] + [UnknownObject] ) /** @@ -104,7 +106,7 @@ export const NotificationMetaObject = Schema.StructWithRest( Schema.Struct({ "io.modelcontextprotocol/subscriptionId": optional(RequestId) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -116,15 +118,15 @@ export const NotificationParams = Schema.Struct({ _meta: optional(NotificationMe */ export const ResultMetaObject = Schema.StructWithRest( Schema.Struct({ - "io.modelcontextprotocol/serverInfo": Implementation + "io.modelcontextprotocol/serverInfo": optional(Implementation) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal */ export const ResultMeta = { - _meta: ResultMetaObject, + _meta: optional(ResultMetaObject), resultType: Schema.Literal("complete") } /** @@ -133,7 +135,7 @@ export const ResultMeta = { export const McpError = Schema.Struct({ code: Schema.Int, message: Schema.String, - data: optional(Schema.Json) + data: optional(Schema.Unknown) }) /** * @internal @@ -220,11 +222,11 @@ const ToolInputSchema = Schema.StructWithRest( $schema: optional(Schema.String), type: Schema.Literal("object") }), - [Schema.JsonObject] + [UnknownObject] ) const ToolOutputSchema = Schema.StructWithRest( Schema.Struct({ $schema: optional(Schema.String) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -251,7 +253,7 @@ export const ToolResultContent = Schema.Struct({ type: Schema.Literal("tool_result"), toolUseId: Schema.String, content: Schema.Array(ContentBlock), - structuredContent: optional(Schema.Json), + structuredContent: optional(Schema.Unknown), isError: optional(Schema.Boolean), _meta: Meta }) @@ -278,7 +280,7 @@ export const SamplingMessage = Schema.Struct({ */ export const ModelHint = Schema.StructWithRest( Schema.Struct({ name: optional(Schema.String) }), - [Schema.JsonObject] + [UnknownObject] ) const ModelPriority = Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 })) /** @@ -309,7 +311,7 @@ export const CreateMessageRequest = Schema.Struct({ temperature: optional(Schema.Finite), maxTokens: Schema.Int, stopSequences: optional(Schema.Array(Schema.String)), - metadata: optional(Schema.JsonObject), + metadata: optional(UnknownObject), tools: optional(Schema.Array(Tool)), toolChoice: optional(ToolChoice) }) @@ -444,7 +446,7 @@ export const InputRequests = Schema.Record(Schema.String, InputRequest) */ export const InputResponses = Schema.Record(Schema.String, InputResponse) const InputRequiredResultMeta = { - _meta: ResultMetaObject, + _meta: optional(ResultMetaObject), resultType: Schema.Literal("input_required") } /** @@ -457,7 +459,7 @@ export const InputRequiredResult = Schema.Union([ inputRequests: InputRequests, requestState: optional(Schema.String) }), - [Schema.JsonObject] + [UnknownObject] ), Schema.StructWithRest( Schema.Struct({ @@ -465,7 +467,7 @@ export const InputRequiredResult = Schema.Union([ inputRequests: optional(InputRequests), requestState: Schema.String }), - [Schema.JsonObject] + [UnknownObject] ) ]) /** @@ -510,7 +512,7 @@ export const DiscoverResult = Schema.StructWithRest( capabilities: ServerCapabilities, instructions: optional(Schema.String) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -530,7 +532,7 @@ export const ListResourcesResult = Schema.StructWithRest( ...CacheableResult, resources: Schema.Array(Resource) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -550,7 +552,7 @@ export const ListResourceTemplatesResult = Schema.StructWithRest( ...CacheableResult, resourceTemplates: Schema.Array(ResourceTemplate) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -569,7 +571,7 @@ export const ReadResourceResult = Schema.StructWithRest( ...CacheableResult, contents: Schema.Array(ResourceContents) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -589,7 +591,7 @@ export const ListPromptsResult = Schema.StructWithRest( ...CacheableResult, prompts: Schema.Array(Prompt) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -609,7 +611,7 @@ export const GetPromptResult = Schema.StructWithRest( description: optional(Schema.String), messages: Schema.Array(PromptMessage) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -633,7 +635,7 @@ export const ListToolsResult = Schema.StructWithRest( ...CacheableResult, tools: Schema.Array(Tool) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -651,10 +653,10 @@ export const CallToolResult = Schema.StructWithRest( Schema.Struct({ ...ResultMeta, content: Schema.Array(ContentBlock), - structuredContent: optional(Schema.Json), + structuredContent: optional(Schema.Unknown), isError: optional(Schema.Boolean) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -665,7 +667,7 @@ export class CallTool extends Rpc.make("tools/call", { payload: { ...InputResponseRequestParams, name: Schema.String, - arguments: optional(Schema.JsonObject) + arguments: optional(UnknownObject) } }) {} @@ -689,7 +691,7 @@ export const CompleteResult = Schema.StructWithRest( hasMore: optional(Schema.Boolean) }) }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -721,10 +723,10 @@ export const SubscriptionFilter = Schema.Struct({ */ export const SubscriptionsListenResultMetaObject = Schema.StructWithRest( Schema.Struct({ - "io.modelcontextprotocol/serverInfo": Implementation, + "io.modelcontextprotocol/serverInfo": optional(Implementation), "io.modelcontextprotocol/subscriptionId": RequestId }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -734,7 +736,7 @@ export const SubscriptionsListenResult = Schema.StructWithRest( _meta: SubscriptionsListenResultMetaObject, resultType: Schema.Literal("complete") }), - [Schema.JsonObject] + [UnknownObject] ) /** * @internal @@ -775,7 +777,7 @@ export class LoggingMessageNotification extends Rpc.make("notifications/message" ...NotificationParams.fields, level: LoggingLevel, logger: optional(Schema.String), - data: Schema.Json + data: Schema.Unknown } }) {} /** diff --git a/repos/effect/packages/effect/src/ai/internal/mcpStatefulRuntime.ts b/packages/effect/src/ai/internal/mcpStatefulRuntime.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/internal/mcpStatefulRuntime.ts rename to packages/effect/src/ai/internal/mcpStatefulRuntime.ts diff --git a/repos/effect/packages/effect/src/ai/internal/structured-output.ts b/packages/effect/src/ai/internal/structured-output.ts similarity index 100% rename from repos/effect/packages/effect/src/ai/internal/structured-output.ts rename to packages/effect/src/ai/internal/structured-output.ts diff --git a/repos/effect/packages/effect/src/cli/Argument.ts b/packages/effect/src/cli/Argument.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/Argument.ts rename to packages/effect/src/cli/Argument.ts diff --git a/repos/effect/packages/effect/src/cli/CliConfig.ts b/packages/effect/src/cli/CliConfig.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/CliConfig.ts rename to packages/effect/src/cli/CliConfig.ts diff --git a/repos/effect/packages/effect/src/cli/CliError.ts b/packages/effect/src/cli/CliError.ts similarity index 92% rename from repos/effect/packages/effect/src/cli/CliError.ts rename to packages/effect/src/cli/CliError.ts index 99afc264f..eb6051dde 100644 --- a/repos/effect/packages/effect/src/cli/CliError.ts +++ b/packages/effect/src/cli/CliError.ts @@ -3,10 +3,11 @@ * * CLI errors describe problems such as unknown or duplicate flags, missing * flags or arguments, unexpected positional arguments, invalid values, unknown - * subcommands, user handler failures, and requests to show command help. This - * module includes the `CliError` union, the `isCliError` guard, schema-backed - * error classes with display messages, and the `NonShowHelpErrors` union used - * when parse or validation errors should be shown with help output. + * subcommands, rejected input combinations, user handler failures, and + * requests to show command help. This module includes the `CliError` union, + * the `isCliError` guard, schema-backed error classes with display messages, + * and the `NonShowHelpErrors` union used when parse or validation errors + * should be shown with help output. * * @stability unstable * @since 4.0.0 @@ -83,6 +84,7 @@ export type CliError = | UnexpectedArgument | InvalidValue | UnknownSubcommand + | ValidationError | ShowHelp | UserError @@ -500,6 +502,47 @@ export class UnknownSubcommand extends Schema.TaggedError( } } +/** + * Error for parsed input that fails a rule of your own, such as two flags + * that cannot be used together. + * + * **Details** + * + * Fail with it from `Command.validate`, `Flag.mapEffect` or + * `Argument.mapEffect`. The runner shows it with the command help, like any + * other parse error. Handlers report their own failures with `UserError`. + * + * **Example** (Rejecting a flag combination) + * + * ```ts import.meta.vitest + * import { CliError } from "effect/cli" + * + * const error = new CliError.ValidationError({ + * message: "--use-api and --use-docker are mutually exclusive" + * }) + * + * error._tag // => "ValidationError" + * error.message // => "--use-api and --use-docker are mutually exclusive" + * ``` + * + * @stability unstable + * @category errors + * @since 4.1.0 + */ +export class ValidationError extends Schema.TaggedError( + `${TypeId}/ValidationError` +)("ValidationError", { + message: Schema.String +}) { + /** + * Marks this value as a CLI validation error for runtime guards. + * + * @stability unstable + * @since 4.1.0 + */ + readonly [TypeId] = TypeId +} + /** * Error wrapper for user handler failures in the CLI error channel. * @@ -604,6 +647,7 @@ export const NonShowHelpErrors: Schema.Union< typeof UnexpectedArgument, typeof InvalidValue, typeof UnknownSubcommand, + typeof ValidationError, typeof UserError ] > = Schema.Union([ @@ -614,6 +658,7 @@ export const NonShowHelpErrors: Schema.Union< UnexpectedArgument, InvalidValue, UnknownSubcommand, + ValidationError, UserError ]) diff --git a/repos/effect/packages/effect/src/cli/CliOutput.ts b/packages/effect/src/cli/CliOutput.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/CliOutput.ts rename to packages/effect/src/cli/CliOutput.ts diff --git a/repos/effect/packages/effect/src/cli/Command.ts b/packages/effect/src/cli/Command.ts similarity index 92% rename from repos/effect/packages/effect/src/cli/Command.ts rename to packages/effect/src/cli/Command.ts index 2a5c9c01b..63ad39cae 100644 --- a/repos/effect/packages/effect/src/cli/Command.ts +++ b/packages/effect/src/cli/Command.ts @@ -30,10 +30,10 @@ import * as Terminal from "../Terminal.ts" import type { Contravariant, Covariant, NoInfer, Simplify } from "../Types.ts" import * as CliConfig from "./CliConfig.ts" import * as CliError from "./CliError.ts" -import * as CliOutput from "./CliOutput.ts" import * as GlobalFlag from "./GlobalFlag.ts" import { checkForDuplicateFlags, makeCommand, makeParser, toImpl, TypeId } from "./internal/command.ts" import { mergeConfig, parseConfig } from "./internal/config.ts" +import { resolveFormatter } from "./internal/formatter.ts" import { getGlobalFlagsForCommandPath, getGlobalFlagsForCommandTree, getHelpForCommandPath } from "./internal/help.ts" import * as Lexer from "./internal/lexer.ts" import * as Parser from "./internal/parser.ts" @@ -735,6 +735,101 @@ export const withHandler: { ): Command> => makeCommand({ ...toImpl(self), handle: handler } as any)) +/** + * Validates the parsed input of a command before the handler runs. + * + * **Details** + * + * Use this for rules that involve several flags or arguments, such as + * mutually exclusive flags. The validation receives the same input as the + * handler, with defaults, prompts and the wizard already applied. A failure + * is shown with the command help, like a parse error, and the handler does + * not run. Fail with `CliError.ValidationError` to supply the message, or + * with any other `CliError`. Validations run in the order they were added and + * stop at the first failure. Adding a handler later with `withHandler` keeps + * them. + * + * A validation sees only the input that exists where it is applied. Add it + * after `withSharedFlags` to see the shared flags. Added after + * `withSubcommands`, it also runs when a subcommand is selected, with the + * parent's shared flags as input. + * + * **Example** (Rejecting mutually exclusive flags) + * + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" + * import { CliError, Command, Flag } from "effect/cli" + * import { ChildProcessSpawner } from "effect/process" + * import { TestConsole } from "effect/testing" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * TestConsole.layer, + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) + * + * const download = Command.make("download", { + * useApi: Flag.Boolean("use-api").pipe(Flag.withDefault(false)), + * useDocker: Flag.Boolean("use-docker").pipe(Flag.withDefault(false)) + * }, () => Effect.void).pipe( + * Command.validate(({ useApi, useDocker }) => + * useApi && useDocker + * ? Effect.fail( + * new CliError.ValidationError({ + * message: "--use-api and --use-docker are mutually exclusive" + * }) + * ) + * : Effect.void + * ) + * ) + * + * const error = await Effect.runPromise( + * Command.runWith(download, { version: "1.0.0" })(["--use-api", "--use-docker"]).pipe( + * Effect.flip, + * Effect.provide(CliTestLayer) + * ) + * ) + * error._tag // => "ShowHelp" + * error.errors[0].message // => "--use-api and --use-docker are mutually exclusive" + * ``` + * + * @stability unstable + * @category combinators + * @since 4.1.0 + */ +export const validate: { + ( + f: (input: Input) => Effect.Effect + ): ( + self: Command + ) => Command + ( + self: Command, + f: (input: Input) => Effect.Effect + ): Command +} = dual(2, ( + self: Command, + f: (input: Input) => Effect.Effect +): Command => { + const impl = toImpl(self) + return makeCommand({ + ...impl, + parse: (raw) => Effect.tap(impl.parse(raw), f) + }) as any +}) + interface SubcommandGroupInternal { readonly group: string | undefined readonly commands: NonEmptyReadonlyArray @@ -954,7 +1049,7 @@ export const withSubcommands: { unlisted: impl.unlisted, annotations: impl.annotations, globalFlags: impl.globalFlags, - examples: impl.examples, + resolveExamples: impl.resolveExamples, service: impl.service, subcommands: normalized.groups, parse, @@ -1055,7 +1150,7 @@ export const withSharedFlags: { unlisted: impl.unlisted, annotations: impl.annotations, globalFlags: impl.globalFlags, - examples: impl.examples, + resolveExamples: impl.resolveExamples, service: impl.service as Context.Key, NextContextInput>, subcommands: impl.subcommands, parse, @@ -1370,6 +1465,10 @@ export const annotateMerge: { * Examples are exposed in structured `HelpDoc` data and rendered by the * default formatter in an `EXAMPLES` section. * + * Pass a function to build examples from the `commandPath` shown in help, so + * they stay correct wherever the command is mounted. The `examples` field + * resolves the function with the command's own name as the path. + * * **Example** (Adding usage examples) * * ```ts import.meta.vitest @@ -1385,22 +1484,48 @@ export const annotateMerge: { * login.examples.map((example) => example.command) // => ["myapp login", "myapp login --token sbp_abc123"] * ``` * + * **Example** (Deriving examples from the command path) + * + * ```ts import.meta.vitest + * import { Command } from "effect/cli" + * + * const deploy = Command.make("deploy").pipe( + * Command.withExamples(({ commandPath }) => [ + * { command: `${commandPath.join(" ")} --yes`, description: "Take every default" } + * ]) + * ) + * + * deploy.examples.map((example) => example.command) // => ["deploy --yes"] + * ``` + * * @stability unstable * @category combinators * @since 4.0.0 */ export const withExamples: { - (examples: ReadonlyArray): ( + ( + examples: + | ReadonlyArray + | ((context: { readonly commandPath: ReadonlyArray }) => ReadonlyArray) + ): ( self: Command ) => Command ( self: Command, - examples: ReadonlyArray + examples: + | ReadonlyArray + | ((context: { readonly commandPath: ReadonlyArray }) => ReadonlyArray) ): Command } = dual(2, ( self: Command, - examples: ReadonlyArray -) => makeCommand({ ...toImpl(self), examples })) + examples: + | ReadonlyArray + | ((context: { readonly commandPath: ReadonlyArray }) => ReadonlyArray) +) => + makeCommand({ + ...toImpl(self), + resolveExamples: Predicate.isFunction(examples) ? (commandPath) => examples({ commandPath }) : () => examples + })) /* ========================================================================== */ /* Providing Services */ @@ -1711,7 +1836,7 @@ const showHelp = ( ): Effect.Effect => Effect.gen(function*() { const { builtIns } = yield* CliConfig.CliConfig - const formatter = yield* CliOutput.Formatter + const formatter = yield* resolveFormatter const helpDoc = yield* getHelpForCommandPath(command, error.commandPath, builtIns) yield* Console.log(formatter.formatHelpDoc(helpDoc)) if (renderErrors && error.errors.length > 0) { @@ -1721,7 +1846,7 @@ const showHelp = ( const showUserError = (error: CliError.UserError): Effect.Effect => Effect.gen(function*() { - const formatter = yield* CliOutput.Formatter + const formatter = yield* resolveFormatter yield* Console.error(formatter.formatError(error)) error[Runtime.errorReported] = false }) diff --git a/repos/effect/packages/effect/src/cli/Completions.ts b/packages/effect/src/cli/Completions.ts similarity index 95% rename from repos/effect/packages/effect/src/cli/Completions.ts rename to packages/effect/src/cli/Completions.ts index 2b0996af3..c637764ff 100644 --- a/repos/effect/packages/effect/src/cli/Completions.ts +++ b/packages/effect/src/cli/Completions.ts @@ -53,6 +53,11 @@ export interface FlagDescriptor { * `=` (`--name=value`), never given as the next word. */ readonly optionalValue?: boolean | undefined + /** + * Long name that sets a boolean flag to `false`. Defaults to `no-`; + * `false` means the flag has no negated form. + */ + readonly negation?: string | false | undefined } /** diff --git a/repos/effect/packages/effect/src/cli/Flag.ts b/packages/effect/src/cli/Flag.ts similarity index 95% rename from repos/effect/packages/effect/src/cli/Flag.ts rename to packages/effect/src/cli/Flag.ts index 1d7f619f5..11625df85 100644 --- a/repos/effect/packages/effect/src/cli/Flag.ts +++ b/packages/effect/src/cli/Flag.ts @@ -546,6 +546,67 @@ export const withOptionalValue: { (self: Flag, value: string): Flag } = dual(2, (self: Flag, value: string) => Param.withOptionalValue(self, value)) +/** + * Renames the negated form of a boolean flag, which defaults to `--no-`. + * + * **Details** + * + * The new name replaces the default, so `--no-` is no longer accepted + * and shell completions offer the new name instead. Has no effect on + * non-boolean flags. + * + * **Example** (Renaming a boolean flag's negation) + * + * ```ts import.meta.vitest + * import { Flag } from "effect/cli" + * + * const validate = Flag.Boolean("validate").pipe( + * Flag.withDefault(true), + * Flag.withNegation("skip-validate") + * ) + * // --validate => true + * // --skip-validate => false + * // --no-validate => unrecognized flag + * validate.kind // => "flag" + * ``` + * + * @stability unstable + * @category combinators + * @since 4.1.0 + */ +export const withNegation: { + (name: string): (self: Flag) => Flag + (self: Flag, name: string): Flag +} = dual(2, (self: Flag, name: string) => Param.withNegation(self, name)) + +/** + * Removes the negated `--no-` form of a boolean flag. + * + * **Details** + * + * The negated form is then rejected as an unrecognized flag and left out of + * shell completions. Has no effect on non-boolean flags. + * + * **Example** (Disabling a boolean flag's negation) + * + * ```ts import.meta.vitest + * import { Flag } from "effect/cli" + * + * const unsafe = Flag.Boolean("unsafe").pipe( + * Flag.withDefault(false), + * Flag.withoutNegation + * ) + * // --unsafe => true + * // --no-unsafe => unrecognized flag + * unsafe.kind // => "flag" + * ``` + * + * @stability unstable + * @category combinators + * @since 4.1.0 + */ +export const withoutNegation = (self: Flag): Flag => Param.withoutNegation(self) + // ------------------------------------------------------------------------------------- // metadata // ------------------------------------------------------------------------------------- diff --git a/repos/effect/packages/effect/src/cli/GlobalFlag.ts b/packages/effect/src/cli/GlobalFlag.ts similarity index 98% rename from repos/effect/packages/effect/src/cli/GlobalFlag.ts rename to packages/effect/src/cli/GlobalFlag.ts index f1d8f4480..166f54f9b 100644 --- a/repos/effect/packages/effect/src/cli/GlobalFlag.ts +++ b/packages/effect/src/cli/GlobalFlag.ts @@ -17,11 +17,11 @@ import * as Context from "../Context.ts" import * as Effect from "../Effect.ts" import type { LogLevel as LogLevelType } from "../LogLevel.ts" import * as Option from "../Option.ts" -import * as CliOutput from "./CliOutput.ts" import type * as Command from "./Command.ts" import * as Completions_ from "./Completions.ts" import * as Flag from "./Flag.ts" import * as CommandDescriptor from "./internal/completions/descriptor.ts" +import { resolveFormatter } from "./internal/formatter.ts" import * as HelpInternal from "./internal/help.ts" /* ========================================================================== */ @@ -169,7 +169,7 @@ export const Help: Action = Action({ Flag.withDefault(false) ), run: Effect.fnUntraced(function*(_, { builtIns, command, commandPath }) { - const formatter = yield* CliOutput.Formatter + const formatter = yield* resolveFormatter const helpDoc = yield* HelpInternal.getHelpForCommandPath(command, commandPath, builtIns) yield* Console.log(formatter.formatHelpDoc(helpDoc)) }) @@ -193,7 +193,7 @@ export const Version: Action = Action({ Flag.withDefault(false) ), run: Effect.fnUntraced(function*(_, { command, version }) { - const formatter = yield* CliOutput.Formatter + const formatter = yield* resolveFormatter yield* Console.log(formatter.formatVersion(command.name, version)) }) }) diff --git a/repos/effect/packages/effect/src/cli/HelpDoc.ts b/packages/effect/src/cli/HelpDoc.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/HelpDoc.ts rename to packages/effect/src/cli/HelpDoc.ts diff --git a/repos/effect/packages/effect/src/cli/Param.ts b/packages/effect/src/cli/Param.ts similarity index 96% rename from repos/effect/packages/effect/src/cli/Param.ts rename to packages/effect/src/cli/Param.ts index 5fcc31e2f..793f8e8f7 100644 --- a/repos/effect/packages/effect/src/cli/Param.ts +++ b/packages/effect/src/cli/Param.ts @@ -217,6 +217,11 @@ export interface Single extends Param { * Raw value used for a bare flag. See `withOptionalValue`. */ readonly optionalValue?: string | undefined + /** + * Long name that sets a boolean flag to `false`. `undefined` means the + * default `no-`, `false` means no negated form. See `withNegation`. + */ + readonly negation?: string | false | undefined } /** @@ -367,6 +372,7 @@ export const makeSingle = (params: { readonly aliases?: ReadonlyArray | undefined readonly hidden?: boolean | undefined readonly optionalValue?: string | undefined + readonly negation?: string | false | undefined }): Single => { const parse: Parse = (args) => params.kind === argumentKind @@ -1867,6 +1873,74 @@ export const withOptionalValue: { optionalValue: value }))) +/** + * Renames the negated form of a boolean flag, which defaults to `--no-`. + * + * **Details** + * + * The new name replaces the default, so `--no-` is no longer accepted. + * Has no effect on non-boolean flags. + * + * **Example** (Renaming a boolean flag's negation) + * + * ```ts import.meta.vitest + * import { Param } from "effect/cli" + * + * // --validate sets true, --skip-validate sets false + * const validate = Param.Boolean(Param.flagKind, "validate").pipe( + * Param.withNegation("skip-validate") + * ) + * validate.kind // => "flag" + * ``` + * + * @stability unstable + * @category combinators + * @since 4.1.0 + */ +export const withNegation: { + (name: string): (self: Param) => Param + (self: Param, name: string): Param +} = dual(2, ( + self: Param, + name: string +) => + transformSingle(self, (single) => + makeSingle({ + ...single, + negation: name + }))) + +/** + * Removes the negated `--no-` form of a boolean flag. + * + * **Details** + * + * The negated form is then rejected as an unrecognized flag and left out of + * shell completions. Has no effect on non-boolean flags. + * + * **Example** (Disabling a boolean flag's negation) + * + * ```ts import.meta.vitest + * import { Param } from "effect/cli" + * + * // --unsafe sets true, --no-unsafe is rejected + * const unsafe = Param.Boolean(Param.flagKind, "unsafe").pipe( + * Param.withoutNegation + * ) + * unsafe.kind // => "flag" + * ``` + * + * @stability unstable + * @category combinators + * @since 4.1.0 + */ +export const withoutNegation = (self: Param): Param => + transformSingle(self, (single) => + makeSingle({ + ...single, + negation: false + })) + /** * Validates parsed values against a Schema, providing detailed error messages. * diff --git a/repos/effect/packages/effect/src/cli/Primitive.ts b/packages/effect/src/cli/Primitive.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/Primitive.ts rename to packages/effect/src/cli/Primitive.ts diff --git a/repos/effect/packages/effect/src/cli/Prompt.ts b/packages/effect/src/cli/Prompt.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/Prompt.ts rename to packages/effect/src/cli/Prompt.ts diff --git a/packages/effect/src/cli/TerminalCapabilities.ts b/packages/effect/src/cli/TerminalCapabilities.ts new file mode 100644 index 000000000..c8a3a0fe7 --- /dev/null +++ b/packages/effect/src/cli/TerminalCapabilities.ts @@ -0,0 +1,156 @@ +/** + * Detection of whether a CLI can color, animate, or prompt. + * + * The default reads `NO_COLOR`, `FORCE_COLOR`, `TERM`, and the TTY state of + * the current process. The layer reads the same variables through + * `ConfigProvider`, plus the TTY state of `Stdio`. Unless a + * `CliOutput.Formatter` is provided, the built-in help, version, and error + * output of `Command` follows `canColor`. + * + * @stability unstable + * @since 4.0.0 + */ +import * as Config from "../Config.ts" +import * as Context from "../Context.ts" +import * as Effect from "../Effect.ts" +import * as Layer from "../Layer.ts" +import * as Option from "../Option.ts" +import * as Stdio from "../Stdio.ts" + +/** + * Terminal rendering flags for the current process. + * + * **Details** + * + * `canColor` says whether styling will be rendered rather than stored as + * bytes. `canAnimate` says whether output is being watched, so pacing means + * something. `canPrompt` says whether a question can be asked and answered. + * Only `canColor` is read by the built-in `Command` output; the other two are + * for application code that renders its own output. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface TerminalCapabilities { + readonly canColor: boolean + readonly canAnimate: boolean + readonly canPrompt: boolean +} + +/** + * Reference to the terminal capabilities of the current process. + * + * **When to use** + * + * Use when application output should share the built-in help, version, and + * error output's answer. Provide `layer` to detect through + * `ConfigProvider` and `Stdio`, or provide a value to override detection. + * + * **Details** + * + * The default is detected from the current process the first time it is + * read, so later changes to the environment do not affect it. Without a + * `process` global, every flag is `false`. + * + * @stability unstable + * @category services + * @since 4.0.0 + */ +export const TerminalCapabilities: Context.Reference = Context.Reference( + "effect/cli/TerminalCapabilities", + { defaultValue: () => fromProcess() } +) + +const isPresent = (value: string | undefined): value is string => value !== undefined && value.length > 0 + +const isForcingColor = (value: string | undefined): boolean => + isPresent(value) && value !== "0" && value.toLowerCase() !== "false" + +const detect = (input: { + readonly noColor: string | undefined + readonly forceColor: string | undefined + readonly term: string | undefined + readonly stdinIsTerminal: boolean + readonly stdoutIsTerminal: boolean +}): TerminalCapabilities => { + if (input.term === "dumb") { + return { canColor: false, canAnimate: false, canPrompt: false } + } + + return { + canColor: !isPresent(input.noColor) && (isForcingColor(input.forceColor) || input.stdoutIsTerminal), + canAnimate: input.stdoutIsTerminal, + canPrompt: input.stdinIsTerminal && input.stdoutIsTerminal + } +} + +const fromProcess = (): TerminalCapabilities => { + const process = (globalThis as any).process + return detect({ + noColor: process?.env?.NO_COLOR, + forceColor: process?.env?.FORCE_COLOR, + term: process?.env?.TERM, + stdinIsTerminal: process?.stdin?.isTTY === true, + stdoutIsTerminal: process?.stdout?.isTTY === true + }) +} + +const environment = Config.all({ + noColor: Config.option(Config.String("NO_COLOR")), + forceColor: Config.option(Config.String("FORCE_COLOR")), + term: Config.option(Config.String("TERM")) +}) + +/** + * Detects terminal capabilities from `Stdio` and the current `ConfigProvider`. + * + * **Details** + * + * `TERM=dumb` clears all three flags, because a terminal that cannot move the + * cursor can neither animate nor redraw a prompt. Otherwise `NO_COLOR`, when + * set to any non-empty value, disables color and wins over `FORCE_COLOR`. + * `FORCE_COLOR` forces color even off a TTY, except for `0` and `false`, which + * mean "not forcing". Empty variables count as unset, matching + * `ConfigProvider.fromEnv`, so `FORCE_COLOR=` does not force color on. + * `canPrompt` requires both standard input and standard output to be + * terminals. A `ConfigProvider` fault is a defect, so the layer never fails. + * + * **Example** (NO_COLOR wins over FORCE_COLOR) + * + * ```ts import.meta.vitest + * import { ConfigProvider, Effect, Stdio } from "effect" + * import { TerminalCapabilities } from "effect/cli" + * + * const program = Effect.gen(function*() { + * const caps = yield* TerminalCapabilities.TerminalCapabilities + * return caps.canColor + * }).pipe( + * Effect.provide(TerminalCapabilities.layer), + * Effect.provide(Stdio.layerTest({ stdoutIsTerminal: Effect.succeed(true) })), + * Effect.provideService( + * ConfigProvider.ConfigProvider, + * ConfigProvider.fromEnv({ env: { NO_COLOR: "1", FORCE_COLOR: "1" } }) + * ) + * ) + * + * Effect.runSync(program) // => false + * ``` + * + * @stability unstable + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.effect( + TerminalCapabilities +)(Effect.gen(function*() { + const stdio = yield* Stdio.Stdio + const env = yield* Effect.orDie(environment) + return detect({ + noColor: Option.getOrUndefined(env.noColor), + forceColor: Option.getOrUndefined(env.forceColor), + term: Option.getOrUndefined(env.term), + stdinIsTerminal: yield* stdio.stdinIsTerminal, + stdoutIsTerminal: yield* stdio.stdoutIsTerminal + }) +})) diff --git a/repos/effect/packages/effect/src/cli/index.ts b/packages/effect/src/cli/index.ts similarity index 91% rename from repos/effect/packages/effect/src/cli/index.ts rename to packages/effect/src/cli/index.ts index 4ed3433ca..de545b8c0 100644 --- a/repos/effect/packages/effect/src/cli/index.ts +++ b/packages/effect/src/cli/index.ts @@ -76,3 +76,9 @@ export * as Primitive from "./Primitive.ts" * @since 4.0.0 */ export * as Prompt from "./Prompt.ts" + +/** + * @stability unstable + * @since 4.0.0 + */ +export * as TerminalCapabilities from "./TerminalCapabilities.ts" diff --git a/repos/effect/packages/effect/src/cli/internal/ansi.ts b/packages/effect/src/cli/internal/ansi.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/internal/ansi.ts rename to packages/effect/src/cli/internal/ansi.ts diff --git a/repos/effect/packages/effect/src/cli/internal/auto-suggest.ts b/packages/effect/src/cli/internal/auto-suggest.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/internal/auto-suggest.ts rename to packages/effect/src/cli/internal/auto-suggest.ts diff --git a/repos/effect/packages/effect/src/cli/internal/command.ts b/packages/effect/src/cli/internal/command.ts similarity index 96% rename from repos/effect/packages/effect/src/cli/internal/command.ts rename to packages/effect/src/cli/internal/command.ts index 7b52da248..0111fe06d 100644 --- a/repos/effect/packages/effect/src/cli/internal/command.ts +++ b/packages/effect/src/cli/internal/command.ts @@ -48,6 +48,7 @@ export interface CommandInternal commandPath: ReadonlyArray ) => Effect.Effect readonly buildHelpDoc: (commandPath: ReadonlyArray) => HelpDoc + readonly resolveExamples: (commandPath: ReadonlyArray) => ReadonlyArray } /* ========================================================================== */ @@ -99,7 +100,7 @@ export const makeCommand = | undefined + readonly resolveExamples?: ((commandPath: ReadonlyArray) => ReadonlyArray) | undefined readonly subcommands?: ReadonlyArray | undefined readonly parse?: ((input: ParsedTokens) => Effect.Effect) | undefined readonly parseContext?: @@ -115,6 +116,7 @@ export const makeCommand = = options.examples ?? [] + const examples: ReadonlyArray = resolveExamples(commandPath) return { description: options.description ?? "", @@ -204,7 +206,8 @@ export const makeCommand = => [] + /** * Converts a single flag param into a FlagDoc for help display. * diff --git a/repos/effect/packages/effect/src/cli/internal/completions/bash.ts b/packages/effect/src/cli/internal/completions/bash.ts similarity index 98% rename from repos/effect/packages/effect/src/cli/internal/completions/bash.ts rename to packages/effect/src/cli/internal/completions/bash.ts index 2ccead6ce..205edd771 100644 --- a/repos/effect/packages/effect/src/cli/internal/completions/bash.ts +++ b/packages/effect/src/cli/internal/completions/bash.ts @@ -7,7 +7,7 @@ * @internal */ import type * as Completions from "../../Completions.ts" -import { takesSeparateValue } from "./descriptor.ts" +import { negatedName, takesSeparateValue } from "./descriptor.ts" // --------------------------------------------------------------------------- // Helpers @@ -22,8 +22,9 @@ const flagNamesForWordlist = (flag: Completions.FlagDescriptor): Array = for (const alias of flag.aliases) { names.push(alias.length === 1 ? `-${alias}` : `--${alias}`) } - if (flag.type._tag === "Boolean") { - names.push(`--no-${flag.name}`) + const negation = negatedName(flag) + if (negation !== undefined) { + names.push(`--${negation}`) } return names } diff --git a/repos/effect/packages/effect/src/cli/internal/completions/descriptor.ts b/packages/effect/src/cli/internal/completions/descriptor.ts similarity index 92% rename from repos/effect/packages/effect/src/cli/internal/completions/descriptor.ts rename to packages/effect/src/cli/internal/completions/descriptor.ts index 378d418b1..b2e87e9ff 100644 --- a/repos/effect/packages/effect/src/cli/internal/completions/descriptor.ts +++ b/packages/effect/src/cli/internal/completions/descriptor.ts @@ -96,7 +96,8 @@ export const fromCommand = ( aliases: single.aliases, description: Option.getOrUndefined(single.description), type: toFlagType(single as Param.Single<"flag", unknown>), - optionalValue: single.optionalValue !== undefined + optionalValue: single.optionalValue !== undefined, + negation: single.negation }) } } @@ -148,3 +149,12 @@ export const fromCommand = ( */ export const takesSeparateValue = (flag: Completions.FlagDescriptor): boolean => flag.type._tag !== "Boolean" && flag.optionalValue !== true + +/** + * Long name that sets a boolean flag to `false`, or `undefined` when the flag + * has no negated form. + * + * @internal + */ +export const negatedName = (flag: Completions.FlagDescriptor): string | undefined => + flag.type._tag !== "Boolean" || flag.negation === false ? undefined : flag.negation ?? `no-${flag.name}` diff --git a/repos/effect/packages/effect/src/cli/internal/completions/fish.ts b/packages/effect/src/cli/internal/completions/fish.ts similarity index 95% rename from repos/effect/packages/effect/src/cli/internal/completions/fish.ts rename to packages/effect/src/cli/internal/completions/fish.ts index 0aefa710b..df4fce1b7 100644 --- a/repos/effect/packages/effect/src/cli/internal/completions/fish.ts +++ b/packages/effect/src/cli/internal/completions/fish.ts @@ -7,7 +7,7 @@ * @internal */ import type * as Completions from "../../Completions.ts" -import { takesSeparateValue } from "./descriptor.ts" +import { negatedName, takesSeparateValue } from "./descriptor.ts" // --------------------------------------------------------------------------- // Helpers @@ -62,8 +62,9 @@ const flagContainsOptCondition = (flag: Completions.FlagDescriptor): string => { optArgs.push(alias) } } - if (flag.type._tag === "Boolean") { - optArgs.push(`no-${flag.name}`) + const negation = negatedName(flag) + if (negation !== undefined) { + optArgs.push(negation) } return `not __fish_contains_opt ${optArgs.join(" ")}` } @@ -170,10 +171,11 @@ const generateCompletions = ( lines.push(parts.join(" ")) // Boolean negation - if (flag.type._tag === "Boolean") { + const negation = negatedName(flag) + if (negation !== undefined) { const negParts = [`complete -c ${executableName}`] if (flagCondArg) negParts.push(flagCondArg) - negParts.push(`-l no-${flag.name}`) + negParts.push(`-l ${negation}`) if (flag.description) { negParts.push(`-d '${escapeFishString(`Disable ${flag.name}`)}'`) } @@ -192,7 +194,7 @@ const generateCompletions = ( for (const flag of descriptor.flags) { const bareCondition = `${bareBase}; and ${flagContainsOptCondition(flag)}` - const isBoolean = flag.type._tag === "Boolean" + const negation = negatedName(flag) const parts = [`complete -c ${executableName}`] parts.push(`-n '${bareCondition}'`) @@ -202,10 +204,10 @@ const generateCompletions = ( } lines.push(parts.join(" ")) - if (isBoolean) { + if (negation !== undefined) { const negParts = [`complete -c ${executableName}`] negParts.push(`-n '${bareCondition}'`) - negParts.push(`-f -a '--no-${flag.name}'`) + negParts.push(`-f -a '--${negation}'`) if (flag.description) { negParts.push(`-d '${escapeFishString(`Disable ${flag.name}`)}'`) } diff --git a/repos/effect/packages/effect/src/cli/internal/completions/powershell.ts b/packages/effect/src/cli/internal/completions/powershell.ts similarity index 90% rename from repos/effect/packages/effect/src/cli/internal/completions/powershell.ts rename to packages/effect/src/cli/internal/completions/powershell.ts index 868d5a26a..9b1e0a9ac 100644 --- a/repos/effect/packages/effect/src/cli/internal/completions/powershell.ts +++ b/packages/effect/src/cli/internal/completions/powershell.ts @@ -8,6 +8,7 @@ * @internal */ import type * as Completions from "../../Completions.ts" +import { negatedName } from "./descriptor.ts" /** * Encode non-ASCII and control characters as `[char]` code units. Windows @@ -46,12 +47,15 @@ const valueFields = (type: Completions.FlagType | Completions.ArgumentType): str `values = ${psArray(type._tag === "Choice" ? type.values : [])}; ` + `pathType = ${type._tag === "Path" ? quotePs(type.pathType) : "$null"}` -const flagEntry = (flag: Completions.FlagDescriptor): string => - `@{ name = ${quotePs(flag.name)}; forms = ${psArray(flagForms(flag))}; ` + - `takesValue = $${flag.type._tag !== "Boolean"}; ` + - (flag.optionalValue === true ? `optionalValue = $true; ` : "") + - `${valueFields(flag.type)}; ` + - `description = ${psOptional(flag.description)} }` +const flagEntry = (flag: Completions.FlagDescriptor): string => { + const negation = negatedName(flag) + return `@{ name = ${quotePs(flag.name)}; forms = ${psArray(flagForms(flag))}; ` + + `takesValue = $${flag.type._tag !== "Boolean"}; ` + + (negation === undefined ? "" : `negation = ${quotePs(`--${negation}`)}; `) + + (flag.optionalValue === true ? `optionalValue = $true; ` : "") + + `${valueFields(flag.type)}; ` + + `description = ${psOptional(flag.description)} }` +} const argumentEntry = (argument: Completions.ArgumentDescriptor): string => `@{ name = ${quotePs(argument.name)}; variadic = $${argument.variadic}; ${valueFields(argument.type)}; ` + @@ -154,10 +158,10 @@ const completer = (dataName: string): string => foreach ($form in $flag.forms) { & $add $form 'ParameterName' $flag.description $typed '' } - if (-not $flag.takesValue) { + if ($flag.negation) { $tooltip = $null if ($flag.description) { $tooltip = 'Disable ' + $flag.name } - & $add ('--no-' + $flag.name) 'ParameterName' $tooltip $typed '' + & $add $flag.negation 'ParameterName' $tooltip $typed '' } } } @@ -228,14 +232,19 @@ const completer = (dataName: string): string => foreach ($form in $forms) { $pending = $null $flag = $lookup[$form] - if ($null -eq $flag) { - if ($form.StartsWith('--no-')) { - $name = $form.Substring(5) - $flag = $lookup['--' + $name] - if ($null -ne $flag -and -not $flag.takesValue -and $flag.name -ceq $name) { $used[$name] = $true } + # As in the CLI parser, a negation wins over an alias but not over a + # canonical name. + if ($null -eq $flag -or $form -cne ('--' + $flag.name)) { + $negated = $null + foreach ($candidate in $context.flags) { + if ($candidate.negation -ceq $form) { $negated = $candidate } + } + if ($null -ne $negated) { + $used[$negated.name] = $true + continue } - continue } + if ($null -eq $flag) { continue } $used[$flag.name] = $true # A flag with an optional value only takes a value attached with '='. if (-not $hasValue -and -not $flag.optionalValue) { $pending = $flag } diff --git a/repos/effect/packages/effect/src/cli/internal/completions/zsh.ts b/packages/effect/src/cli/internal/completions/zsh.ts similarity index 95% rename from repos/effect/packages/effect/src/cli/internal/completions/zsh.ts rename to packages/effect/src/cli/internal/completions/zsh.ts index 334bf4218..5bc91d73a 100644 --- a/repos/effect/packages/effect/src/cli/internal/completions/zsh.ts +++ b/packages/effect/src/cli/internal/completions/zsh.ts @@ -7,6 +7,7 @@ * @internal */ import type * as Completions from "../../Completions.ts" +import { negatedName } from "./descriptor.ts" // --------------------------------------------------------------------------- // Helpers @@ -22,15 +23,16 @@ const escapeZshChoice = (s: string): string => s.replace(/[^A-Za-z0-9_.,/@%+-]/g const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_]/g, "_") /** - * All forms of a flag (--name, -alias, --no-name) for the exclusion group. + * All forms of a flag (--name, -alias, negation) for the exclusion group. */ const allForms = (flag: Completions.FlagDescriptor): Array => { const forms: Array = [`--${flag.name}`] for (const alias of flag.aliases) { forms.push(alias.length === 1 ? `-${alias}` : `--${alias}`) } - if (flag.type._tag === "Boolean") { - forms.push(`--no-${flag.name}`) + const negation = negatedName(flag) + if (negation !== undefined) { + forms.push(`--${negation}`) } return forms } @@ -89,9 +91,10 @@ const flagSpecs = (flag: Completions.FlagDescriptor): Array => { } // Boolean negation - if (flag.type._tag === "Boolean") { + const negation = negatedName(flag) + if (negation !== undefined) { const negDesc = flag.description ? `[${escapeZshExplanation(`Disable ${flag.name}`)}]` : "" - specs.push(`'${excl}--no-${flag.name}${negDesc}'`) + specs.push(`'${excl}--${negation}${negDesc}'`) } return specs diff --git a/repos/effect/packages/effect/src/cli/internal/config.ts b/packages/effect/src/cli/internal/config.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/internal/config.ts rename to packages/effect/src/cli/internal/config.ts diff --git a/packages/effect/src/cli/internal/formatter.ts b/packages/effect/src/cli/internal/formatter.ts new file mode 100644 index 000000000..60fcf16ec --- /dev/null +++ b/packages/effect/src/cli/internal/formatter.ts @@ -0,0 +1,18 @@ +import * as Context from "../../Context.ts" +import * as Effect from "../../Effect.ts" +import * as CliOutput from "../CliOutput.ts" +import * as TerminalCapabilities from "../TerminalCapabilities.ts" + +/** + * The formatter for built-in command output. An installed `CliOutput.Formatter` + * wins, otherwise the default formatter colors when `TerminalCapabilities` + * says it can. + * + * @internal + */ +export const resolveFormatter: Effect.Effect = Effect.contextWith((context) => + Effect.succeed( + Context.getOrUndefined(context, CliOutput.Formatter) ?? + CliOutput.defaultFormatter({ colors: Context.get(context, TerminalCapabilities.TerminalCapabilities).canColor }) + ) +) diff --git a/repos/effect/packages/effect/src/cli/internal/help.ts b/packages/effect/src/cli/internal/help.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/internal/help.ts rename to packages/effect/src/cli/internal/help.ts diff --git a/repos/effect/packages/effect/src/cli/internal/lexer.ts b/packages/effect/src/cli/internal/lexer.ts similarity index 83% rename from repos/effect/packages/effect/src/cli/internal/lexer.ts rename to packages/effect/src/cli/internal/lexer.ts index 71fc7d343..3f534aa03 100644 --- a/repos/effect/packages/effect/src/cli/internal/lexer.ts +++ b/packages/effect/src/cli/internal/lexer.ts @@ -58,8 +58,12 @@ const lexTokens = (args: ReadonlyArray): ReadonlyArray => { const equalIndex = arg.indexOf("=") if (equalIndex !== -1) { const name = arg.slice(2, equalIndex) - const value = arg.slice(equalIndex + 1) - tokens.push({ _tag: "LongOption", name, raw: arg, value }) + if (name === "") { + tokens.push({ _tag: "Value", value: arg }) + } else { + const value = arg.slice(equalIndex + 1) + tokens.push({ _tag: "LongOption", name, raw: arg, value }) + } } else { tokens.push({ _tag: "LongOption", name: arg.slice(2), raw: arg }) } @@ -67,9 +71,12 @@ const lexTokens = (args: ReadonlyArray): ReadonlyArray => { const flags = arg.slice(1) const equalIndex = flags.indexOf("=") if (equalIndex !== -1) { - const flag = flags.slice(0, equalIndex) - const value = flags.slice(equalIndex + 1) - tokens.push({ _tag: "ShortOption", flag, raw: `-${flag}`, value }) + const chars = Array.from(flags.slice(0, equalIndex)) + const flag = chars.pop() ?? "" + for (const ch of chars) { + tokens.push({ _tag: "ShortOption", flag: ch, raw: `-${ch}` }) + } + tokens.push({ _tag: "ShortOption", flag, raw: `-${flag}`, value: flags.slice(equalIndex + 1) }) } else { for (const ch of flags) { tokens.push({ _tag: "ShortOption", flag: ch, raw: `-${ch}` }) diff --git a/repos/effect/packages/effect/src/cli/internal/parser.ts b/packages/effect/src/cli/internal/parser.ts similarity index 96% rename from repos/effect/packages/effect/src/cli/internal/parser.ts rename to packages/effect/src/cli/internal/parser.ts index 2e77922d4..4c917fa43 100644 --- a/repos/effect/packages/effect/src/cli/internal/parser.ts +++ b/packages/effect/src/cli/internal/parser.ts @@ -125,6 +125,8 @@ type FlagRegistry = { readonly params: ReadonlyArray /** Maps canonical names and aliases → flag param */ readonly index: Map + /** Maps negated names of boolean flags → flag param */ + readonly negations: Map } /** @@ -229,12 +231,22 @@ const makeCursor = (tokens: ReadonlyArray): TokenCursor => { /* Flag Registry */ /* ========================================================================== */ +/** + * Long name that sets a boolean flag to `false`, or `undefined` when the flag + * has no negated form. + */ +const negatedName = (param: FlagParam): string | undefined => + !Primitive.isBoolean(param.primitiveType) || param.negation === false + ? undefined + : param.negation ?? `no-${param.name}` + /** * Creates a registry for O(1) flag lookup by name or alias. * @internal */ export const createFlagRegistry = (params: ReadonlyArray): FlagRegistry => { const index = new Map() + const negations = new Map() for (const param of params) { if (index.has(param.name)) { @@ -250,9 +262,19 @@ export const createFlagRegistry = (params: ReadonlyArray): FlagRegist } index.set(alias, param) } + + const negation = negatedName(param) + if (negation !== undefined) { + if (negations.has(negation)) { + throw new Error( + `Duplicate negation "${negation}" in command definition (conflicts with "${negations.get(negation)!.name}")` + ) + } + negations.set(negation, param) + } } - return { params, index } + return { params, index, negations } } const buildSubcommandIndex = ( @@ -362,14 +384,11 @@ const resolveFlag = ( } } - if (token._tag === "LongOption" && token.name.startsWith("no-")) { - const canonicalName = token.name.slice(3) - const param = registry.index.get(canonicalName) - if (param && param.name === canonicalName && Primitive.isBoolean(param.primitiveType)) { - return { - param, - negated: true - } + const negated = token._tag === "LongOption" ? registry.negations.get(token.name) : undefined + if (negated) { + return { + param: negated, + negated: true } } @@ -740,8 +759,9 @@ const createUnrecognizedFlagError = ( // that was intentionally kept out of --help. if (p.hidden) continue validNames.push(p.name) - if (Primitive.isBoolean(p.primitiveType)) { - validNames.push(`no-${p.name}`) + const negation = negatedName(p) + if (negation !== undefined) { + validNames.push(negation) } for (const alias of p.aliases) { validNames.push(alias) diff --git a/repos/effect/packages/effect/src/cli/internal/wizard.ts b/packages/effect/src/cli/internal/wizard.ts similarity index 100% rename from repos/effect/packages/effect/src/cli/internal/wizard.ts rename to packages/effect/src/cli/internal/wizard.ts diff --git a/packages/effect/src/cluster/ClusterCron.ts b/packages/effect/src/cluster/ClusterCron.ts new file mode 100644 index 000000000..1c0ea9cbc --- /dev/null +++ b/packages/effect/src/cluster/ClusterCron.ts @@ -0,0 +1,225 @@ +/** + * Runs recurring cron jobs through cluster sharding. + * + * This module turns a `Cron.Cron` schedule into a `Layer` that coordinates one + * recurring job across a cluster. It registers a singleton for the initial + * scheduling step and a persisted entity message for each run. This is useful + * for distributed maintenance work where the job should be owned by the cluster + * rather than by every runner independently. + * + * With `calculateNextRunFromPrevious`, every run is processed by a single + * entity per cron job, so runs never overlap and are caught up in order. The + * scheduled time of the current run is available through + * `CurrentScheduledTime`. + * + * @stability unstable + * @since 4.0.0 + */ +import * as Context from "../Context.ts" +import * as Cron from "../Cron.ts" +import * as DateTime from "../DateTime.ts" +import * as Duration from "../Duration.ts" +import * as Effect from "../Effect.ts" +import * as Exit from "../Exit.ts" +import * as Layer from "../Layer.ts" +import * as Option from "../Option.ts" +import * as PrimaryKey from "../PrimaryKey.ts" +import * as Rpc from "../rpc/Rpc.ts" +import * as Schedule from "../Schedule.ts" +import * as Schema from "../Schema.ts" +import type { Scope } from "../Scope.ts" +import * as ClusterSchema from "./ClusterSchema.ts" +import { Persisted, Uninterruptible } from "./ClusterSchema.ts" +import * as DeliverAt from "./DeliverAt.ts" +import * as Entity from "./Entity.ts" +import type { Sharding } from "./Sharding.ts" +import * as Singleton from "./Singleton.ts" + +/** + * Service tag for the scheduled time of the cron run currently executing. + * + * **When to use** + * + * Use to read the time the current run was scheduled for, inside the + * `execute` effect of `make`. + * + * @stability unstable + * @category services + * @since 4.0.0 + */ +export class CurrentScheduledTime extends Context.Service< + CurrentScheduledTime, + DateTime.Utc +>()("effect/cluster/ClusterCron/CurrentScheduledTime") {} + +/** + * Creates a layer that runs a cron job through the cluster sharding system. + * + * **Details** + * + * The job is scheduled as persisted entity messages, with an initial singleton + * scheduling step and optional controls for shard group, next-run calculation, + * and skipping stale scheduled runs. + * + * @stability unstable + * @category constructors + * @since 4.0.0 + */ +export const make = (options: { + readonly name: string + readonly cron: Cron.Cron + readonly execute: Effect.Effect + + /** + * Choose a shard group to run this cron job on. + */ + readonly shardGroup?: string | undefined + + /** + * Controls whether the next cron job is based on the time of the previous + * run. + * + * **Details** + * + * Defaults to `false`, meaning the next run will be calculated from the + * current time. + * + * When enabled, all runs are processed one at a time by a single entity, so + * missed runs are caught up in order without overlapping. A run that fails + * is retried in place and blocks later runs until it succeeds or is older + * than `skipIfOlderThan`. + */ + readonly calculateNextRunFromPrevious?: boolean | undefined + + /** + * If set, the cron job will skip execution if the scheduled time is older + * than this duration. + * + * **When to use** + * + * Use to prevent running jobs that were scheduled too far in the past. + * + * **Details** + * + * Defaults to "1 day". + */ + readonly skipIfOlderThan?: Duration.Input | undefined +}): Layer.Layer, Scope>> => { + const fromPrevious = options.calculateNextRunFromPrevious === true + const nextRun = (after: DateTime.Utc) => DateTime.fromDateUnsafe(Cron.next(options.cron, after)) + + const CronEntity = Entity.make(`ClusterCron/${options.name}`, [ + Rpc.make("run", { + payload: CronPayload + }) + .annotate(Persisted, true) + .annotate(Uninterruptible, true), + Rpc.make("start").annotate(Persisted, true) + ]) + .annotate(ClusterSchema.ShardGroup, () => options.shardGroup ?? "default") + .annotate(ClusterSchema.ClientTracingEnabled, false) + + // with `calculateNextRunFromPrevious`, every run shares the executor address + const send = (dateTime: DateTime.Utc) => + Effect.flatMap(CronEntity.client, (makeClient) => + fromPrevious + ? makeClient(executorId).run(new CronPayload({ dateTime, executor: true }), { discard: true }) + : makeClient(DateTime.formatIso(dateTime)).run(new CronPayload({ dateTime }), { discard: true })) + + const schedule = (dateTime: DateTime.Utc) => send(dateTime).pipe(withRetry("Failed to schedule next run, retrying")) + + const InitialRun = Singleton.make( + `ClusterCron/${options.name}`, + fromPrevious + // the executor schedules the next run if its chain has been lost. + // `start` has no primary key, so each retry is a new message + ? Effect.flatMap(CronEntity.client, (makeClient) => makeClient(executorId).start()).pipe( + withRetry("Failed to start cron job, retrying") + ) + : Effect.flatMap( + DateTime.now, + (now) => send(nextRun(now)).pipe(withRetry("Failed to schedule initial run, retrying")) + ), + { shardGroup: options.shardGroup } + ) + + const skipIfOlderThan = Option.fromUndefinedOr(options.skipIfOlderThan).pipe( + Option.map(Duration.fromInputUnsafe), + Option.getOrElse(() => Duration.days(1)) + ) + + const execute = Effect.fnUntraced(function*(dateTime: DateTime.Utc) { + const now = yield* DateTime.now + if (DateTime.isLessThan(dateTime, DateTime.subtractDuration(now, skipIfOlderThan))) { + return + } + return yield* Effect.provideService(options.execute, CurrentScheduledTime, dateTime) + }, Effect.orDie) + + const EntityLayer = CronEntity.toLayer(Effect.gen(function*() { + const address = yield* Entity.CurrentAddress + // runs are processed one at a time, so a run processed before `start` + // means the chain is alive + let processedRun = false + return { + run: (request) => { + const dateTime = request.payload.dateTime + if (fromPrevious && address.entityId !== executorId) { + // a run scheduled by an earlier release joins the executor's chain + return schedule(dateTime) + } + processedRun = true + return Effect.onExitPrimitive( + execute(dateTime), + (exit) => + Effect.andThen( + Exit.isFailure(exit) ? Effect.logWarning(exit.cause) : Effect.void, + fromPrevious ? schedule(nextRun(dateTime)) : Effect.flatMap(DateTime.now, (now) => schedule(nextRun(now))) + ), + true + ).pipe( + Effect.annotateLogs({ + module: "effect/cluster/ClusterCron", + name: options.name, + dateTime + }) + ) + }, + start: () => + processedRun + ? Effect.void + // deduplicated against the pending run if the chain is alive + : Effect.flatMap(DateTime.now, (now) => schedule(nextRun(now))) + } + })) + + return Layer.merge(InitialRun, EntityLayer) +} + +const executorId = "executor" + +const retryPolicy = Schedule.min([ + Schedule.exponential(200, 1.5), + Schedule.spaced("1 minute") +]) + +const withRetry = (message: string) => (self: Effect.Effect): Effect.Effect => + self.pipe( + Effect.tapCause((cause) => Effect.logWarning(message, cause)), + Effect.sandbox, + Effect.retry(retryPolicy), + Effect.orDie + ) + +class CronPayload extends Schema.Class("effect/cluster/ClusterCron/CronPayload")({ + dateTime: Schema.DateTimeUtc, + // set for runs sent to the executor, which share one entity address + executor: Schema.optional(Schema.Boolean) +}) { + [PrimaryKey.symbol]() { + return this.executor ? DateTime.formatIso(this.dateTime) : "" + } + [DeliverAt.symbol]() { + return this.dateTime + } +} diff --git a/repos/effect/packages/effect/src/cluster/ClusterError.ts b/packages/effect/src/cluster/ClusterError.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/ClusterError.ts rename to packages/effect/src/cluster/ClusterError.ts diff --git a/packages/effect/src/cluster/ClusterMetrics.ts b/packages/effect/src/cluster/ClusterMetrics.ts new file mode 100644 index 000000000..4c2777982 --- /dev/null +++ b/packages/effect/src/cluster/ClusterMetrics.ts @@ -0,0 +1,260 @@ +/** + * Standard metric definitions used by the unstable cluster runtime. The gauges + * track runner-local entities, entity mailbox sizes, singleton processes, + * registered runners, healthy runners, acquired shards, and shards being + * released. The timer tracks shard release duration. + * + * This module only defines the metric handles. The runtime components that + * manage entities, runners, singletons, and sharding update these metrics while + * the cluster is running. + * + * @stability unstable + * @since 4.0.0 + */ +import * as Metric from "../Metric.ts" + +/** + * Creates a gauge tracking the number of active entity instances for each entity type on + * the current runner. + * + * **When to use** + * + * Use when instrumenting runner-local entity counts and tagging them by entity + * type for cluster dashboards. + * + * **Details** + * + * Bigint gauge named `effect_cluster_entities`, tagged with the entity type. + * Each entity manager registers a series that reads its active entity count + * when metrics are snapshotted, and removes it when the manager shuts down. + * + * **Gotchas** + * + * This gauge is runner-local. Aggregate across runners. + * + * @see {@link singletons} for singleton process counts on the current runner + * @see {@link shards} for shard ownership on the current runner + * + * @stability unstable + * @category metrics + * @since 4.0.0 + */ +export const entities = Metric.gauge("effect_cluster_entities", { bigint: true }) + +/** + * Represents a gauge tracking the mailbox size of each entity resident on the + * current runner. + * + * **When to use** + * + * Use to find entities whose mailboxes back up, before sends start failing + * with `MailboxFull`. + * + * **Details** + * + * Bigint gauge named `effect_cluster_entity_mailbox_size`, with one series per + * resident entity tagged with `type`, `id` and `shard`. The value is the + * number the mailbox capacity check compares against: requests admitted to the + * entity, whether queued or in flight, excluding internal keep-alive requests. + * + * The value is read when metrics are snapshotted, so it adds no work per + * message. Each series is removed when its entity is released. + * + * **Gotchas** + * + * Off by default. Enable it with `ShardingConfig.entityMailboxMetrics` or the + * `mailboxMetrics` option when registering an entity. One series per resident + * entity can mean many series on busy runners. + * + * An entity keeps reporting while it drains in-flight requests during + * shutdown. `Metric.update` and `Metric.modify` on this gauge do nothing for + * series the runtime registered. + * + * Like the other runner gauges it has no runner attribute: runners sharing a + * registry without distinct metric attributes overwrite each other's series. + * + * @see {@link entities} for the number of resident entities per type + * @see {@link entityMailboxSizeMax} for the always-on per-type maximum + * + * @stability unstable + * @category metrics + * @since 4.0.6 + */ +export const entityMailboxSize = Metric.gauge("effect_cluster_entity_mailbox_size", { bigint: true }) + +/** + * Represents a gauge tracking the largest mailbox size among the entities of + * each type resident on the current runner. + * + * **When to use** + * + * Use to alert on entity types whose mailboxes back up, before sends start + * failing with `MailboxFull`, without paying for a series per entity. + * + * **Details** + * + * Bigint gauge named `effect_cluster_entity_mailbox_size_max`, with one series + * per registered entity type tagged with `type`. The value is the largest + * mailbox size that {@link entityMailboxSize} would report for the type, + * including entities draining in-flight requests during shutdown, or `0` when + * the type has no resident entities. + * + * The value is computed when metrics are snapshotted, by visiting each + * resident entity of the type, so it adds no work per message. Entities + * draining during shutdown keep their slot until they finish, so the visit is + * bounded by `ShardingConfig.maxResidentEntities` across all types, unless it + * is `"unbounded"`. The series is removed when the entity type shuts down. + * + * **Gotchas** + * + * Always on. `Metric.update` and `Metric.modify` on this gauge do nothing for + * series the runtime registered. + * + * Like the other runner gauges it has no runner attribute: runners sharing a + * registry without distinct metric attributes overwrite each other's series. + * + * @see {@link entityMailboxSize} for the opt-in per-entity mailbox sizes + * + * @stability unstable + * @category metrics + * @since 4.0.6 + */ +export const entityMailboxSizeMax = Metric.gauge("effect_cluster_entity_mailbox_size_max", { bigint: true }) + +/** + * Creates a gauge tracking the number of singleton processes currently running on the + * current runner. + * + * @stability unstable + * @category metrics + * @since 4.0.0 + */ +export const singletons = Metric.gauge("effect_cluster_singletons", { bigint: true }) + +/** + * Represents a gauge tracking the number of registered cluster runners. + * + * **When to use** + * + * Use to monitor the registered runners currently known to the cluster runtime. + * + * **Gotchas** + * + * The value can lag briefly during membership changes or failure detection. + * + * @see {@link runnersHealthy} for the healthy-runner subset + * + * @stability unstable + * @category metrics + * @since 4.0.0 + */ +export const runners = Metric.gauge("effect_cluster_runners", { bigint: true }) + +/** + * Represents a gauge tracking the number of cluster runners currently considered healthy. + * + * **When to use** + * + * Use to monitor the healthy subset of registered cluster runners. + * + * **Details** + * + * Bigint gauge named `effect_cluster_runners_healthy`. + * + * **Gotchas** + * + * The value reflects the runtime's health-check view and can lag during + * membership changes or failure detection. + * + * @see {@link runners} for the total registered-runner gauge + * + * @stability unstable + * @category metrics + * @since 4.0.0 + */ +export const runnersHealthy = Metric.gauge("effect_cluster_runners_healthy", { bigint: true }) + +/** + * Represents a gauge tracking the number of shards currently acquired by the current runner. + * + * **When to use** + * + * Use to observe shard ownership held by the current runner during startup, + * rebalancing, or failover. + * + * **Details** + * + * Bigint gauge named `effect_cluster_shards`, updated from the sharding + * acquisition loop using the current acquired shard count. + * + * **Gotchas** + * + * This is runner-local, not a cluster-wide shard total. Aggregate per-runner + * values carefully. + * + * @stability unstable + * @category metrics + * @since 4.0.0 + */ +export const shards = Metric.gauge("effect_cluster_shards", { bigint: true }) + +/** + * Represents a gauge tracking the number of shards the current runner is still + * releasing. + * + * **When to use** + * + * Use to detect shard releases that stall during rebalancing, shutdown handoff + * or lock storage failures. + * + * **Details** + * + * Bigint gauge named `effect_cluster_shards_releasing`, updated with the number + * of distinct shards being released. A shard counts from the moment it is no + * longer acquired until its entities and singletons have stopped and its lock + * is released, including waits for lock storage to recover and release + * retries. + * + * **Gotchas** + * + * Positive values are normal while shards move; alert only when the value + * stays positive. It does not reflect lock ownership, and reacquisition can + * stay blocked briefly after it returns to zero, while a forced release + * finishes its bookkeeping. + * + * Like the other runner gauges it assumes one runner per process: runners + * sharing a registry without distinct metric attributes overwrite each other's + * value. + * + * @stability unstable + * @category metrics + * @since 4.1.0 + */ +export const shardsReleasing = Metric.gauge("effect_cluster_shards_releasing", { bigint: true }) + +/** + * Represents a timer tracking how long the current runner takes to release a + * shard after reassignment, lock loss or shutdown handoff. + * + * **When to use** + * + * Use to observe shard handoff latency during rebalancing, shutdown, or lock + * storage failures. + * + * **Details** + * + * Millisecond timer named `effect_cluster_shard_release`. Each sample covers + * entity teardown, retries and any wait for shard lock storage to recover. + * + * **Gotchas** + * + * Only completed releases are recorded. Interrupted releases and forced + * releases after lock storage becomes unhealthy are not. + * + * @stability unstable + * @category metrics + * @since 4.0.0 + */ +export const shardReleaseDuration = Metric.timer("effect_cluster_shard_release", { + description: "Time taken to release a shard, including entity teardown, retries and lock storage recovery" +}) diff --git a/repos/effect/packages/effect/src/cluster/ClusterSchema.ts b/packages/effect/src/cluster/ClusterSchema.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/ClusterSchema.ts rename to packages/effect/src/cluster/ClusterSchema.ts diff --git a/repos/effect/packages/effect/src/cluster/ClusterWorkflowEngine.ts b/packages/effect/src/cluster/ClusterWorkflowEngine.ts similarity index 90% rename from repos/effect/packages/effect/src/cluster/ClusterWorkflowEngine.ts rename to packages/effect/src/cluster/ClusterWorkflowEngine.ts index 8e0c3be7b..aa32bac39 100644 --- a/repos/effect/packages/effect/src/cluster/ClusterWorkflowEngine.ts +++ b/packages/effect/src/cluster/ClusterWorkflowEngine.ts @@ -24,7 +24,7 @@ import * as Rpc from "../rpc/Rpc.ts" import { ClientAbort } from "../rpc/RpcSchema.ts" import * as Schedule from "../Schedule.ts" import * as Schema from "../Schema.ts" -import type * as Scope from "../Scope.ts" +import * as Scope from "../Scope.ts" import * as Semaphore from "../Semaphore.ts" import * as Activity from "../workflow/Activity.ts" import * as DurableClock from "../workflow/DurableClock.ts" @@ -135,11 +135,21 @@ export const make = Effect.gen(function*() { // Rebuilds share an activation; overlapping activations have separate results. // Weak keys do not retain scopes, but contexts captured by clients, activities, // or finalizers can keep a closed scope (and its results) reachable until later. - const pendingByActivation = new WeakMap> - }>() + const pendingByActivation = new WeakMap() + // Closes the scopes retained by suspended runs. The latest run already closed + // its own scope with its real exit, so earlier finalizers observe success and + // skip superseded compensations. Abandonment releases owner-local resources + // without running durable finalizers, so the run can replay elsewhere. + const closeRuns = (state: ActivationState, abandoned: boolean) => + Effect.suspend(() => { + const scope = state.scope + if (abandoned) { + for (const instance of state.instances) instance.abandoned = true + } + state.instances.clear() + state.scope = Scope.makeUnsafe() + return Scope.close(scope, Exit.void) + }) const clients = yield* RcMap.make({ lookup: Effect.fnUntraced(function*(workflowName: string) { const entity = entities.get(workflowName) @@ -374,26 +384,25 @@ export const make = Effect.gen(function*() { // `Suspended`. Replies are only observed on the runner that saves them, so // callers on other runners, and any failure here, leave delivery to the // caller's retry schedule. - const awaitComplete = (workflow: Workflow.Any, executionId: string) => - Effect.gen(function*() { - const address = entityAddressFor({ workflow, entityType: `Workflow/${workflow._tag}`, executionId }) - if (!sharding.hasShardId(address.shardId)) return yield* Effect.never - const requestId = yield* storage.requestIdForPrimaryKey({ address, tag: "run", id: "" }) - if (Option.isNone(requestId)) return yield* Effect.never - const request = Envelope.makeRequest({ - requestId: requestId.value, - address, - tag: "run", - payload: {}, - headers: Headers.empty - }) - // A replay that suspends again re-arms the wait, so it does not spend a - // step of the caller's retry schedule. - let settled = false - while (!settled) { - settled = yield* awaitRunReply(workflow, request, (reply) => !isSuspendedReply(reply)) - } - }).pipe(Effect.catchCause(() => Effect.never)) + const awaitComplete = Effect.fnUntraced(function*(workflow: Workflow.Any, executionId: string) { + const address = entityAddressFor({ workflow, entityType: `Workflow/${workflow._tag}`, executionId }) + if (!sharding.hasShardId(address.shardId)) return yield* Effect.never + const requestId = yield* storage.requestIdForPrimaryKey({ address, tag: "run", id: "" }) + if (Option.isNone(requestId)) return yield* Effect.never + const request = Envelope.makeRequest({ + requestId: requestId.value, + address, + tag: "run", + payload: {}, + headers: Headers.empty + }) + // A replay that suspends again re-arms the wait, so it does not spend a + // step of the caller's retry schedule. + let settled = false + while (!settled) { + settled = yield* awaitRunReply(workflow, request, (reply) => !isSuspendedReply(reply)) + } + }, Effect.catchCause(() => Effect.never)) const interrupt = Effect.fnUntraced( function*(workflow: Workflow.Any, executionId: string) { @@ -465,7 +474,14 @@ export const make = Effect.gen(function*() { (decoded.exit._tag === "Success" ? decoded.exit.value : decoded.exit) as Exit.Exit ) } - pending = { workflowName: workflow._tag, executionId, results } + pending = { + workflowName: workflow._tag, + executionId, + results, + instances: new Set(), + scope: Scope.makeUnsafe() + } + yield* Scope.addFinalizer(activation, closeRuns(pending, true)) pendingByActivation.set(activation, pending) } // Retain awaited names through reply persistence, including suspension. @@ -476,24 +492,34 @@ export const make = Effect.gen(function*() { // Activity awaits must wake the run after suspension without preempting it. readonly activityAwaits: Map } | undefined - // Concurrent wakes share one wait for the current run to publish its reply. - // Explicit wakes cannot be dropped: operator resume bypasses the gate; - // InterruptSignal enters it only on a fresh entity, with no run to wait for. + // Concurrent automatic wakes share one wait for the current run to + // publish its reply. Explicit wakes cannot be dropped, because an + // automatic wake skips the reset of a failure suspension, so they + // wait outside the gate. Operator resume does not use the gate. const resumeGate = Semaphore.makeUnsafe(1) + const waitForCurrentRunReply = Effect.suspend(() => + currentRun ? waitForRunReply(workflow, currentRun.request) : Effect.void + ) const resumeCurrentRun = (mode: WakeMode) => - Effect.suspend(() => - resumeGate.withPermitsIfAvailable(1)( - currentRun ? waitForRunReply(workflow, currentRun.request) : Effect.void - ) - ).pipe( - // Release the gate before reset can start another run, so later - // completions can wake that replay. - Effect.flatMap((waited) => Option.isSome(waited) ? resume(workflow, executionId, mode) : Effect.void), - ensureSuccess + ensureSuccess( + mode === "explicit" + ? Effect.andThen(waitForCurrentRunReply, resume(workflow, executionId, mode)) + : resumeGate.withPermitsIfAvailable(1)(waitForCurrentRunReply).pipe( + // Release the gate before reset can start another run, so later + // completions can wake that replay. + Effect.flatMap((waited) => + Option.isSome(waited) ? resume(workflow, executionId, mode) : Effect.void + ) + ) ) return { run: (request: Entity.Request) => { - const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId) + // Each run gets a fresh scope under the activation's parent, so + // replays rebuild compensations while suspended runs keep theirs. + const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId, { + scope: Scope.forkUnsafe(pending.scope) + }) + pending.instances.add(instance) currentRun = { request, instance, activityAwaits: new Map() } const parent = (request.payload as any)[payloadParentKey] as | { workflowName: string; executionId: string; detached?: boolean } @@ -533,6 +559,11 @@ export const make = Effect.gen(function*() { ) }), Workflow.intoResult, + Effect.onExit((exit) => + Exit.isSuccess(exit) && exit.value._tag === "Suspended" + ? Effect.void + : closeRuns(pending, instance.abandoned) + ), (effect) => deferredState.trackRun(instance, effect), // Request middleware and local callers may carry another entity's context. Effect.provideService(CurrentActivationScope, activation) @@ -591,8 +622,10 @@ export const make = Effect.gen(function*() { // a later await would require waiting for its reply before reset. // A run completing its own deferred also reads it from the cache // when it awaits later, unless an activity is already parked on it. + // A run only reads InterruptSignal while suspending, so an interrupt + // always waits for the current run's reply before it resets the run. const wake = Effect.suspend(() => - currentRun && !currentRun.instance.awaitedDeferreds.has(payload.name) + currentRun && mode === "automatic" && !currentRun.instance.awaitedDeferreds.has(payload.name) ? currentRun.activityAwaits.has(payload.name) // Hold an activity wake until the run has published its reply. ? resumeCurrentRun(mode) @@ -909,6 +942,16 @@ const DeferredRpc = Rpc.make("deferred", { type WakeMode = "explicit" | "automatic" +interface ActivationState { + readonly workflowName: string + readonly executionId: string + readonly results: Map> + // Every run forks its scope from here, so runs that suspended keep their + // resources and durable finalizers until the execution finishes. + scope: Scope.Closeable + readonly instances: Set +} + const isSuspendedReply = (reply: WithExitEncoded>) => reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended" diff --git a/repos/effect/packages/effect/src/cluster/DeliverAt.ts b/packages/effect/src/cluster/DeliverAt.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/DeliverAt.ts rename to packages/effect/src/cluster/DeliverAt.ts diff --git a/repos/effect/packages/effect/src/cluster/Entity.ts b/packages/effect/src/cluster/Entity.ts similarity index 99% rename from repos/effect/packages/effect/src/cluster/Entity.ts rename to packages/effect/src/cluster/Entity.ts index b9da75bee..23704305d 100644 --- a/repos/effect/packages/effect/src/cluster/Entity.ts +++ b/packages/effect/src/cluster/Entity.ts @@ -147,6 +147,7 @@ export interface Entity< readonly maxIdleTime?: Duration.Input | undefined readonly concurrency?: number | "unbounded" | undefined readonly mailboxCapacity?: number | "unbounded" | undefined + readonly mailboxMetrics?: boolean | undefined readonly disableFatalDefects?: boolean | undefined readonly defectRetryPolicy?: Schedule.Schedule | undefined readonly spanAttributes?: Record | undefined @@ -191,6 +192,7 @@ export interface Entity< options?: { readonly maxIdleTime?: Duration.Input | undefined readonly mailboxCapacity?: number | "unbounded" | undefined + readonly mailboxMetrics?: boolean | undefined readonly disableFatalDefects?: boolean | undefined readonly defectRetryPolicy?: Schedule.Schedule | undefined readonly spanAttributes?: Record | undefined @@ -287,6 +289,7 @@ const Proto = { readonly maxIdleTime?: Duration.Input | undefined readonly concurrency?: number | "unbounded" | undefined readonly mailboxCapacity?: number | "unbounded" | undefined + readonly mailboxMetrics?: boolean | undefined readonly disableFatalDefects?: boolean | undefined readonly defectRetryPolicy?: Schedule.Schedule | undefined readonly spanAttributes?: Record | undefined @@ -335,6 +338,7 @@ const Proto = { options?: { readonly maxIdleTime?: Duration.Input | undefined readonly mailboxCapacity?: number | "unbounded" | undefined + readonly mailboxMetrics?: boolean | undefined readonly disableFatalDefects?: boolean | undefined readonly defectRetryPolicy?: Schedule.Schedule | undefined readonly spanAttributes?: Record | undefined diff --git a/repos/effect/packages/effect/src/cluster/EntityAddress.ts b/packages/effect/src/cluster/EntityAddress.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/EntityAddress.ts rename to packages/effect/src/cluster/EntityAddress.ts diff --git a/repos/effect/packages/effect/src/cluster/EntityId.ts b/packages/effect/src/cluster/EntityId.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/EntityId.ts rename to packages/effect/src/cluster/EntityId.ts diff --git a/repos/effect/packages/effect/src/cluster/EntityProxy.ts b/packages/effect/src/cluster/EntityProxy.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/EntityProxy.ts rename to packages/effect/src/cluster/EntityProxy.ts diff --git a/repos/effect/packages/effect/src/cluster/EntityProxyServer.ts b/packages/effect/src/cluster/EntityProxyServer.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/EntityProxyServer.ts rename to packages/effect/src/cluster/EntityProxyServer.ts diff --git a/repos/effect/packages/effect/src/cluster/EntityResource.ts b/packages/effect/src/cluster/EntityResource.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/EntityResource.ts rename to packages/effect/src/cluster/EntityResource.ts diff --git a/repos/effect/packages/effect/src/cluster/EntityType.ts b/packages/effect/src/cluster/EntityType.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/EntityType.ts rename to packages/effect/src/cluster/EntityType.ts diff --git a/repos/effect/packages/effect/src/cluster/Envelope.ts b/packages/effect/src/cluster/Envelope.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/Envelope.ts rename to packages/effect/src/cluster/Envelope.ts diff --git a/repos/effect/packages/effect/src/cluster/HttpRunner.ts b/packages/effect/src/cluster/HttpRunner.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/HttpRunner.ts rename to packages/effect/src/cluster/HttpRunner.ts diff --git a/repos/effect/packages/effect/src/cluster/K8sHttpClient.ts b/packages/effect/src/cluster/K8sHttpClient.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/K8sHttpClient.ts rename to packages/effect/src/cluster/K8sHttpClient.ts diff --git a/repos/effect/packages/effect/src/cluster/K8sTypes.ts b/packages/effect/src/cluster/K8sTypes.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/K8sTypes.ts rename to packages/effect/src/cluster/K8sTypes.ts diff --git a/repos/effect/packages/effect/src/cluster/MachineId.ts b/packages/effect/src/cluster/MachineId.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/MachineId.ts rename to packages/effect/src/cluster/MachineId.ts diff --git a/repos/effect/packages/effect/src/cluster/Message.ts b/packages/effect/src/cluster/Message.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/Message.ts rename to packages/effect/src/cluster/Message.ts diff --git a/repos/effect/packages/effect/src/cluster/MessageStorage.ts b/packages/effect/src/cluster/MessageStorage.ts similarity index 99% rename from repos/effect/packages/effect/src/cluster/MessageStorage.ts rename to packages/effect/src/cluster/MessageStorage.ts index b1876c5c0..f14101ea6 100644 --- a/repos/effect/packages/effect/src/cluster/MessageStorage.ts +++ b/packages/effect/src/cluster/MessageStorage.ts @@ -119,6 +119,13 @@ export class MessageStorage extends Context.Service( message: Message.OutgoingRequest | Message.IncomingRequest diff --git a/repos/effect/packages/effect/src/cluster/Reply.ts b/packages/effect/src/cluster/Reply.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/Reply.ts rename to packages/effect/src/cluster/Reply.ts diff --git a/repos/effect/packages/effect/src/cluster/Runner.ts b/packages/effect/src/cluster/Runner.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/Runner.ts rename to packages/effect/src/cluster/Runner.ts diff --git a/repos/effect/packages/effect/src/cluster/RunnerAddress.ts b/packages/effect/src/cluster/RunnerAddress.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/RunnerAddress.ts rename to packages/effect/src/cluster/RunnerAddress.ts diff --git a/repos/effect/packages/effect/src/cluster/RunnerHealth.ts b/packages/effect/src/cluster/RunnerHealth.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/RunnerHealth.ts rename to packages/effect/src/cluster/RunnerHealth.ts diff --git a/repos/effect/packages/effect/src/cluster/RunnerServer.ts b/packages/effect/src/cluster/RunnerServer.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/RunnerServer.ts rename to packages/effect/src/cluster/RunnerServer.ts diff --git a/repos/effect/packages/effect/src/cluster/RunnerStorage.ts b/packages/effect/src/cluster/RunnerStorage.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/RunnerStorage.ts rename to packages/effect/src/cluster/RunnerStorage.ts diff --git a/repos/effect/packages/effect/src/cluster/Runners.ts b/packages/effect/src/cluster/Runners.ts similarity index 88% rename from repos/effect/packages/effect/src/cluster/Runners.ts rename to packages/effect/src/cluster/Runners.ts index 7872bcaee..002382062 100644 --- a/repos/effect/packages/effect/src/cluster/Runners.ts +++ b/packages/effect/src/cluster/Runners.ts @@ -265,17 +265,15 @@ export const make: ( // The pump is owned by the shared entry rather than the first caller, so any // waiter can be interrupted without stopping delivery to the others. + // + // `replyFromStorage` registers the request with the storage loop before the + // pump starts, so the pump only re-registers after a stream chunk is acked. const pumpReplies = Effect.fnUntraced(function*( requestId: Snowflake.Snowflake, entry: Omit ) { while (true) { // wait for the storage loop to notify us - entry.latch.closeUnsafe() - const message = entry.messages.values().next().value - if (!message) return - waitingStorageRequests.set(requestId, message) - storageLatch.openUnsafe() yield* entry.latch.await // send the replies back @@ -300,6 +298,13 @@ export const make: ( yield* entry.latch.await } entry.replies = [] + + // ask the storage loop for the next chunk + entry.latch.closeUnsafe() + const message = entry.messages.values().next().value + if (!message) return + waitingStorageRequests.set(requestId, message) + storageLatch.openUnsafe() } }) @@ -316,9 +321,14 @@ export const make: ( } entry = Object.assign(state, { pump: yield* Effect.forkIn(pumpReplies(requestId, state), runnersScope) }) storageRequests.set(requestId, entry) + waitingStorageRequests.set(requestId, message) } const current = entry current.messages.add(message) + // A waiter that joins an existing entry, such as a duplicate, reads + // now. A reply committed without a notification would otherwise wait + // for the next poll. + storageLatch.openUnsafe() return yield* restore(Fiber.join(current.pump)).pipe( Effect.ensuring(Effect.suspend(() => { current.messages.delete(message) @@ -336,6 +346,52 @@ export const make: ( }) ) + // A request that may already be processing has to read its replies back + // from storage. A local reply handler wakes the storage loop, so streams do + // not have to wait for the next poll. + const replyFromStorageWithWake = (message: Message.OutgoingRequest) => + Effect.raceFirst( + replyFromStorage(message), + storage.registerReplyHandler( + new Message.OutgoingRequest({ ...message, respond: () => storageLatch.open }) + ).pipe(Effect.ignore, Effect.andThen(Effect.never)) + ) + + // Stream replies stay on the storage loop, which delivers chunks in order + // and waits for their acks. + const canReplyLocally = (message: Message.OutgoingRequest) => { + const rpc = message.rpc as any as Rpc.AnyWithProps + return storage !== MessageStorage.noop && Context.get(rpc.annotations, Persisted) && + !RpcSchema.isStreamSchema(rpc.successSchema) + } + + // A request can be processed as soon as it is saved, so its reply handler is + // registered first. `registerReplyHandler` registers as soon as it starts, + // so a fresh request gets its reply from the handler without reading it + // back from storage. + const notifyLocalRequest = Effect.fnUntraced(function*( + message: Message.OutgoingRequest, + notifyEntity: (message: Message.Outgoing) => Effect.Effect + ) { + const handler = yield* Effect.forkScoped(storage.registerReplyHandler(message), { startImmediately: true }) + return yield* notifyWith(message, (message, duplicate) => { + const outgoing = message as Message.OutgoingRequest + // A duplicate is answered under its original request id. + if (duplicate) { + return Fiber.interrupt(handler).pipe( + Effect.andThen(notifyEntity(message)), + Effect.andThen(replyFromStorageWithWake(outgoing)) + ) + } + // The handler can be dropped, for example when its shard moves or a + // commit error hides the notification. The reply is then in storage. + return notifyEntity(message).pipe( + Effect.andThen(Fiber.join(handler)), + Effect.catchTag("EntityNotAssignedToRunner", () => replyFromStorage(outgoing)) + ) + }) + }, Effect.scoped) + const storageLatch = Latch.makeUnsafe(false) if (storage !== MessageStorage.noop) { yield* Effect.gen(function*() { @@ -431,28 +487,23 @@ export const make: ( }) }, notifyLocal(options) { - return notifyWith(options.message, (message) => { + const message = options.message + const notifyEntity = (message: Message.Outgoing) => + Effect.catchTag( + options.notify(Message.incomingLocalFromOutgoing(message)), + "EntityNotAssignedToRunner", + () => Effect.void + ) + if (message._tag === "OutgoingRequest" && !options.discard && !options.storageOnly && canReplyLocally(message)) { + return notifyLocalRequest(message, notifyEntity) + } + return notifyWith(message, (message) => { if (options.discard || message._tag === "OutgoingEnvelope") { - return Effect.catchTag( - options.notify(Message.incomingLocalFromOutgoing(message)), - "EntityNotAssignedToRunner", - () => Effect.void - ) + return notifyEntity(message) } - // The request can be processed as soon as it is saved, so replies are - // always read back from storage. The local reply handler only wakes the - // storage loop, so streams do not have to wait for the next poll. - const readReplies = options.storageOnly - ? replyFromStorage(message) - : Effect.raceFirst( - replyFromStorage(message), - storage.registerReplyHandler( - new Message.OutgoingRequest({ ...message, respond: () => storageLatch.open }) - ).pipe(Effect.ignore, Effect.andThen(Effect.never)) - ) - return options.notify(Message.incomingLocalFromOutgoing(message)).pipe( - Effect.catchTag("EntityNotAssignedToRunner", () => Effect.void), - Effect.andThen(readReplies) + return Effect.andThen( + notifyEntity(message), + options.storageOnly ? replyFromStorage(message) : replyFromStorageWithWake(message) ) }) } diff --git a/repos/effect/packages/effect/src/cluster/ShardId.ts b/packages/effect/src/cluster/ShardId.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/ShardId.ts rename to packages/effect/src/cluster/ShardId.ts diff --git a/repos/effect/packages/effect/src/cluster/Sharding.ts b/packages/effect/src/cluster/Sharding.ts similarity index 93% rename from repos/effect/packages/effect/src/cluster/Sharding.ts rename to packages/effect/src/cluster/Sharding.ts index 2e088dfd7..4142f42bd 100644 --- a/repos/effect/packages/effect/src/cluster/Sharding.ts +++ b/packages/effect/src/cluster/Sharding.ts @@ -27,6 +27,7 @@ import * as HashRing from "../HashRing.ts" import * as InternalRecord from "../internal/record.ts" import * as Latch from "../Latch.ts" import * as Layer from "../Layer.ts" +import * as Metric from "../Metric.ts" import * as MutableHashMap from "../MutableHashMap.ts" import * as MutableHashSet from "../MutableHashSet.ts" import * as MutableRef from "../MutableRef.ts" @@ -141,6 +142,7 @@ export class Sharding extends Context.Service | undefined readonly spanAttributes?: Record | undefined @@ -276,6 +278,17 @@ const make = Effect.gen(function*() { // the active shards are the ones that we have acquired the lock for const acquiredShards = MutableHashSet.empty() + // shards still held by this runner while their entities stop and their locks + // are released + const releasingShards = MutableHashSet.empty() + // The releasing gauge reports to the metric registry and attributes the + // layer was built with. + const updateReleasingShardsGauge = () => + ClusterMetrics.shardsReleasing.updateUnsafe(BigInt(MutableHashSet.size(releasingShards)), services) + const updateShardGauges = () => { + ClusterMetrics.shards.updateUnsafe(BigInt(MutableHashSet.size(acquiredShards)), Context.empty()) + updateReleasingShardsGauge() + } const activeShardsLatch = yield* Latch.make(false) // open once shutdown starts handing every shard off const handoffLatch = Latch.makeUnsafe(false) @@ -343,7 +356,9 @@ const make = Effect.gen(function*() { // This should be shutdown last, when all entities have been shutdown, to // allow them to move to another runner. - const releasingShards = MutableHashSet.empty() + // Keep startup fibers behind singleton initialization without moving resource + // acquisition (and therefore changing the shutdown finalizer order). + const singletonsReady = Latch.makeUnsafe(false) // Hands every held shard off on shutdown, set when this is a runner let handoff: Effect.Effect = Effect.void // Shards whose entities must be force interrupted and locks fully released @@ -352,6 +367,7 @@ const make = Effect.gen(function*() { const initialRunnerAddress = getRunnerAddress() if (initialRunnerAddress) { const selfAddress = initialRunnerAddress + updateReleasingShardsGauge() yield* Scope.addFinalizerExit(shardingScope, () => { // the locks expire over time, so if this fails we ignore it return Effect.ignore(runnerStorage.releaseAll(selfAddress)) @@ -405,6 +421,7 @@ const make = Effect.gen(function*() { for (const shardId of shardIds) { MutableHashSet.remove(releasingShards, shardId) MutableHashSet.remove(forceReleasingShards, shardId) + updateReleasingShardsGauge() // During shutdown, parked waiters are interrupted: this runner will // never observe the reply. During a rebalance the waiters are resumed // with `EntityNotAssignedToRunner`, and the client falls back to @@ -419,22 +436,30 @@ const make = Effect.gen(function*() { const retryShardRelease = (annotations: { readonly fiber: string; readonly shardId?: ShardId }) => (effect: Effect.Effect) => - effect.pipe( - Effect.sandbox, - Effect.tapError((cause) => - Effect.logDebug(`Could not release shards, retrying`, cause).pipe( - Effect.annotateLogs({ - module: "effect/cluster/Sharding", - runner: selfAddress, - ...annotations - }), - // Effect.eventually retries immediately, so space failures to - // avoid hot-looping while storage is unavailable. - Effect.andThen(Effect.sleep(50)) - ) - ), - Effect.eventually - ) + Effect.suspend(() => { + let lastWarning = -Infinity + return effect.pipe( + Effect.sandbox, + Effect.tapError((cause) => + Effect.suspend(() => { + const now = clock.currentTimeMillisUnsafe() + if (now - lastWarning < 30_000) return Effect.void + lastWarning = now + return Effect.logWarning("Could not release shards, retrying", cause) + }).pipe( + Effect.annotateLogs({ + module: "effect/cluster/Sharding", + runner: selfAddress, + ...annotations + }), + // Effect.eventually retries immediately, so space failures to + // avoid hot-looping while storage is unavailable. + Effect.andThen(Effect.sleep(50)) + ) + ), + Effect.eventually + ) + }) const releaseShard = Effect.fnUntraced( function*(shardId: ShardId) { yield* runShardRelease( @@ -446,6 +471,8 @@ const make = Effect.gen(function*() { (effect, shardId) => effect.pipe( retryShardRelease({ fiber: "releaseShard", shardId }), + Effect.timed, + Effect.flatMap(([duration]) => Metric.update(ClusterMetrics.shardReleaseDuration, duration)), FiberMap.run(releaseShardsMap, shardId, { onlyIfMissing: true }) ) ) @@ -454,14 +481,62 @@ const make = Effect.gen(function*() { // normal path while it is pending, otherwise the bulk release wipes a lock // the runner already considers acquired. const forcedShardReleasePending = () => forcedShardReleaseRunning || MutableHashSet.size(forceReleasingShards) > 0 + // Repeat while pending, but no more often than every 30 seconds + const forcedReleaseWarningInterval = Duration.max( + Duration.fromInputUnsafe(config.shardLockExpiration), + Duration.seconds(30) + ) + // Names the shards still blocking reacquisition and the entity types and + // singletons still stopping on them + const warnForcedReleasePending = Effect.suspend(() => { + const entityTypes = Arr.empty() + for (const [entityType, state] of entityManagers) { + for (const shardId of forceReleasingShards) { + if (state.manager.hasServersUnsafe(shardId)) { + entityTypes.push(entityType) + break + } + } + } + const singletonNames = new Set() + for (const shardId of forceReleasingShards) { + for (const [address] of singletons.get(shardId) ?? []) { + if (FiberMap.hasUnsafe(singletonFibers, address)) singletonNames.add(address.name) + } + } + MutableHashMap.forEach(stoppingSingletons, (_, address) => { + if (MutableHashSet.has(forceReleasingShards, address.shardId)) singletonNames.add(address.name) + }) + return Effect.logWarning( + "Forced shard release is still pending; reacquisition is blocked until release completes" + ).pipe(Effect.annotateLogs({ + module: "effect/cluster/Sharding", + runner: selfAddress, + shardIds: Array.from(forceReleasingShards, String), + entityTypes, + singletonNames: [...singletonNames] + })) + }) const releaseForcedShards = Effect.suspend(() => { if (forcedShardReleaseRunning || MutableHashSet.size(forceReleasingShards) === 0) { return Effect.void } forcedShardReleaseRunning = true const shardIds = [...forceReleasingShards] - return runShardRelease(shardIds, true, runnerStorage.releaseAll(selfAddress)).pipe( - retryShardRelease({ fiber: "releaseForcedShards" }), + return Effect.gen(function*() { + // Observe independently: a timeout around release cannot interrupt + // uninterruptible work, and must not allow locks to be reacquired. + const monitor = yield* warnForcedReleasePending.pipe( + Effect.repeat(Schedule.spaced(forcedReleaseWarningInterval)), + Effect.delay(config.shardLockExpiration), + Effect.interruptible, + Effect.forkIn(releaseScope) + ) + yield* runShardRelease(shardIds, true, runnerStorage.releaseAll(selfAddress)).pipe( + retryShardRelease({ fiber: "releaseForcedShards" }), + Effect.ensuring(Fiber.interrupt(monitor)) + ) + }).pipe( Effect.ensuring(Effect.sync(() => { forcedShardReleaseRunning = false activeShardsLatch.openUnsafe() @@ -480,6 +555,7 @@ const make = Effect.gen(function*() { }) yield* Effect.gen(function*() { + yield* singletonsReady.await activeShardsLatch.openUnsafe() while (true) { @@ -494,6 +570,7 @@ const make = Effect.gen(function*() { MutableHashSet.remove(acquiredShards, shardId) MutableHashSet.add(releasingShards, shardId) } + updateShardGauges() if (MutableHashSet.size(releasingShards) > 0 || MutableHashSet.size(forceReleasingShards) > 0) { yield* Effect.forkIn(syncSingletons, shardingScope) @@ -555,13 +632,11 @@ const make = Effect.gen(function*() { } MutableHashSet.add(acquiredShards, shardId) } + updateShardGauges() if (acquired.length > 0) { yield* notifyShardLockWaiters yield* storageReadLatch.open yield* Effect.forkIn(syncSingletons, shardingScope) - - // update metrics - ClusterMetrics.shards.updateUnsafe(BigInt(MutableHashSet.size(acquiredShards)), Context.empty()) } // the shutdown handoff releases the shards acquired here without waiting yield* Effect.raceFirst(Effect.sleep(1000), handoffLatch.await) @@ -589,7 +664,7 @@ const make = Effect.gen(function*() { MutableHashSet.add(releasingShards, shardId) MutableHashSet.add(forceReleasingShards, shardId) } - ClusterMetrics.shards.updateUnsafe(BigInt(0), Context.empty()) + updateShardGauges() activeShardsLatch.openUnsafe() return Effect.gen(function*() { @@ -632,6 +707,7 @@ const make = Effect.gen(function*() { MutableHashSet.add(releasingShards, shardId) } } + updateShardGauges() if (MutableHashSet.size(releasingShards) > 0) { yield* activeShardsLatch.open } @@ -655,6 +731,7 @@ const make = Effect.gen(function*() { yield* Effect.suspend(() => shardLocksHealthyLatch.isOpen() ? refreshShardLocks : probeShardLocks).pipe( Effect.repeat(Schedule.fixed(shardLockInterval)), Effect.forever, + (effect) => Effect.andThen(singletonsReady.await, effect), Effect.forkIn(shardingScope) ) @@ -941,20 +1018,18 @@ const make = Effect.gen(function*() { } removableNotifications.clear() } - if (MutableHashSet.size(resetAddresses) > 0) { - for (const address of resetAddresses) { - yield* Effect.logWarning("Could not find entity manager for address, retrying").pipe( - Effect.annotateLogs({ address }) - ) - yield* Effect.forkIn(storage.resetAddress(address), shardingScope) - } - MutableHashSet.clear(resetAddresses) + for (const address of resetAddresses) { + yield* Effect.logWarning("Could not find entity manager for address, retrying").pipe( + Effect.annotateLogs({ address }) + ) } - // Capture claims skipped because of the entity cap. They are reset in - // one storage operation after releasing the read lock. - let cappedAddressesToReset: Array | undefined - if (MutableHashSet.size(cappedAddresses) > 0) { - cappedAddressesToReset = Arr.fromIterable(cappedAddresses) + // Capture claims for unregistered entity types, and claims skipped + // because of the entity cap. They are reset in one storage operation + // after releasing the read lock, and before the next read starts. + let addressesToReset: Array | undefined + if (MutableHashSet.size(resetAddresses) > 0 || MutableHashSet.size(cappedAddresses) > 0) { + addressesToReset = [...resetAddresses, ...cappedAddresses] + MutableHashSet.clear(resetAddresses) MutableHashSet.clear(cappedAddresses) } @@ -966,8 +1041,8 @@ const make = Effect.gen(function*() { resetRequestIds.length = 0 } - if (cappedAddressesToReset !== undefined) { - yield* Effect.ignore(storage.resetAddresses(cappedAddressesToReset)) + if (addressesToReset !== undefined) { + yield* Effect.ignore(storage.resetAddresses(addressesToReset)) } // A full batch means more messages could be waiting; start the next @@ -1796,6 +1871,8 @@ const make = Effect.gen(function*() { (stops) => Effect.all(stops, { discard: true }) ) + singletonsReady.openUnsafe() + // --- Entities --- const reaper = yield* EntityReaper @@ -1851,6 +1928,9 @@ const make = Effect.gen(function*() { yield* withStorageReadLock(Effect.sync(() => { entityRegistrationStartMillis ??= clock.currentTimeMillisUnsafe() entityManagers.set(entity.type, state) + // persisted messages read before this registration were reset, so + // read storage again instead of waiting for the next poll interval + storageReadLatch.openUnsafe() if (entityManagerLatches.has(entity.type)) { entityManagerLatches.get(entity.type)!.openUnsafe() entityManagerLatches.delete(entity.type) diff --git a/repos/effect/packages/effect/src/cluster/ShardingConfig.ts b/packages/effect/src/cluster/ShardingConfig.ts similarity index 96% rename from repos/effect/packages/effect/src/cluster/ShardingConfig.ts rename to packages/effect/src/cluster/ShardingConfig.ts index 4b8b8474c..8120e7df5 100644 --- a/repos/effect/packages/effect/src/cluster/ShardingConfig.ts +++ b/packages/effect/src/cluster/ShardingConfig.ts @@ -85,6 +85,8 @@ export class ShardingConfig extends Context.Service = Config.all({ Config.withDefault(defaults.entityMailboxCapacity) // Config.withDescription("The default capacity of the mailbox for entities.") ), + entityMailboxMetrics: Config.Boolean("entityMailboxMetrics").pipe( + Config.withDefault(defaults.entityMailboxMetrics) + // Config.withDescription("Report the mailbox size of each resident entity.") + ), maxResidentEntities: Config.schema( Schema.Int.check(Schema.isGreaterThan(0)), "maxResidentEntities" diff --git a/repos/effect/packages/effect/src/cluster/ShardingRegistrationEvent.ts b/packages/effect/src/cluster/ShardingRegistrationEvent.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/ShardingRegistrationEvent.ts rename to packages/effect/src/cluster/ShardingRegistrationEvent.ts diff --git a/repos/effect/packages/effect/src/cluster/SingleRunner.ts b/packages/effect/src/cluster/SingleRunner.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/SingleRunner.ts rename to packages/effect/src/cluster/SingleRunner.ts diff --git a/repos/effect/packages/effect/src/cluster/Singleton.ts b/packages/effect/src/cluster/Singleton.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/Singleton.ts rename to packages/effect/src/cluster/Singleton.ts diff --git a/repos/effect/packages/effect/src/cluster/SingletonAddress.ts b/packages/effect/src/cluster/SingletonAddress.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/SingletonAddress.ts rename to packages/effect/src/cluster/SingletonAddress.ts diff --git a/repos/effect/packages/effect/src/cluster/Snowflake.ts b/packages/effect/src/cluster/Snowflake.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/Snowflake.ts rename to packages/effect/src/cluster/Snowflake.ts diff --git a/repos/effect/packages/effect/src/cluster/SocketRunner.ts b/packages/effect/src/cluster/SocketRunner.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/SocketRunner.ts rename to packages/effect/src/cluster/SocketRunner.ts diff --git a/repos/effect/packages/effect/src/cluster/SqlMessageStorage.ts b/packages/effect/src/cluster/SqlMessageStorage.ts similarity index 93% rename from repos/effect/packages/effect/src/cluster/SqlMessageStorage.ts rename to packages/effect/src/cluster/SqlMessageStorage.ts index 74fc016a3..98dee0f73 100644 --- a/repos/effect/packages/effect/src/cluster/SqlMessageStorage.ts +++ b/packages/effect/src/cluster/SqlMessageStorage.ts @@ -277,13 +277,42 @@ const makeEncodedStorage = Effect.fnUntraced(function*( const sqlFalse = sql.literal(supportsBooleans ? "FALSE" : "0") const sqlTrue = sql.literal(supportsBooleans ? "TRUE" : "1") + // Read SQLite IDs as text to avoid rounding on number-returning drivers + // such as Durable Objects. + const idsAsText = sql.onDialectOrElse({ + sqlite: () => true, + orElse: () => false + }) + const idAs = (column: string, alias: string) => + idsAsText ? `CAST(${column} AS TEXT) AS ${alias}` : `${column} AS ${alias}` + const replyJoinColumns = "r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence" + const messageColumns = idsAsText + ? `${idAs("m.id", "id")}, m.message_id, m.shard_id, m.entity_type, m.entity_id, m.kind, m.tag, m.payload, ` + + `m.headers, m.trace_id, m.span_id, m.sampled, m.processed, ${idAs("m.request_id", "request_id")}, ` + + `${idAs("m.reply_id", "reply_id")}, ${idAs("m.last_reply_id", "last_reply_id")}, m.last_read, m.deliver_at` + : "m.*" + + const idColumn = sql.literal(idAs("id", "id")) + const replyColumns = sql.literal( + `${idAs("id", "id")}, kind, ${idAs("request_id", "request_id")}, payload, sequence` + ) + const duplicateColumns = sql.literal( + `${idAs("m.id", "id")}, ${idAs("r.id", "reply_id")}, ${replyJoinColumns}` + ) + const messageJoinColumns = sql.literal( + `${messageColumns}, ${idAs("r.id", "reply_reply_id")}, ${replyJoinColumns}` + ) + const selectByMessageId = (message_id: string): Effect.Effect, SqlError> => sql` - SELECT m.id, r.id as reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + SELECT ${duplicateColumns} FROM ${messagesTableSql} m LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id WHERE m.message_id = ${message_id} ` + // an insert that returned no rows hit an existing message_id + const selectUnlessInserted = (message_id: string) => (inserted: ReadonlyArray) => + inserted.length > 0 ? Effect.succeed([]) : selectByMessageId(message_id) const insertEnvelope: ( row: MessageRow, @@ -294,11 +323,7 @@ const makeEncodedStorage = Effect.fnUntraced(function*( INSERT INTO ${messagesTableSql} ${sql.insert(row)} ON CONFLICT (message_id) DO NOTHING RETURNING id - `.pipe(Effect.flatMap((rows) => { - // inserted a new row - if (rows.length > 0) return Effect.succeed([]) - return selectByMessageId(message_id) - })), + `.pipe(Effect.flatMap(selectUnlessInserted(message_id))), mysql: () => (row, message_id) => Effect.flatMap( sql`INSERT IGNORE INTO ${messagesTableSql} ${sql.insert(row)}`.raw, @@ -316,37 +341,8 @@ const makeEncodedStorage = Effect.fnUntraced(function*( ON target.message_id = source.message_id WHEN NOT MATCHED THEN INSERT ${sql.insert(row)} - OUTPUT - inserted.id, - CASE - WHEN inserted.id IS NULL THEN ( - SELECT r.id, r.kind, r.payload - FROM ${repliesTableSql} r - WHERE r.id = target.last_reply_id - ) - END as reply_id, - CASE - WHEN inserted.id IS NULL THEN ( - SELECT r.kind - FROM ${repliesTableSql} r - WHERE r.id = target.last_reply_id - ) - END as reply_kind, - CASE - WHEN inserted.id IS NULL THEN ( - SELECT r.payload - FROM ${repliesTableSql} r - WHERE r.id = target.last_reply_id - ) - END as reply_payload, - CASE - WHEN inserted.id IS NULL THEN ( - SELECT r.sequence - FROM ${repliesTableSql} r - WHERE r.id = target.last_reply_id - ) - END as reply_sequence; - `, + OUTPUT inserted.id; + `.pipe(Effect.flatMap(selectUnlessInserted(message_id))), orElse: () => (row, message_id) => selectByMessageId(message_id).pipe( Effect.tap(sql`INSERT OR IGNORE INTO ${messagesTableSql} ${sql.insert(row)}`), @@ -374,9 +370,15 @@ const makeEncodedStorage = Effect.fnUntraced(function*( orElse: () => (s: string) => `'${s}'` }) const forUpdate = sql.onDialectOrElse({ + mssql: () => sql.literal(""), sqlite: () => sql.literal(""), orElse: () => sql.literal("FOR UPDATE") }) + // mssql locks claimed rows with a table hint instead of FOR UPDATE + const updateLock = sql.onDialectOrElse({ + mssql: () => sql.literal("WITH (UPDLOCK)"), + orElse: () => sql.literal("") + }) const emptyFragment = sql.literal("") // mssql only limits with OFFSET/FETCH, which requires the ORDER BY that @@ -459,8 +461,8 @@ const makeEncodedStorage = Effect.fnUntraced(function*( `, orElse: () => (shardIds: ReadonlyArray, now: number, filters: UnprocessedFilters) => sql` - SELECT m.*, r.id as reply_reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence - FROM ${messagesTableSql} m + SELECT ${messageJoinColumns} + FROM ${messagesTableSql} m ${updateLock} LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id WHERE m.shard_id IN (${sql.literal(shardIds.map(wrapString).join(","))}) ${filters.addressFilter} @@ -630,11 +632,11 @@ const makeEncodedStorage = Effect.fnUntraced(function*( requestIdForPrimaryKey: (primaryKey) => messageIdForPrimaryKey(primaryKey).pipe( Effect.flatMap((messageId) => - sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${messageId}` + sql<{ id: string | bigint }>`SELECT ${idColumn} FROM ${messagesTableSql} WHERE message_id = ${messageId}` ), Effect.flatMap((rows) => rows.length === 0 && mayHaveLegacyRow(primaryKey) - ? sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${primaryKey}` + ? sql<{ id: string | bigint }>`SELECT ${idColumn} FROM ${messagesTableSql} WHERE message_id = ${primaryKey}` : Effect.succeed(rows) ), Effect.map((rows) => Option.map(Option.fromNullishOr(rows[0]?.id), Snowflake.Snowflake)), @@ -649,7 +651,7 @@ const makeEncodedStorage = Effect.fnUntraced(function*( // - the kind is WithExit // - or the kind is Chunk and has not been acked yet sql` - SELECT id, kind, request_id, payload, sequence + SELECT ${replyColumns} FROM ${repliesTableSql} WHERE request_id IN (${sql.literal(requestIds.join(","))}) AND ( @@ -669,7 +671,7 @@ const makeEncodedStorage = Effect.fnUntraced(function*( repliesForUnfiltered: (requestIds) => sql` - SELECT id, kind, request_id, payload, sequence + SELECT ${replyColumns} FROM ${repliesTableSql} WHERE request_id IN (${sql.literal(requestIds.join(","))}) ORDER BY rowid ASC @@ -705,7 +707,7 @@ const makeEncodedStorage = Effect.fnUntraced(function*( unprocessedMessagesById(ids, now) { const idArr = Array.from(ids, (id) => String(id)) return sql` - SELECT m.*, r.id as reply_reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence + SELECT ${messageJoinColumns} FROM ${messagesTableSql} m LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id WHERE m.id IN (${sql.literal(idArr.join(","))}) @@ -840,6 +842,8 @@ export const migrations = (options?: { const messagesTableSql = sql(messagesTable) const repliesTableSql = sql(repliesTable) + // mssql UNIQUE constraints treat NULLs as equal, so nullable keys use + // filtered unique indexes instead yield* sql.onDialectOrElse({ mssql: () => sql` @@ -853,8 +857,8 @@ export const migrations = (options?: { entity_id VARCHAR(255) NOT NULL, kind INT NOT NULL, tag VARCHAR(50), - payload TEXT, - headers TEXT, + payload NVARCHAR(MAX), + headers NVARCHAR(MAX), trace_id VARCHAR(32), span_id VARCHAR(16), sampled BIT, @@ -864,7 +868,7 @@ export const migrations = (options?: { last_reply_id BIGINT, last_read DATETIME, deliver_at BIGINT, - UNIQUE (message_id) + INDEX ${sql(messagesTable + "_message_id_idx")} UNIQUE (message_id) WHERE message_id IS NOT NULL ) `, mysql: () => @@ -967,17 +971,16 @@ export const migrations = (options?: { CREATE INDEX ${sql(requestIdLookupIndex)} ON ${messagesTableSql} (request_id); `.unprepared.pipe(Effect.ignore), - pg: () => - Effect.gen(function*() { - yield* sql` + pg: Effect.fnUntraced(function*() { + yield* sql` CREATE INDEX IF NOT EXISTS ${sql(shardLookupIndex)} ON ${messagesTableSql} (shard_id, processed, last_read, deliver_at) ` - yield* sql` + yield* sql` CREATE INDEX IF NOT EXISTS ${sql(requestIdLookupIndex)} ON ${messagesTableSql} (request_id) ` - }), + }), orElse: () => // sqlite Effect.all([ @@ -1001,11 +1004,11 @@ export const migrations = (options?: { rowid BIGINT IDENTITY(1,1), kind INT, request_id BIGINT NOT NULL, - payload TEXT NOT NULL, + payload NVARCHAR(MAX) NOT NULL, sequence INT, acked BIT NOT NULL DEFAULT 0, - CONSTRAINT ${sql(repliesTable + "_one_exit")} UNIQUE (request_id, kind), - CONSTRAINT ${sql(repliesTable + "_sequence")} UNIQUE (request_id, sequence) + INDEX ${sql(repliesTable + "_one_exit")} UNIQUE (request_id, kind) WHERE kind IS NOT NULL, + INDEX ${sql(repliesTable + "_sequence")} UNIQUE (request_id, sequence) WHERE sequence IS NOT NULL ) `, mysql: () => diff --git a/repos/effect/packages/effect/src/cluster/SqlRunnerStorage.ts b/packages/effect/src/cluster/SqlRunnerStorage.ts similarity index 98% rename from repos/effect/packages/effect/src/cluster/SqlRunnerStorage.ts rename to packages/effect/src/cluster/SqlRunnerStorage.ts index f5a807823..95fafaec8 100644 --- a/repos/effect/packages/effect/src/cluster/SqlRunnerStorage.ts +++ b/packages/effect/src/cluster/SqlRunnerStorage.ts @@ -6,6 +6,8 @@ * shard. This module migrates the required runner and lock tables, supports an * optional table prefix, uses advisory locks for PostgreSQL and MySQL when * enabled, and provides constructors and layers for the storage service. + * PGlite clients always use lock table rows on the shared client, so the + * storage never holds their only connection. * * `layer`, `layerWith`, and `make` run the migrations before building the * storage. To run migrations with a different connection, such as a schema @@ -53,9 +55,13 @@ const makeStorage = Effect.fnUntraced(function*(options: { const config = yield* ShardingConfig.ShardingConfig const shardGroups = ShardingConfig.shardGroupConfig(config) const availableShardGroups = Array.from(shardGroups.available) - const disableAdvisoryLocks = config.shardLockDisableAdvisory const lockOperationInterval = effectiveInterval(config) const sql = (yield* SqlClient.SqlClient).withoutTransforms() + // PGlite runs every statement on one session, so advisory locks are shared by + // every user of the client and a reserved connection would block all other + // statements. Use table leases on the shared client instead. + const singleSession = sql.driver === "pglite" + const disableAdvisoryLocks = config.shardLockDisableAdvisory || singleSession const layerScope = yield* Effect.scope const prefix = options?.prefix ?? "cluster" const table = (name: string) => `${prefix}_${name}` @@ -65,7 +71,7 @@ const makeStorage = Effect.fnUntraced(function*(options: { // Keep all PostgreSQL and MySQL shard-lock operations on a rebuildable // reserved connection, including when advisory locks are disabled. - const acquireLockConn = sql.onDialectOrElse({ + const acquireLockConn = singleSession ? undefined : sql.onDialectOrElse({ pg: () => Effect.fnUntraced(function*(scope: Scope.Scope) { const conn = yield* Effect.orDie(sql.reserve).pipe( @@ -667,8 +673,8 @@ const makeStorage = Effect.fnUntraced(function*(options: { * permission to create tables. When `prefix` is omitted, `make` uses the * `cluster` prefix, creating `cluster_runners`, `cluster_locks`, and the * `cluster_runner_migrations` history table. PostgreSQL and MySQL use advisory - * locks unless `ShardingConfig.shardLockDisableAdvisory` is enabled; other - * dialects use rows in the locks table. + * locks unless `ShardingConfig.shardLockDisableAdvisory` is enabled or the + * client is PGlite; other dialects and PGlite use rows in the locks table. * * **Gotchas** * diff --git a/repos/effect/packages/effect/src/cluster/TestRunner.ts b/packages/effect/src/cluster/TestRunner.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/TestRunner.ts rename to packages/effect/src/cluster/TestRunner.ts diff --git a/repos/effect/packages/effect/src/cluster/index.ts b/packages/effect/src/cluster/index.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/index.ts rename to packages/effect/src/cluster/index.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/clusterAbandon.ts b/packages/effect/src/cluster/internal/clusterAbandon.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/clusterAbandon.ts rename to packages/effect/src/cluster/internal/clusterAbandon.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/entityActivation.ts b/packages/effect/src/cluster/internal/entityActivation.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/entityActivation.ts rename to packages/effect/src/cluster/internal/entityActivation.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/entityManager.ts b/packages/effect/src/cluster/internal/entityManager.ts similarity index 91% rename from repos/effect/packages/effect/src/cluster/internal/entityManager.ts rename to packages/effect/src/cluster/internal/entityManager.ts index f36da9ae5..4b1df918d 100644 --- a/repos/effect/packages/effect/src/cluster/internal/entityManager.ts +++ b/packages/effect/src/cluster/internal/entityManager.ts @@ -8,7 +8,7 @@ import * as Effect from "../../Effect.ts" import * as Equal from "../../Equal.ts" import * as Exit from "../../Exit.ts" import * as Fiber from "../../Fiber.ts" -import { identity } from "../../Function.ts" +import { constVoid, identity } from "../../Function.ts" import { scopeAddFinalizerUnsafe, scopeRemoveFinalizerUnsafe } from "../../internal/effect.ts" import * as Latch from "../../Latch.ts" import * as Metric from "../../Metric.ts" @@ -67,6 +67,9 @@ export interface EntityManager { readonly interruptShard: (shardId: ShardId, options?: { readonly force?: boolean }) => Effect.Effect + // Whether any of the shard's entities are still open, including those that + // `interruptShard` is waiting on + readonly hasServersUnsafe: (shardId: ShardId) => boolean readonly activeEntityCount: Effect.Effect } @@ -137,6 +140,7 @@ export const make = Effect.fnUntraced(function*< readonly maxIdleTime?: Input | undefined readonly concurrency?: number | "unbounded" | undefined readonly mailboxCapacity?: number | "unbounded" | undefined + readonly mailboxMetrics?: boolean | undefined readonly disableFatalDefects?: boolean | undefined readonly defectRetryPolicy?: Schedule.Schedule | undefined readonly spanAttributes?: Record | undefined @@ -147,6 +151,7 @@ export const make = Effect.fnUntraced(function*< const managerScope = yield* Effect.scope const storageEnabled = options.storage !== MessageStorage.noop const mailboxCapacity = options.mailboxCapacity ?? config.entityMailboxCapacity + const mailboxMetrics = options.mailboxMetrics ?? config.entityMailboxMetrics const clock = yield* Clock const context = yield* Effect.context | Rpc.Middleware | RX>() const defectRetryPolicy = options.defectRetryPolicy @@ -155,6 +160,22 @@ export const make = Effect.fnUntraced(function*< const retryDriver = yield* Schedule.toStepWithSleep(defectRetryPolicy) const entityRpcs = new Map(entity.protocol.requests) + // The series reads the mailbox size on snapshot, so delivery does no extra work. + const registerMailboxSize = (address: EntityAddress, activeRequests: EntityState["activeRequests"]) => + Metric.registerUnsafe( + Metric.withAttributes(ClusterMetrics.entityMailboxSize, { + type: entity.type, + id: address.entityId, + shard: address.shardId.toString() + }), + context as Context.Context, + { + get: () => ({ value: BigInt(activeRequests.size) }), + update: constVoid, + modify: constVoid + } + ) + // add internal rpcs entityRpcs.set(KeepAliveRpc._tag, KeepAliveRpc as any) @@ -162,6 +183,34 @@ export const make = Effect.fnUntraced(function*< // Entities finishing their in-flight requests after removal, which still // accept interrupts and acks const drainingServers = new Map() + // Mailboxes of every resident entity, including draining ones. Unlike the + // server maps it is not keyed by entity id, which can be resident on several + // shards at once. + const residentMailboxes = new Set() + + // Registered before the entities resource map, so the series is removed + // only after every entity has drained on shutdown. + yield* Effect.acquireRelease( + Effect.sync(() => + Metric.registerUnsafe( + Metric.withAttributes(ClusterMetrics.entityMailboxSizeMax, { type: entity.type }), + context as Context.Context, + { + get: () => { + let max = 0 + for (const mailbox of residentMailboxes) { + if (mailbox.size > max) max = mailbox.size + } + return { value: BigInt(max) } + }, + update: constVoid, + modify: constVoid + } + ) + ), + (registration) => Effect.sync(registration.remove) + ) + const serverCloseLatches = new Map { releaseEntity(address) + mailboxSeries?.remove() + residentMailboxes.delete(activeRequests) }) ) @@ -213,7 +268,6 @@ export const make = Effect.fnUntraced(function*< } ) - const activeRequests: EntityState["activeRequests"] = new Map() const retired = Latch.makeUnsafe() const isActive = () => !retired.isOpen() @@ -488,6 +542,9 @@ export const make = Effect.fnUntraced(function*< yield* state.write(0, { _tag: "Eof" }) }).pipe( Effect.andThen(endLatch.await), + // A defect restart that finds the entity retired opens `endLatch` + // without building handlers, so the writes above may never land. + Effect.raceFirst(endLatch.await), Effect.timeoutOption(config.entityTerminationTimeout), Effect.interruptible ), @@ -505,6 +562,10 @@ export const make = Effect.fnUntraced(function*< // Do not make shard interruption wait for an entity that is still building. serverCloseLatches.set(address, closeLatches) activeServers.set(address.entityId, state) + residentMailboxes.add(activeRequests) + if (mailboxMetrics) { + mailboxSeries = registerMailboxSize(address, activeRequests) + } return state }, Effect.provideService(CurrentLogAnnotations, {}))) @@ -521,15 +582,18 @@ export const make = Effect.fnUntraced(function*< }) } - // update metrics for active servers - const typeAttributes = Metric.CurrentMetricAttributes.context({ type: entity.type }) - yield* Effect.sync(() => { - ClusterMetrics.entities.updateUnsafe(BigInt(activeServers.size), typeAttributes) - }).pipe( - Effect.andThen(Effect.sleep(1000)), - Effect.forever, - Effect.forkIn(managerScope) + // The series reads the active entity count on snapshot, so an idle runner + // does no work. + const entitiesSeries = Metric.registerUnsafe( + Metric.withAttributes(ClusterMetrics.entities, { type: entity.type }), + context as Context.Context, + { + get: () => ({ value: BigInt(activeServers.size) }), + update: constVoid, + modify: constVoid + } ) + yield* Scope.addFinalizer(managerScope, Effect.sync(entitiesSeries.remove)) function sendEnvelope(server: EntityState, message: Message.IncomingEnvelope): Effect.Effect { const entry = server.activeRequests.get(message.envelope.requestId) @@ -752,6 +816,12 @@ export const make = Effect.fnUntraced(function*< clearProcessed() { processedRequestIds.clear() }, + hasServersUnsafe(shardId) { + for (const address of serverCloseLatches.keys()) { + if (shardId[Equal.symbol](address.shardId)) return true + } + return false + }, isResidentUnsafe: (address) => entities.hasUnsafe(address), residentAddressesUnsafe: () => entities.keysUnsafe(), sendLocal, diff --git a/repos/effect/packages/effect/src/cluster/internal/entityReaper.ts b/packages/effect/src/cluster/internal/entityReaper.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/entityReaper.ts rename to packages/effect/src/cluster/internal/entityReaper.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/hash.ts b/packages/effect/src/cluster/internal/hash.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/hash.ts rename to packages/effect/src/cluster/internal/hash.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/interruptors.ts b/packages/effect/src/cluster/internal/interruptors.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/interruptors.ts rename to packages/effect/src/cluster/internal/interruptors.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/resourceMap.ts b/packages/effect/src/cluster/internal/resourceMap.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/resourceMap.ts rename to packages/effect/src/cluster/internal/resourceMap.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/resourceRef.ts b/packages/effect/src/cluster/internal/resourceRef.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/resourceRef.ts rename to packages/effect/src/cluster/internal/resourceRef.ts diff --git a/repos/effect/packages/effect/src/cluster/internal/shardLock.ts b/packages/effect/src/cluster/internal/shardLock.ts similarity index 100% rename from repos/effect/packages/effect/src/cluster/internal/shardLock.ts rename to packages/effect/src/cluster/internal/shardLock.ts diff --git a/repos/effect/packages/effect/src/devtools/DevTools.ts b/packages/effect/src/devtools/DevTools.ts similarity index 92% rename from repos/effect/packages/effect/src/devtools/DevTools.ts rename to packages/effect/src/devtools/DevTools.ts index 48434199e..8c6eaec00 100644 --- a/repos/effect/packages/effect/src/devtools/DevTools.ts +++ b/packages/effect/src/devtools/DevTools.ts @@ -56,8 +56,9 @@ export const layerWebSocket = ( * **Gotchas** * * This layer only installs the client-side tracer; it does not start a devtools - * server, so the configured WebSocket endpoint must already be reachable. It - * relies on `globalThis.WebSocket` being available in the runtime. + * server. The client retries failed connections every second, so the configured + * endpoint can become reachable after startup. It relies on + * `globalThis.WebSocket` being available in the runtime. * * @see {@link layerWebSocket} for installing the devtools tracer with an explicit `WebSocketConstructor` requirement * @see {@link layerSocket} for installing the devtools tracer over an existing `Socket` transport diff --git a/repos/effect/packages/effect/src/devtools/DevToolsClient.ts b/packages/effect/src/devtools/DevToolsClient.ts similarity index 84% rename from repos/effect/packages/effect/src/devtools/DevToolsClient.ts rename to packages/effect/src/devtools/DevToolsClient.ts index 4fd8879ee..eb42bde65 100644 --- a/repos/effect/packages/effect/src/devtools/DevToolsClient.ts +++ b/packages/effect/src/devtools/DevToolsClient.ts @@ -9,7 +9,6 @@ * @stability unstable * @since 4.0.0 */ -import * as Cause from "../Cause.ts" import * as Context from "../Context.ts" import * as Deferred from "../Deferred.ts" import * as Effect from "../Effect.ts" @@ -18,6 +17,7 @@ import * as Fiber from "../Fiber.ts" import * as Layer from "../Layer.ts" import * as Metric from "../Metric.ts" import * as Queue from "../Queue.ts" +import * as Schedule from "../Schedule.ts" import * as Schema from "../Schema.ts" import type * as Scope from "../Scope.ts" import * as Socket from "../socket/Socket.ts" @@ -49,7 +49,20 @@ const makeEffect = Effect.gen(function*() { const socket = yield* Socket.Socket const services = yield* Effect.context() const requests = yield* Queue.unbounded() + yield* Queue.offer(requests, { _tag: "Ping" }) const connected = yield* Deferred.make() + let closing = false + let writable = false + const setWritable = (value: boolean) => + Effect.sync(() => { + writable = value + }) + // Tracks whether the current connection has a writer that can drain the + // queue on shutdown. + const transport = Socket.make({ + reader: socket.reader, + writer: Effect.tap(socket.writer, Effect.acquireRelease(setWritable(true), () => setWritable(false))) + }) const offerMetricsSnapshot = Effect.sync(() => { Queue.offerUnsafe(requests, toMetricsSnapshot(services)) @@ -70,24 +83,28 @@ const makeEffect = Effect.gen(function*() { const fiber = yield* Stream.fromQueue(requests).pipe( Stream.pipeThroughChannel( - Ndjson.duplexSchemaString(Socket.toChannelString(socket), { + Ndjson.duplexSchemaString(Socket.toChannelString(transport), { inputSchema: RequestSchema, outputSchema: ResponseSchema }) ), Stream.onFirst(() => Deferred.completeWith(connected, Effect.void)), + // Each run closes its stream scope, including the socket reader and writer, + // before retrying with a fresh connection and the same request queue. Stream.runForEach(handleResponse), + Effect.tapError((error) => Effect.logDebug("DevTools connection failed", error)), + Effect.retry({ schedule: Schedule.spaced("1 second"), while: () => !closing }), Effect.forkDetach ) - yield* Effect.addFinalizer(() => - offerMetricsSnapshot.pipe( - Effect.andThen( - Effect.flatMap(Effect.fiberId, (id) => Queue.failCause(requests, Cause.interrupt(id))) - ), - Effect.andThen(Fiber.await(fiber)) - ) - ) + yield* Effect.addFinalizer(Effect.fnUntraced(function*() { + closing = true + yield* offerMetricsSnapshot + yield* Queue.interrupt(requests) + // A ready writer drains the queue before stopping. Failed or pending + // acquisition cannot observe the queue interruption, so stop it directly. + yield* writable ? Fiber.await(fiber) : Fiber.interrupt(fiber) + })) yield* Effect.suspend(() => Queue.offer(requests, { _tag: "Ping" })).pipe( Effect.delay("3 seconds"), diff --git a/repos/effect/packages/effect/src/devtools/DevToolsSchema.ts b/packages/effect/src/devtools/DevToolsSchema.ts similarity index 100% rename from repos/effect/packages/effect/src/devtools/DevToolsSchema.ts rename to packages/effect/src/devtools/DevToolsSchema.ts diff --git a/repos/effect/packages/effect/src/devtools/DevToolsServer.ts b/packages/effect/src/devtools/DevToolsServer.ts similarity index 100% rename from repos/effect/packages/effect/src/devtools/DevToolsServer.ts rename to packages/effect/src/devtools/DevToolsServer.ts diff --git a/repos/effect/packages/effect/src/devtools/index.ts b/packages/effect/src/devtools/index.ts similarity index 100% rename from repos/effect/packages/effect/src/devtools/index.ts rename to packages/effect/src/devtools/index.ts diff --git a/repos/effect/packages/effect/src/encoding/Base64.ts b/packages/effect/src/encoding/Base64.ts similarity index 100% rename from repos/effect/packages/effect/src/encoding/Base64.ts rename to packages/effect/src/encoding/Base64.ts diff --git a/repos/effect/packages/effect/src/encoding/Base64Url.ts b/packages/effect/src/encoding/Base64Url.ts similarity index 81% rename from repos/effect/packages/effect/src/encoding/Base64Url.ts rename to packages/effect/src/encoding/Base64Url.ts index 92486cb5f..31d87c176 100644 --- a/repos/effect/packages/effect/src/encoding/Base64Url.ts +++ b/packages/effect/src/encoding/Base64Url.ts @@ -32,6 +32,7 @@ import { EncodingError } from "./EncodingError.ts" * Base64Url.encode(bytes) // => "SGVsbG8_" * ``` * + * @see {@link encodePadded} for output that keeps `=` padding * @see {@link decode} for decoding URL-safe Base64 to bytes * @see {@link decodeString} for decoding URL-safe Base64 to UTF-8 text * @@ -42,6 +43,36 @@ import { EncodingError } from "./EncodingError.ts" export const encode: (input: Uint8Array | string) => string = (input) => Base64.encode(input).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") +/** + * Encodes the given value into a padded URL-safe Base64 string. + * + * **When to use** + * + * Use when a consumer requires `=` padding on URL-safe Base64, such as Go's + * `base64.URLEncoding`. + * + * **Details** + * + * Same as {@link encode}, but keeps the `=` padding. {@link decode} accepts + * both forms. + * + * **Example** (Encoding padded URL-safe Base64) + * + * ```ts import.meta.vitest + * import { Base64Url } from "effect/encoding" + * + * Base64Url.encodePadded("fo") // => "Zm8=" + * ``` + * + * @see {@link encode} for unpadded output + * + * @stability stable + * @category encoding + * @since 4.0.6 + */ +export const encodePadded: (input: Uint8Array | string) => string = (input) => + Base64.encode(input).replace(/\+/g, "-").replace(/\//g, "_") + /** * Decodes a padded or unpadded URL-safe Base64 string into bytes safely. * diff --git a/repos/effect/packages/effect/src/encoding/EncodingError.ts b/packages/effect/src/encoding/EncodingError.ts similarity index 100% rename from repos/effect/packages/effect/src/encoding/EncodingError.ts rename to packages/effect/src/encoding/EncodingError.ts diff --git a/repos/effect/packages/effect/src/encoding/Hex.ts b/packages/effect/src/encoding/Hex.ts similarity index 100% rename from repos/effect/packages/effect/src/encoding/Hex.ts rename to packages/effect/src/encoding/Hex.ts diff --git a/repos/effect/packages/effect/src/encoding/Ini.ts b/packages/effect/src/encoding/Ini.ts similarity index 99% rename from repos/effect/packages/effect/src/encoding/Ini.ts rename to packages/effect/src/encoding/Ini.ts index 235d35fc1..937e503a2 100644 --- a/repos/effect/packages/effect/src/encoding/Ini.ts +++ b/packages/effect/src/encoding/Ini.ts @@ -77,6 +77,9 @@ const unquote = (input: string | undefined): string => { output += character } } + if (escaped) { + output += "\\" + } return output.trim() } diff --git a/packages/effect/src/encoding/JsonArray.ts b/packages/effect/src/encoding/JsonArray.ts new file mode 100644 index 000000000..fc5e2dc3f --- /dev/null +++ b/packages/effect/src/encoding/JsonArray.ts @@ -0,0 +1,849 @@ +/** + * Encodes and decodes a single JSON array as a stream of its elements. + * + * **Details** + * + * Use `JsonArray` when a large response or file is one JSON document, such as + * `[...]` or `{"items":[...],"total":n}`, and its elements should be processed + * without buffering the whole document. Use `Ndjson` when the producer writes + * one JSON value per line. This module is unrelated to `Schema.JsonArray`, the + * type of a JSON array value. + * + * Decoders stream the elements of one array, the root array by default or the + * array selected by an RFC 6901 JSON Pointer. Each element is buffered only + * until it is complete and then parsed with `JSON.parse`. The rest of the + * document is validated and discarded. Encoders write a stream of values as + * one JSON array. + * + * **Example** (Streaming the items of a large HTTP response) + * + * ```ts + * import { Effect, Schema, Stream } from "effect" + * import { JsonArray } from "effect/encoding" + * import { HttpClient, HttpClientResponse } from "effect/http" + * + * const User = Schema.Struct({ id: Schema.Number, name: Schema.String }) + * + * // The response body is {"data":[{"id":1,"name":"Alice"},...],"total":...} + * const program = HttpClientResponse.stream(HttpClient.get("https://example.com/users")).pipe( + * Stream.pipeThroughChannel(JsonArray.decodeSchema(User)({ pointer: "/data" })), + * Stream.runForEach((user) => Effect.log(user.name)) + * ) + * ``` + * + * @stability unstable + * @since 4.0.6 + */ +import * as Arr from "../Array.ts" +import * as Cause from "../Cause.ts" +import * as Channel from "../Channel.ts" +import * as ChannelSchema from "../ChannelSchema.ts" +import * as Data from "../Data.ts" +import * as Effect from "../Effect.ts" +import * as JsonPointer from "../JsonPointer.ts" +import * as Predicate from "../Predicate.ts" +import * as Pull from "../Pull.ts" +import type * as Schema from "../Schema.ts" + +const JsonArrayErrorTypeId = "~effect/encoding/JsonArray/JsonArrayError" + +const encoder = new TextEncoder() + +/** + * Error raised when JSON array encoding or decoding fails. + * + * **Details** + * + * - `Syntax`: the input is not valid JSON, including an empty input or + * content after the document. + * - `Limit`: a value exceeds `maxDepth` or `maxElementSize`, whether it is an + * element, a key, or a value outside the selected array. + * - `Pointer`: the pointer target is missing or not an array, a value along the + * pointer is not an object or array, or an object along the pointer repeats + * the key that the pointer follows. + * - `Encode`: a value has no JSON representation. + * + * Decoding errors set `offset`, a zero-based offset in UTF-16 code units into + * the decoded text after any leading BOM. When a value is invalid or exceeds a + * limit, the offset is the start of that value. `Encode` errors have no + * offset. When `JSON.parse` or `JSON.stringify` throws, `Syntax` and `Encode` + * errors retain the original thrown value as `cause`. + * + * @stability unstable + * @category errors + * @since 4.0.6 + */ +export class JsonArrayError extends Data.TaggedError("JsonArrayError")<{ + readonly kind: "Syntax" | "Limit" | "Pointer" | "Encode" + readonly message: string + readonly offset?: number | undefined + readonly cause?: unknown +}> { + /** + * Marks this value as a JSON array encoding or decoding error for runtime + * guards. + * + * @stability unstable + * @since 4.0.6 + */ + readonly [JsonArrayErrorTypeId] = JsonArrayErrorTypeId +} + +/** + * Checks whether a value is a `JsonArrayError`. + * + * @stability unstable + * @category guards + * @since 4.0.6 + */ +export const isJsonArrayError = (u: unknown): u is JsonArrayError => Predicate.hasProperty(u, JsonArrayErrorTypeId) + +/** + * JSON array decoding options. + * + * **Details** + * + * - `pointer`: an RFC 6901 JSON Pointer to the array to stream, such as + * `"/data"`. Defaults to `""`, the root. Tokens select object members by + * key and array elements by index. + * - `maxDepth`: the maximum nesting of arrays and objects in one value, where + * an array or object element has depth 1. Defaults to 1000. + * - `maxElementSize`: the maximum length of one value in UTF-16 code units. + * Defaults to 16 MiB; `Infinity` disables the limit. + * + * Both limits also apply to values outside the selected array, which are + * buffered and validated before being discarded. Invalid options throw when + * the channel is created. + * + * @stability unstable + * @category configuration + * @since 4.0.6 + */ +export interface DecodeOptions { + readonly pointer?: string | undefined + readonly maxDepth?: number | undefined + readonly maxElementSize?: number | undefined +} + +interface Config { + readonly tokens: ReadonlyArray + // The array index selected by each token, or -1 when it is not an index. + readonly indices: ReadonlyArray + readonly maxDepth: number + readonly maxElementSize: number +} + +const makeConfig = (options: DecodeOptions | undefined): Config => { + const pointer = options?.pointer ?? "" + if (pointer !== "" && (!pointer.startsWith("/") || /~(?:[^01]|$)/.test(pointer))) { + throw new Error(`JsonArray: pointer must be an RFC 6901 JSON Pointer, got ${JSON.stringify(pointer)}`) + } + const maxDepth = options?.maxDepth ?? 1000 + if (!(Number.isInteger(maxDepth) && maxDepth >= 0)) { + throw new Error(`JsonArray: maxDepth must be a non-negative integer, got ${maxDepth}`) + } + const maxElementSize = options?.maxElementSize ?? 16 * 1024 * 1024 + if (!(maxElementSize >= 1)) { + throw new Error(`JsonArray: maxElementSize must be at least 1, got ${maxElementSize}`) + } + const tokens = pointer === "" ? [] : pointer.slice(1).split("/").map(JsonPointer.unescapeToken) + return { + tokens, + indices: tokens.map((token) => /^(?:0|[1-9]\d*)$/.test(token) ? Number(token) : -1), + maxDepth, + maxElementSize + } +} + +const TAB = 9 +const LF = 10 +const CR = 13 +const SPACE = 32 +const QUOTE = 34 +const COMMA = 44 +const COLON = 58 +const LBRACKET = 91 +const BACKSLASH = 92 +const RBRACKET = 93 +const LBRACE = 123 +const RBRACE = 125 + +const isWhitespace = (c: number): boolean => c === SPACE || c === LF || c === CR || c === TAB + +const indexOf = (text: string, search: string, i: number): number => { + const index = text.indexOf(search, i) + return index === -1 ? text.length : index +} + +const isValueStart = (c: number): boolean => + c === QUOTE || c === LBRACKET || c === LBRACE || c === 45 || (c >= 48 && c <= 57) || c === 102 || c === 110 || + c === 116 + +// The scanner walks the containers along the pointer and the selected array. +// Every other value is captured whole and parsed with `JSON.parse`: elements +// are emitted, keys are matched against the pointer, and the rest is +// discarded. + +// Structural states. +const VALUE = 0 // the value at the current pointer prefix +const OBJECT_START = 1 // after `{`: a key or `}` +const OBJECT_KEY = 2 // after `,`: a key +const OBJECT_COLON = 3 // after a key: `:` +const OBJECT_VALUE = 4 // after `:` for a key the pointer does not follow +const OBJECT_NEXT = 5 // after a member: `,` or `}` +const ARRAY_START = 6 // after `[`: a value or `]` +const ARRAY_VALUE = 7 // after `,`: a value +const ARRAY_NEXT = 8 // after an element: `,` or `]` +const END = 9 // after the document + +// Capture modes. +const NONE = 0 +const SKIP = 1 +const ELEMENT = 2 +const KEY = 3 + +interface Frame { + readonly object: boolean + // Whether this is the selected array. + readonly target: boolean + // Whether the pointer token for this container has been found. + found: boolean + index: number +} + +class Scanner { + readonly config: Config + state = VALUE + readonly frames: Array = [] + key = "" + started = false + // The offset of the start of the current chunk. + base = 0 + + // The value being captured, which may span chunks. + capture = NONE + start = 0 + buffer = "" + scalar = false + depth = 0 + inString = false + escape = false + + // Cached quote and backslash positions; the chunk length means not found. + quote = -1 + backslash = -1 + // Absolute end of a batch that failed to parse. The precise scan handles + // text before it, so the batch is not rescanned for every element. + slow = 0 + + constructor(config: Config) { + this.config = config + } + + fail(kind: "Syntax" | "Limit" | "Pointer", message: string, offset: number, cause?: unknown): JsonArrayError { + return new JsonArrayError({ kind, message, offset, cause }) + } + + pointerPrefix(length: number, ...rest: Array): string { + return JSON.stringify( + this.config.tokens.slice(0, length).concat(rest).map((token) => "/" + JsonPointer.escapeToken(token)).join("") + ) + } + + // Names the value being captured, for limit errors. + subject(): string { + if (this.capture === ELEMENT) return "Element" + const depth = this.frames.length - 1 + if (this.capture === KEY) return `Key in ${this.pointerPrefix(depth)}` + const frame = this.frames[depth] + const token = frame.object ? this.key : String(frame.index - 1) + return `Value outside the selected array at ${this.pointerPrefix(depth, token)}` + } + + feed(text: string, out: Array): void { + if (!this.started && text.length > 0) { + this.started = true + if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1) + } + this.quote = -1 + this.backslash = -1 + let i = this.capture === NONE ? 0 : this.scan(text, 0, out) + while (i < text.length) { + const c = text.charCodeAt(i) + i = isWhitespace(c) ? i + 1 : this.step(text, i, c, out) + } + if (this.capture !== NONE) { + this.buffer += text.slice(Math.max(this.start - this.base, 0)) + this.checkSize(this.buffer.length) + } + this.base += text.length + } + + end(): void { + if (this.state !== END || this.capture !== NONE) { + throw this.fail("Syntax", "Unexpected end of input", this.base) + } + } + + // Handles one structural character and returns the next index. + step(text: string, i: number, c: number, out: Array): number { + const frame = this.frames[this.frames.length - 1] + switch (this.state) { + case VALUE: { + const target = this.frames.length === this.config.tokens.length + if (c === LBRACKET || (c === LBRACE && !target)) { + this.frames.push({ object: c === LBRACE, target, found: false, index: 0 }) + this.state = c === LBRACE ? OBJECT_START : ARRAY_START + return i + 1 + } + if (isValueStart(c)) { + const expected = target ? "an array" : "an object or array" + throw this.fail( + "Pointer", + `Expected ${expected} at ${this.pointerPrefix(this.frames.length)}`, + this.base + i + ) + } + break + } + case OBJECT_START: + case OBJECT_KEY: { + if (c === QUOTE) { + this.state = OBJECT_COLON + return this.begin(KEY, text, i, out) + } + if (c === RBRACE && this.state === OBJECT_START) return this.close(i) + break + } + case OBJECT_COLON: { + if (c !== COLON) break + if (this.key === this.config.tokens[this.frames.length - 1]) { + if (frame.found) { + throw this.fail("Pointer", `Duplicate key ${JSON.stringify(this.key)} along the pointer`, this.base + i) + } + frame.found = true + this.state = VALUE + } else { + this.state = OBJECT_VALUE + } + return i + 1 + } + case OBJECT_VALUE: { + if (!isValueStart(c)) break + this.state = OBJECT_NEXT + return this.begin(SKIP, text, i, out) + } + case OBJECT_NEXT: + case ARRAY_NEXT: { + if (c === COMMA) { + this.state = this.state === OBJECT_NEXT ? OBJECT_KEY : ARRAY_VALUE + return i + 1 + } + if (c === (this.state === OBJECT_NEXT ? RBRACE : RBRACKET)) return this.close(i) + break + } + case ARRAY_START: + case ARRAY_VALUE: { + if (c === RBRACKET && this.state === ARRAY_START) return this.close(i) + if (!isValueStart(c)) break + if (frame.target) { + const next = this.scanElements(text, i, out) + if (next > i) return next + this.state = ARRAY_NEXT + return this.begin(ELEMENT, text, i, out) + } + this.state = ARRAY_NEXT + if (frame.index++ === this.config.indices[this.frames.length - 1]) { + frame.found = true + this.state = VALUE + return i + } + return this.begin(SKIP, text, i, out) + } + } + throw this.fail( + "Syntax", + this.state === END + ? "Unexpected content after the document" + : `Unexpected character ${JSON.stringify(String.fromCharCode(c))}`, + this.base + i + ) + } + + close(i: number): number { + const frame = this.frames.pop()! + if (!frame.target && !frame.found) { + throw this.fail("Pointer", `No value at ${this.pointerPrefix(this.frames.length + 1)}`, this.base + i) + } + const parent = this.frames[this.frames.length - 1] + this.state = parent === undefined ? END : parent.object ? OBJECT_NEXT : ARRAY_NEXT + return i + 1 + } + + // Starts capturing the value that begins at `i`. + begin(mode: number, text: string, i: number, out: Array): number { + const c = text.charCodeAt(i) + this.capture = mode + this.start = this.base + i + this.inString = c === QUOTE + this.escape = false + this.depth = c === LBRACKET || c === LBRACE ? 1 : 0 + this.scalar = !this.inString && this.depth === 0 + if (this.depth > this.config.maxDepth) this.failDepth() + return this.scan(text, i + 1, out) + } + + // Continues the captured value from `i`, completing it if it ends in this + // chunk. Returns the index after the value, or the chunk length. + scan(text: string, i: number, out: Array): number { + const end = this.scalar ? this.scanScalar(text, i) : this.scanNested(text, i) + if (end === -1) return text.length + const source = this.buffer + text.slice(Math.max(this.start - this.base, 0), end) + this.checkSize(source.length) + const mode = this.capture + this.capture = NONE + this.buffer = "" + let value: unknown + try { + value = JSON.parse(source) + } catch (cause) { + throw this.fail("Syntax", "Invalid JSON value", this.start, cause) + } + if (mode === ELEMENT) out.push(value) + else if (mode === KEY) this.key = value as string + return end + } + + // A scalar ends at the first delimiter, which `JSON.parse` then validates. + scanScalar(text: string, i: number): number { + for (; i < text.length; i++) { + const c = text.charCodeAt(i) + if (isWhitespace(c) || c === COMMA || c === RBRACKET || c === RBRACE) return i + } + return -1 + } + + // Tracks only string, escape and nesting state; `JSON.parse` validates the + // rest. Returns the index after the value, or -1. + scanNested(text: string, i: number): number { + if (this.inString) { + i = this.scanString(text, i, this.escape) + if (i === -1) return -1 + this.inString = false + if (this.depth === 0) return i + } + let depth = this.depth + while (i < text.length) { + const c = text.charCodeAt(i++) + if (c === QUOTE) { + i = this.scanString(text, i, false) + if (i === -1) { + this.inString = true + break + } + } else if (c === LBRACKET || c === LBRACE) { + if (++depth > this.config.maxDepth) this.failDepth() + } else if ((c === RBRACKET || c === RBRACE) && --depth === 0) { + this.depth = 0 + return i + } + } + this.depth = depth + return -1 + } + + // Scans string contents from `i`; returns the index after the closing quote, + // or -1 if the string continues into the next chunk, recording `escape` for + // `scanNested` to resume with. + scanString(text: string, i: number, escape: boolean): number { + let plain = 0 + while (i < text.length) { + const c = text.charCodeAt(i++) + if (escape) { + escape = false + } else if (c === BACKSLASH) { + escape = true + } else if (c === QUOTE) { + return i + } else if (++plain === 16) { + // A long string: jump to the next quote or backslash. + if (this.quote < i) this.quote = indexOf(text, "\"", i) + if (this.backslash < i) this.backslash = indexOf(text, "\\", i) + i = Math.min(this.quote, this.backslash) + plain = 0 + } + } + this.escape = escape + return -1 + } + + // Parses the elements from `i` up to the last comma or closing bracket at + // the array's level with one `JSON.parse`. Wrapped in brackets, that text + // parses only when every element and separator in it is valid. Returns `i` + // to leave anything else, including errors, to the precise scan. + scanElements(text: string, i: number, out: Array): number { + if (this.base + i < this.slow) return i + const { maxDepth, maxElementSize } = this.config + let cut = -1 + let start = i + let close = false + let depth = 0 + let j = i + while (j < text.length) { + const c = text.charCodeAt(j++) + if (c === QUOTE) { + j = this.scanString(text, j, false) + if (j === -1) break + } else if (c === LBRACKET || c === LBRACE) { + if (++depth > maxDepth) break + } else if (c === RBRACKET || c === RBRACE) { + if (depth-- > 0) continue + // Including surrounding whitespace makes this a conservative size check. + if (c === RBRACKET && j - 1 - start <= maxElementSize) { + cut = j - 1 + close = true + } + break + } else if (c === COMMA && depth === 0) { + if (j - 1 - start > maxElementSize) break + cut = j - 1 + start = j + } + } + let values: Array | undefined + if (cut !== -1) { + try { + values = JSON.parse("[" + text.slice(i, cut) + "]") + } catch { + this.slow = this.base + cut + } + } + // Rescanning may start before the cached matches, so discard them. + this.quote = -1 + this.backslash = -1 + if (values === undefined) return i + for (let k = 0; k < values.length; k++) out.push(values[k]) + if (close) return this.close(cut) + this.state = ARRAY_VALUE + return cut + 1 + } + + checkSize(size: number): void { + if (size > this.config.maxElementSize) { + throw this.fail("Limit", `${this.subject()} exceeds maxElementSize of ${this.config.maxElementSize}`, this.start) + } + } + + failDepth(): never { + throw this.fail("Limit", `${this.subject()} exceeds maxDepth of ${this.config.maxDepth}`, this.start) + } +} + +/** + * Creates a channel that parses the text of one JSON document and emits the + * elements of one of its arrays. + * + * **When to use** + * + * Use when a large JSON array, or an object that contains one, arrives as + * string chunks and its elements should be processed as they arrive. + * + * **Details** + * + * Each output chunk holds the elements completed by one input chunk. Elements + * may span input chunks and are buffered only until they are complete. A + * leading BOM is stripped. + * + * **Gotchas** + * + * - The document is validated as it streams, so elements from earlier + * successfully processed input chunks may already have been emitted when a + * later error fails the channel. Elements accumulated in the failing chunk + * are not emitted. A failure in content after the selected array signals + * that the document was invalid, but cannot guard against using earlier + * output. + * - The pointer follows the first occurrence of a key, where `JSON.parse` + * keeps the last. A repeated followed key fails with a `Pointer` error, as + * does a first occurrence that is not an object or array where the pointer + * needs one. When the repeat comes after the selected array, elements from + * earlier successfully processed chunks may already have been emitted. + * - A root that is not an array fails with a `Pointer` error unless the + * pointer selects an array inside it. + * + * **Example** (Streaming an array inside an envelope) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * import { JsonArray } from "effect/encoding" + * + * const items = Stream.make("{\"total\":2,\"items\":[{\"id\":1},{\"i", "d\":2}]}").pipe( + * Stream.pipeThroughChannel(JsonArray.decodeString({ pointer: "/items" })), + * Stream.runCollect + * ) + * await Effect.runPromise(items) // => [{ id: 1 }, { id: 2 }] + * ``` + * + * @see decode + * @see decodeSchemaString + * @stability unstable + * @category decoding + * @since 4.0.6 + */ +export const decodeString = (options?: DecodeOptions): Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | JsonArrayError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => { + const config = makeConfig(options) + return Channel.fromTransform((upstream, _scope) => + Effect.sync(() => { + const scanner = new Scanner(config) + let done: Pull.Pull | undefined + type Out = Pull.Pull, IE | JsonArrayError, Done> + + const emit = (run: (out: Array) => void): Out => { + const out: Array = [] + try { + run(out) + } catch (error) { + if (isJsonArrayError(error)) return Effect.fail(error) + throw error + } + return Arr.isArrayNonEmpty(out) ? Effect.succeed(out) : pull + } + + const pull: Out = Effect.suspend(() => { + if (done !== undefined) return done + return Pull.matchEffect(upstream, { + onSuccess: (texts) => + emit((out) => { + for (const text of texts) scanner.feed(text, out) + }), + onFailure: Effect.failCause, + onDone: (value) => { + done = Cause.done(value) + return emit(() => scanner.end()) + } + }) + }) + return pull + }) + ) +} + +/** + * Creates a channel that parses UTF-8 bytes of one JSON document and emits the + * elements of one of its arrays. + * + * **Details** + * + * Multi-byte characters may span input chunks, and invalid UTF-8 is replaced + * with U+FFFD. + * + * @see decodeString + * @stability unstable + * @category decoding + * @since 4.0.6 + */ +export const decode = (options?: DecodeOptions): Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | JsonArrayError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => Channel.pipeTo(Channel.decodeText("utf-8", { ignoreBOM: true }), decodeString(options)) + +/** + * Creates a channel that parses the text of one JSON document and decodes the + * elements of one of its arrays with a schema. + * + * @see decodeString + * @stability unstable + * @category decoding + * @since 4.0.6 + */ +export const decodeSchemaString = ( + schema: S +) => +(options?: DecodeOptions): Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | JsonArrayError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["DecodingServices"] +> => Channel.pipeTo(decodeString(options), ChannelSchema.decodeUnknown(schema)()) + +/** + * Creates a channel that parses UTF-8 bytes of one JSON document and decodes + * the elements of one of its arrays with a schema. + * + * @see decodeSchemaString + * @stability unstable + * @category decoding + * @since 4.0.6 + */ +export const decodeSchema = ( + schema: S +) => +(options?: DecodeOptions): Channel.Channel< + Arr.NonEmptyReadonlyArray, + Schema.SchemaError | JsonArrayError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["DecodingServices"] +> => Channel.pipeTo(decode(options), ChannelSchema.decodeUnknown(schema)()) + +const stringify = (value: unknown): string => { + let output: string | undefined + try { + output = JSON.stringify(value) + } catch (cause) { + throw new JsonArrayError({ kind: "Encode", message: cause instanceof Error ? cause.message : String(cause), cause }) + } + if (output === undefined) { + throw new JsonArrayError({ kind: "Encode", message: `Value of type ${typeof value} cannot be represented as JSON` }) + } + return output +} + +/** + * Creates a channel that writes a stream of values as the text of one JSON + * array. + * + * **Details** + * + * Each input chunk produces one string. An empty stream produces `"[]"`. + * Values are written with `JSON.stringify`. + * + * **Gotchas** + * + * A value that `JSON.stringify` cannot write, such as `undefined`, a function + * or a `bigint`, fails with an `Encode` error after the preceding text has + * been emitted. + * + * **Example** (Writing values as a JSON array) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * import { JsonArray } from "effect/encoding" + * + * const json = Stream.make({ id: 1 }, { id: 2 }).pipe( + * Stream.pipeThroughChannel(JsonArray.encodeString()), + * Stream.mkString + * ) + * await Effect.runPromise(json) // => '[{"id":1},{"id":2}]' + * ``` + * + * @see encode + * @see encodeSchemaString + * @stability unstable + * @category encoding + * @since 4.0.6 + */ +export const encodeString = (): Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | JsonArrayError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => + Channel.fromTransform((upstream, _scope) => + Effect.sync(() => { + let started = false + let done: Pull.Pull | undefined + type Out = Pull.Pull, IE | JsonArrayError, Done> + const pull: Out = Effect.suspend((): Out => { + if (done !== undefined) return done + return Pull.matchEffect(upstream, { + onSuccess: (values): Out => { + let text: string + try { + text = (started ? "," : "[") + values.map(stringify).join(",") + } catch (error) { + if (isJsonArrayError(error)) return Effect.fail(error) + throw error + } + started = true + return Effect.succeed(Arr.of(text)) + }, + onFailure: Effect.failCause, + onDone: (value) => { + done = Cause.done(value) + return Effect.succeed(Arr.of(started ? "]" : "[]")) + } + }) + }) + return pull + }) + ) + +/** + * Creates a channel that writes a stream of values as UTF-8 bytes of one JSON + * array. + * + * @see encodeString + * @stability unstable + * @category encoding + * @since 4.0.6 + */ +export const encode = (): Channel.Channel< + Arr.NonEmptyReadonlyArray, + IE | JsonArrayError, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => Channel.map(encodeString(), Arr.map((text) => encoder.encode(text))) + +/** + * Creates a channel that encodes values with a schema and writes them as the + * text of one JSON array. + * + * @see encodeString + * @stability unstable + * @category encoding + * @since 4.0.6 + */ +export const encodeSchemaString = ( + schema: S +) => +(): Channel.Channel< + Arr.NonEmptyReadonlyArray, + JsonArrayError | Schema.SchemaError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["EncodingServices"] +> => Channel.pipeTo(ChannelSchema.encode(schema)(), encodeString()) + +/** + * Creates a channel that encodes values with a schema and writes them as UTF-8 + * bytes of one JSON array. + * + * @see encodeSchemaString + * @stability unstable + * @category encoding + * @since 4.0.6 + */ +export const encodeSchema = ( + schema: S +) => +(): Channel.Channel< + Arr.NonEmptyReadonlyArray, + JsonArrayError | Schema.SchemaError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + S["EncodingServices"] +> => Channel.pipeTo(ChannelSchema.encode(schema)(), encode()) diff --git a/repos/effect/packages/effect/src/encoding/Ndjson.ts b/packages/effect/src/encoding/Ndjson.ts similarity index 66% rename from repos/effect/packages/effect/src/encoding/Ndjson.ts rename to packages/effect/src/encoding/Ndjson.ts index d692a5864..9e1c2ab8b 100644 --- a/repos/effect/packages/effect/src/encoding/Ndjson.ts +++ b/packages/effect/src/encoding/Ndjson.ts @@ -9,17 +9,20 @@ * @since 4.0.0 */ import * as Arr from "../Array.ts" +import * as Cause from "../Cause.ts" import * as Channel from "../Channel.ts" import * as ChannelSchema from "../ChannelSchema.ts" import * as Data from "../Data.ts" import * as Effect from "../Effect.ts" -import { dual, identity } from "../Function.ts" +import { dual } from "../Function.ts" import * as Predicate from "../Predicate.ts" +import * as Pull from "../Pull.ts" import type * as Schema from "../Schema.ts" const NdjsonErrorTypeId = "~effect/encoding/Ndjson/NdjsonError" const encoder = new TextEncoder() +const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }) /** * Error raised when NDJSON encoding or decoding fails. @@ -29,12 +32,18 @@ const encoder = new TextEncoder() * The `kind` field identifies whether the failure happened while packing or * unpacking, and `cause` preserves the original error. * + * Decoders also set `reason` to say why a line was rejected, and + * `unterminated` when that line is the final line of the input and has no + * line terminator. + * * @stability unstable * @category errors * @since 4.0.0 */ export class NdjsonError extends Data.TaggedError("NdjsonError")<{ readonly kind: "Pack" | "Unpack" + readonly reason?: NdjsonErrorReason | undefined + readonly unterminated?: boolean | undefined readonly cause: unknown }> { /** @@ -56,6 +65,21 @@ export class NdjsonError extends Data.TaggedError("NdjsonError")<{ } } +/** + * Why a decoder rejected a line. + * + * **Details** + * + * - `InvalidJson`: the line is not valid JSON. + * - `InvalidUtf8`: the line is not valid UTF-8 (byte decoders only). + * - `LineTooLarge`: the line is longer than `maxLineBytes` (byte decoders only). + * + * @stability unstable + * @category errors + * @since 4.0.6 + */ +export type NdjsonErrorReason = "InvalidJson" | "InvalidUtf8" | "LineTooLarge" + /** * Checks whether a value is an `NdjsonError`. * @@ -169,6 +193,59 @@ export const encodeSchemaString = ( S["EncodingServices"] > => Channel.pipeTo(ChannelSchema.encode(schema)(), encodeString()) +// Collects the values of one input chunk into `out`. A failure stops the +// chunk, and the values collected before it are still emitted. +type DecodeStep = (input: In, out: Array) => NdjsonError | undefined + +const makeDecodePull = ( + upstream: Pull.Pull, + step: DecodeStep, + flush: (out: Array) => NdjsonError | undefined +): Pull.Pull, IE | NdjsonError, Done> => { + let failure: NdjsonError | undefined + let done: { readonly leftover: Done } | undefined + + const emit = (out: Array, error: NdjsonError | undefined) => { + failure = error + if (Arr.isArrayNonEmpty(out)) return Effect.succeed(out) + return error !== undefined ? Effect.fail(error) : undefined + } + + const pull: Pull.Pull, IE | NdjsonError, Done> = Effect.suspend(() => { + if (failure !== undefined) return Effect.fail(failure) + if (done !== undefined) return Cause.done(done.leftover) + return Pull.matchEffect(upstream, { + onSuccess: (input) => { + const out: Array = [] + return emit(out, step(input, out)) ?? pull + }, + onFailure: Effect.failCause, + onDone: (leftover): Pull.Pull, NdjsonError, Done> => { + done = { leftover } + const out: Array = [] + return emit(out, flush(out)) ?? Cause.done(leftover) + } + }) + }) + return pull +} + +const parseLine = ( + line: string, + out: Array, + ignoreEmptyLines: boolean, + unterminated: boolean +): NdjsonError | undefined => { + if (ignoreEmptyLines && line.length === 0) return + try { + out.push(JSON.parse(line)) + } catch (cause) { + return new NdjsonError({ kind: "Unpack", reason: "InvalidJson", unterminated, cause }) + } +} + +const flushNothing = () => undefined + /** * Creates a channel that parses NDJSON string chunks into values. * @@ -179,7 +256,9 @@ export const encodeSchemaString = ( * * **Details** * - * Lines may span input chunks. + * Lines may span input chunks. `\n`, `\r\n` and a bare `\r` end a line. The + * first line that fails to parse fails the channel with + * `reason: "InvalidJson"`, after the lines before it have been emitted. * * **Gotchas** * @@ -199,28 +278,53 @@ export const decodeString = (options?: { Arr.NonEmptyReadonlyArray, IE, Done -> => { - const lines = Channel.splitLines().pipe( - options?.ignoreEmptyLines === true ? - Channel.filterArray((line) => line.length > 0) : - identity - ) - return Channel.mapEffect(lines, (chunk) => { - try { - return Effect.succeed(Arr.map(chunk, (line) => JSON.parse(line))) - } catch (cause) { - return Effect.fail(new NdjsonError({ kind: "Unpack", cause })) - } +> => + Channel.fromTransform((upstream, scope) => { + const ignoreEmptyLines = options?.ignoreEmptyLines === true + // splitLines only emits a line after its input has ended when that line + // has no terminator + let ended = false + const input = Pull.matchEffect(upstream, { + onSuccess: Effect.succeed, + onFailure: Effect.failCause, + onDone: (leftover) => { + ended = true + return Cause.done(leftover) + } + }) + return Effect.map( + Channel.toTransform(Channel.splitLines())(input, scope), + (lines) => + makeDecodePull(lines, (chunk, out) => { + for (const line of chunk) { + const error = parseLine(line, out, ignoreEmptyLines, ended) + if (error !== undefined) return error + } + }, flushNothing) + ) }) -} + +const LF = 0x0a +const CR = 0x0d +const emptyBytes = new Uint8Array(0) /** * Creates a channel that decodes UTF-8 byte chunks and parses them as NDJSON. * * **Details** * - * Lines may span input chunks, and `ignoreEmptyLines` controls whether blank - * lines are skipped before JSON parsing. + * Lines may span input chunks. `\n`, `\r\n` and a bare `\r` end a line, and a + * leading UTF-8 BOM is skipped. `ignoreEmptyLines` controls whether blank lines + * are skipped before JSON parsing. + * + * The first bad line fails the channel after the lines before it have been + * emitted. A line that is not valid UTF-8 fails with `reason: "InvalidUtf8"` + * and a line that is not valid JSON fails with `reason: "InvalidJson"`. + * + * `maxLineBytes` limits the UTF-8 bytes of a line, not counting its + * terminator. A longer line fails with `reason: "LineTooLarge"` as soon as the + * limit is passed, without reading the rest of the line. Lines are unbounded + * by default. * * @stability unstable * @category constructors @@ -228,6 +332,7 @@ export const decodeString = (options?: { */ export const decode = (options?: { readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined }): Channel.Channel< Arr.NonEmptyReadonlyArray, IE | NdjsonError, @@ -235,9 +340,88 @@ export const decode = (options?: { Arr.NonEmptyReadonlyArray, IE, Done -> => { - return Channel.pipeTo(Channel.decodeText(), decodeString(options)) -} +> => + Channel.fromTransform((upstream, _scope) => + Effect.sync(() => { + const ignoreEmptyLines = options?.ignoreEmptyLines === true + const maxLineBytes = options?.maxLineBytes ?? Number.POSITIVE_INFINITY + // The first decoder strips a leading BOM. `\n` and `\r` never occur + // inside a multi-byte character, so each line is decoded on its own. + let decoder = new TextDecoder("utf-8", { fatal: true }) + // Bytes of the current line from earlier chunks + let pending: Array = [] + let pendingBytes = 0 + // The previous byte was `\r`, so a `\n` right after it ends nothing + let afterCR = false + + const takeLine = (chunk: Uint8Array, start: number, end: number): Uint8Array => { + if (pendingBytes === 0) return chunk.subarray(start, end) + const line = new Uint8Array(pendingBytes + end - start) + let offset = 0 + for (const bytes of pending) { + line.set(bytes, offset) + offset += bytes.length + } + line.set(chunk.subarray(start, end), offset) + pending = [] + pendingBytes = 0 + return line + } + + const decodeLine = (bytes: Uint8Array, out: Array, unterminated: boolean) => { + let line: string + try { + line = decoder.decode(bytes) + } catch (cause) { + return new NdjsonError({ kind: "Unpack", reason: "InvalidUtf8", unterminated, cause }) + } + const firstLine = decoder !== utf8Decoder + decoder = utf8Decoder + // A stripped BOM alone is not an unterminated record. + if (firstLine && unterminated && line.length === 0) return + return parseLine(line, out, ignoreEmptyLines, unterminated) + } + + const step: DecodeStep> = (chunks, out) => { + for (const chunk of chunks) { + let start = 0 + for (let i = 0; i < chunk.length; i++) { + const byte = chunk[i] + if (byte === LF || byte === CR) { + if (byte === LF && afterCR) { + start = i + 1 + } else { + const error = decodeLine(takeLine(chunk, start, i), out, false) + if (error !== undefined) return error + start = i + 1 + } + afterCR = byte === CR + } else { + afterCR = false + if (pendingBytes + i + 1 - start > maxLineBytes) { + return new NdjsonError({ + kind: "Unpack", + reason: "LineTooLarge", + unterminated: false, + cause: new RangeError(`NDJSON line exceeds maxLineBytes of ${maxLineBytes}`) + }) + } + } + } + if (start < chunk.length) { + // Copy, so a reused upstream buffer cannot change the line + pending.push(chunk.slice(start)) + pendingBytes += chunk.length - start + } + } + } + + const flush = (out: Array) => + pendingBytes === 0 ? undefined : decodeLine(takeLine(emptyBytes, 0, 0), out, true) + + return makeDecodePull(upstream, step, flush) + }) + ) /** * Creates an NDJSON byte decoder channel for values of a schema. @@ -256,6 +440,7 @@ export const decodeSchema = ( ) => (options?: { readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined }): Channel.Channel< Arr.NonEmptyReadonlyArray, Schema.SchemaError | NdjsonError | IE, @@ -308,6 +493,7 @@ export const decodeSchemaString = ( export const duplex: { (options?: { readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined }): ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, @@ -339,6 +525,7 @@ export const duplex: { >, options?: { readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined } ): Channel.Channel< Arr.NonEmptyReadonlyArray, @@ -361,6 +548,7 @@ export const duplex: { >, options?: { readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined } ): Channel.Channel< Arr.NonEmptyReadonlyArray, @@ -478,6 +666,7 @@ export const duplexSchema: { readonly inputSchema: In readonly outputSchema: Out readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined } ): ( self: Channel.Channel< @@ -512,6 +701,7 @@ export const duplexSchema: { readonly inputSchema: In readonly outputSchema: Out readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined } ): Channel.Channel< Arr.NonEmptyReadonlyArray, @@ -536,6 +726,7 @@ export const duplexSchema: { readonly inputSchema: In readonly outputSchema: Out readonly ignoreEmptyLines?: boolean | undefined + readonly maxLineBytes?: number | undefined } ): Channel.Channel< Arr.NonEmptyReadonlyArray, diff --git a/repos/effect/packages/effect/src/encoding/SchemaBinary.ts b/packages/effect/src/encoding/SchemaBinary.ts similarity index 99% rename from repos/effect/packages/effect/src/encoding/SchemaBinary.ts rename to packages/effect/src/encoding/SchemaBinary.ts index a8cf71fb8..4e0eae5be 100644 --- a/repos/effect/packages/effect/src/encoding/SchemaBinary.ts +++ b/packages/effect/src/encoding/SchemaBinary.ts @@ -13,6 +13,7 @@ */ import * as Arr from "../Array.ts" import * as BigDecimal from "../BigDecimal.ts" +import * as Bytes from "../Bytes.ts" import * as Cause from "../Cause.ts" import * as Channel from "../Channel.ts" import * as Chunk from "../Chunk.ts" @@ -43,7 +44,7 @@ import * as SchemaTransformation from "../SchemaTransformation.ts" * * The default mode supports compatible schema evolution. `fingerprint: true` * uses positional layouts and an 8-byte layout hash for smaller frames, but - * requires peers to use the same schema definition. + * requires peers to use an identical encoded layout. * * @stability unstable * @category models @@ -92,6 +93,14 @@ const directFingerprintCodecCache = new WeakMap> * Encoded results are arena-backed views and may share a larger buffer. Use * `bytes.slice()` when independent ownership is required. * + * **Gotchas** + * + * Bytes carry the layout of the schema that wrote them. In the default mode, + * decode them with that schema or a compatible evolution of it; fingerprint + * mode requires an identical encoded layout. Most union codecs prefix values + * with a member selector, so they cannot read bytes written by a member's own + * codec. + * * **Example** (Deriving a binary codec) * * ```ts @@ -248,16 +257,7 @@ export function encodeManyUnknownSync( } function concatFrames(frames: ReadonlyArray>): Uint8Array { - if (frames.length === 1) return frames[0] - let length = 0 - for (const frame of frames) length += frame.length - const out = new Uint8Array(length) - let offset = 0 - for (const frame of frames) { - out.set(frame, offset) - offset += frame.length - } - return out + return frames.length === 1 ? frames[0] : Bytes.concat(frames) } /** @@ -1074,14 +1074,6 @@ function pushU64(out: Array, n: bigint) { } } -function compareBytes(a: Uint8Array, b: Uint8Array): number { - const len = Math.min(a.length, b.length) - for (let i = 0; i < len; i++) { - if (a[i] !== b[i]) return a[i] - b[i] - } - return a.length - b.length -} - class IssueError extends Error { readonly issue: SchemaIssue.Issue constructor(issue: SchemaIssue.Issue) { @@ -2050,7 +2042,7 @@ function sentinelSetHash(sentinels: ReadonlyArray): number { const bn = typeof b.key === "number" if (an !== bn) return an ? -1 : 1 if (an) return (a.key as number) - (b.key as number) - return compareBytes(utf8Encode.encode(a.key as string), utf8Encode.encode(b.key as string)) + return Bytes.Order(utf8Encode.encode(a.key as string), utf8Encode.encode(b.key as string)) }) const out: Array = [] for (const sentinel of sorted) { @@ -3171,7 +3163,7 @@ function extraPairs(ctx: EncodeContext, layout: StructLayout, obj: Record a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0) } else { for (const pair of pairs) pair[2] = utf8Encode.encode(pair[0]) - pairs.sort((a, b) => compareBytes(a[2]!, b[2]!)) + pairs.sort((a, b) => Bytes.Order(a[2]!, b[2]!)) } if (persistent) { for (const pair of pairs) pair[2] ??= utf8Encode.encode(pair[0]) diff --git a/repos/effect/packages/effect/src/encoding/Sse.ts b/packages/effect/src/encoding/Sse.ts similarity index 100% rename from repos/effect/packages/effect/src/encoding/Sse.ts rename to packages/effect/src/encoding/Sse.ts diff --git a/packages/effect/src/encoding/Tar.ts b/packages/effect/src/encoding/Tar.ts new file mode 100644 index 000000000..6ee58b732 --- /dev/null +++ b/packages/effect/src/encoding/Tar.ts @@ -0,0 +1,1417 @@ +/** + * Encodes and decodes tar archives as streams in Effect channels. + * + * `decode` streams entry bodies from the archive input; `encode` streams them + * back into archive bytes. Neither buffers an entire entry body. + * + * The decoder reads the formats produced by common tools: v7, USTAR, PAX + * extended headers, and GNU long names and base-256 numbers. The encoder writes + * USTAR, adding PAX extended headers for fields that do not fit or for custom + * metadata. It does not access the file system or compress archives. + * + * @stability unstable + * @since 4.0.5 + */ +import * as Arr from "../Array.ts" +import * as Cause from "../Cause.ts" +import * as Channel from "../Channel.ts" +import * as Data from "../Data.ts" +import * as Effect from "../Effect.ts" +import * as Exit from "../Exit.ts" +import * as Predicate from "../Predicate.ts" +import * as Pull from "../Pull.ts" +import * as Scope from "../Scope.ts" +import * as Semaphore from "../Semaphore.ts" +import * as Stream from "../Stream.ts" +import type * as Types from "../Types.ts" + +const TarErrorTypeId = "~effect/encoding/Tar/TarError" + +/** + * Error raised when a tar archive cannot be encoded or decoded. + * + * **Details** + * + * The `kind` field identifies the failure: + * + * - `InvalidHeader`: a header has a bad checksum, a malformed field, or a value + * that cannot be written. + * - `Truncated`: the input ended inside a header or an entry body. + * - `SizeMismatch`: an entry body produced a byte count other than its declared + * `size`. + * - `Unsupported`: the archive uses a feature this module does not read, such + * as GNU sparse files. + * - `BodyConsumed`: an entry body was read after the decoder had moved past it. + * + * @stability unstable + * @category errors + * @since 4.0.5 + */ +export class TarError extends Data.TaggedError("TarError")<{ + readonly kind: "InvalidHeader" | "Truncated" | "SizeMismatch" | "Unsupported" | "BodyConsumed" + readonly message: string +}> { + /** + * Marks this value as a tar error for runtime guards. + * + * @stability unstable + * @since 4.0.5 + */ + readonly [TarErrorTypeId] = TarErrorTypeId +} + +/** + * Checks whether a value is a `TarError`. + * + * @stability unstable + * @category guards + * @since 4.0.5 + */ +export const isTarError = (u: unknown): u is TarError => Predicate.hasProperty(u, TarErrorTypeId) + +/** + * Kind of filesystem object described by a tar entry. + * + * **Details** + * + * `Other` covers every typeflag without a dedicated kind, such as GNU volume + * labels; the raw flag is kept in `Header.typeflag`. + * + * @stability unstable + * @category models + * @since 4.0.5 + */ +export type EntryType = + | "File" + | "Directory" + | "Symlink" + | "Hardlink" + | "CharDevice" + | "BlockDevice" + | "Fifo" + | "Other" + +/** + * Metadata of a tar entry. + * + * **Details** + * + * `path` and `linkTarget` are archive paths; hard link targets name another + * entry of the same archive. `mode` holds the permission bits, including + * setuid, setgid and sticky; file type bits, as in a `stat` mode, are dropped + * when decoding and encoding. `pax` holds the PAX extended header records, + * including those of global headers, that are not reflected in the other + * fields, such as `SCHILY.xattr.*`, `atime` or the `comment` of `git archive`. + * `devMajor` and `devMinor` are set for device entries only. + * + * @stability unstable + * @category models + * @since 4.0.5 + */ +export interface Header { + readonly path: string + readonly type: EntryType + readonly typeflag: string + readonly size: number + readonly mode: number + readonly uid: number + readonly gid: number + readonly uname: string + readonly gname: string + readonly mtime: Date + readonly linkTarget?: string | undefined + readonly devMajor?: number | undefined + readonly devMinor?: number | undefined + readonly pax: Readonly> +} + +/** + * Tar entry: a header and a stream of exactly `header.size` body bytes. + * + * @stability unstable + * @category models + * @since 4.0.5 + */ +export interface Entry { + readonly header: Header + readonly body: Stream.Stream +} + +/** + * Options for the entry constructors. + * + * **Details** + * + * Unset fields default to mode `0o644` (`0o755` for directories, `0o777` for + * symlinks), uid and gid `0`, empty user and group names, an mtime at the Unix + * epoch, and no extra PAX records. The defaults make archives reproducible. + * + * @stability unstable + * @category models + * @since 4.0.5 + */ +export interface EntryOptions { + readonly mode?: number | undefined + readonly uid?: number | undefined + readonly gid?: number | undefined + readonly uname?: string | undefined + readonly gname?: string | undefined + readonly mtime?: Date | undefined + readonly pax?: Readonly> | undefined +} + +const makeHeader = ( + path: string, + type: "File" | "Directory" | "Symlink" | "Hardlink", + size: number, + options: EntryOptions | undefined, + linkTarget?: string +): Header => ({ + path, + type, + typeflag: typeflags[type], + size, + mode: options?.mode ?? (type === "Directory" ? 0o755 : type === "Symlink" ? 0o777 : 0o644), + uid: options?.uid ?? 0, + gid: options?.gid ?? 0, + uname: options?.uname ?? "", + gname: options?.gname ?? "", + mtime: options?.mtime ?? new Date(0), + ...(linkTarget === undefined || linkTarget === "" ? undefined : { linkTarget }), + pax: options?.pax ?? {} +}) + +/** + * Creates a regular file entry from in-memory content. + * + * **Details** + * + * String content is encoded as UTF-8, and `size` is the byte length of the + * content. + * + * @see {@link fileStream} for content that is streamed instead of held in memory + * @see {@link encode} for writing entries to an archive + * + * @stability unstable + * @category constructors + * @since 4.0.5 + */ +export const file = (path: string, content: string | Uint8Array, options?: EntryOptions): Entry => { + const bytes = typeof content === "string" ? textEncoder.encode(content) : content + const header = makeHeader(path, "File", bytes.length, options) + if (bytes.length === 0) return { header, body: Stream.empty } + const body = Stream.succeed(bytes) + inMemoryBodies.set(body, bytes) + return { header, body } +} + +// Key by body identity so replacing an entry body bypasses the encode fast path. +const inMemoryBodies = new WeakMap, Uint8Array>() + +/** + * Creates a regular file entry whose content is streamed from `body`. + * + * **When to use** + * + * Use when the content is too large to hold in memory, or is read from a file + * or another stream, and its byte length is known up front. + * + * **Details** + * + * The defaults match `file`: mode `0o644`, uid and gid `0`, and an mtime at the + * Unix epoch. `body` is not run until the entry is encoded. + * + * **Gotchas** + * + * `size` must be the exact byte length of `body`: `encode` fails with a + * `SizeMismatch` `TarError` when the body produces fewer or more bytes. + * + * @see {@link file} for in-memory content + * @see {@link encode} for writing entries to an archive + * + * @stability unstable + * @category constructors + * @since 4.0.5 + */ +export const fileStream = ( + path: string, + size: number, + body: Stream.Stream, + options?: EntryOptions +): Entry => ({ + header: makeHeader(path, "File", size, options), + body +}) + +/** + * Creates a directory entry. + * + * **Details** + * + * A trailing `/` is added to a non-empty `path` when missing, as tar tools + * expect for directories. + * + * @stability unstable + * @category constructors + * @since 4.0.5 + */ +export const directory = (path: string, options?: EntryOptions): Entry => ({ + // An empty path stays empty, so that `encode` rejects it + header: makeHeader(path === "" || path.endsWith("/") ? path : `${path}/`, "Directory", 0, options), + body: Stream.empty +}) + +/** + * Creates a symbolic link entry pointing at `target`. + * + * @stability unstable + * @category constructors + * @since 4.0.5 + */ +export const symlink = (path: string, target: string, options?: EntryOptions): Entry => ({ + header: makeHeader(path, "Symlink", 0, options, target), + body: Stream.empty +}) + +/** + * Creates a hard link entry pointing at another entry of the archive. + * + * **Details** + * + * `target` is the archive path of an entry written earlier in the same + * archive. + * + * @stability unstable + * @category constructors + * @since 4.0.5 + */ +export const hardlink = (path: string, target: string, options?: EntryOptions): Entry => ({ + header: makeHeader(path, "Hardlink", 0, options, target), + body: Stream.empty +}) + +/** + * Creates a channel that decodes tar archive bytes into entries. + * + * **Details** + * + * Each output chunk holds one entry, and each entry body streams exactly + * `header.size` bytes straight from the input. Pulling the next entry skips + * whatever is left of the current body, like Go's `tar.Reader`. The decoder + * reads v7, USTAR, PAX extended headers (applying `path`, `linkpath`, `size`, + * `uid`, `gid`, `uname`, `gname` and `mtime`), GNU long names and links, and + * base-256 numbers. The records of a PAX global header apply to every entry + * that follows, except `path`, `linkpath` and `size`, which describe a single + * entry. Anything after the end-of-archive marker is ignored. + * + * **Gotchas** + * + * - Read each body before pulling the next entry: a body read afterwards fails + * with a `BodyConsumed` `TarError`. Operators that pull ahead, such as + * `Stream.buffer`, therefore can't sit between `decode` and the body reads. + * - Read bodies while the entry stream runs, for example in `Stream.mapEffect`: + * once it has ended, as after `Stream.runCollect`, `Stream.runHead` or + * `Stream.take`, every body fails with a `BodyConsumed` `TarError`. + * - A body reads the entry's bytes once: running it again continues where the + * previous run stopped. + * - Directories, links, devices and FIFOs have no body, and their `size` is + * reported as `0`. + * - Paths and link targets are returned as written in the archive, so they can + * be absolute or contain `..`, and a symlink entry can redirect a later entry + * outside the target directory. Validate both before writing entries to disk. + * - Names that are not valid UTF-8 are decoded with replacement characters. + * - PAX extended headers and GNU long names of more than 1 MiB fail with an + * `InvalidHeader` `TarError`. + * - The channel completes only when its input ends: input after the + * end-of-archive marker is read and discarded, so that the upstream done + * value can be passed on. + * - GNU sparse entries fail with an `Unsupported` `TarError`. + * + * @see {@link encode} for the inverse operation + * + * @stability unstable + * @category decoding + * @since 4.0.5 + */ +export const decode = (): Channel.Channel< + Arr.NonEmptyReadonlyArray>, + TarError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => + Channel.fromTransform((upstream, scope) => + Effect.suspend(() => { + const decoder = new Decoder() + let done: { readonly value: Done } | undefined + let pulls = 0 + + const pullUpstream = upstream.pipe( + Effect.map((chunks) => { + pulls++ + for (const chunk of chunks) decoder.push(chunk) + }), + Pull.catchDone((value) => { + pulls++ + done = { value: value as Done } + decoder.end() + return Effect.void + }) + ) + + // Serialize entry and body pulls; reuse input pulled while waiting for the lock. + const lock = Semaphore.makeUnsafe(1) + const pullInput = (seen: number) => + lock.withPermit(Effect.suspend(() => pulls === seen ? pullUpstream : Effect.void)) + + const run = (f: () => A | undefined | typeof NeedInput, end: () => D): Pull.Pull => + Effect.suspend(function loop(): Pull.Pull { + const seen = pulls + let result: A | undefined | typeof NeedInput + try { + result = f() + } catch (error) { + return isTarError(error) ? Effect.fail(error) : Effect.die(error) + } + if (result === NeedInput) return Effect.flatMap(pullInput(seen), loop) + return result === undefined ? Cause.done(end()) : Effect.succeed(result) + }) + + const body = (id: number): Stream.Stream => + Stream.fromPull(Effect.succeed(run(() => decoder.read(id), () => undefined))) + + const next = run(() => { + const header = decoder.next() + return header === NeedInput || header === undefined + ? header + : Arr.of({ header, body: body(decoder.generation) }) + }, () => done!.value) + + return Effect.as(Scope.addFinalizer(scope, Effect.sync(() => decoder.close())), next) + }) + ) + +/** + * Creates a channel that encodes entries into tar archive bytes. + * + * **Details** + * + * Each entry is written as a USTAR header, with the path split into the USTAR + * prefix and name fields when it is longer than 100 bytes. A PAX extended + * header is added only for what USTAR cannot hold: a long or non-ASCII path, + * link target, user or group name, a size of 8 GiB or more, a uid or gid above + * `2097151`, an mtime outside the USTAR range, or custom `pax` records. The + * archive ends with two zero blocks. + * + * Headers, padding and body chunks below 16 KiB are gathered into output chunks + * of up to 64 KiB, and larger body chunks pass through without copying, so + * bodies stream without being held in memory. Output is handed out before + * waiting for more entries, but up to 64 KiB of it can be held while a body is + * pulled. + * + * **Gotchas** + * + * - A body that produces any byte count other than `header.size` fails with a + * `SizeMismatch` `TarError`, so the size must be known before the body is + * written. + * - `header.pax` may contain custom metadata such as `SCHILY.xattr.*`. Keys + * `path`, `linkpath`, `size`, `uid`, `gid`, `uname`, `gname` and `mtime` must + * be set through the corresponding header fields. These keys and + * `GNU.sparse.*` records fail with an `InvalidHeader` `TarError`. + * - A `File` entry whose path ends with `/` fails with an `InvalidHeader` + * `TarError`, because other tools read it as a directory. + * - Entries other than `File` and `Other` must have `size` 0, and `devMajor` + * and `devMinor` must not exceed `2097151`; otherwise encoding fails with an + * `InvalidHeader` `TarError`. + * - mtime is rounded down to whole seconds. + * - `typeflag` is only written for `Other` entries; every other type writes its + * standard flag. + * + * @see {@link decode} for the inverse operation + * @see {@link file} for building entries from in-memory content + * @see {@link fileStream} for building entries from streamed content + * + * @stability unstable + * @category encoding + * @since 4.0.5 + */ +export const encode = = Entry>(): Channel.Channel< + Arr.NonEmptyReadonlyArray, + TarError | IE | (In extends Entry ? E : never), + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + In extends Entry ? R : never +> => { + // Distribute over entry unions to infer body errors and services. + type E = In extends Entry ? E : never + type R = In extends Entry ? R : never + return Channel.fromTransform((upstream: Pull.Pull, IE, Done>, scope) => + Effect.sync(() => { + const encoder = new Encoder() + let entries: ReadonlyArray> = [] + let index = 0 + let done: { readonly value: Done } | undefined + let body: Pull.Pull, E, void, R> | undefined + let bodyScope: Scope.Closeable | undefined + + const write = (f: () => void): Effect.Effect => { + try { + f() + } catch (error) { + return isTarError(error) ? Effect.fail(error) : Effect.die(error) + } + return Effect.void + } + + return Effect.gen(function*() { + while (true) { + if (encoder.buffered >= SLAB_SIZE) return encoder.flush() + if (body !== undefined) { + const chunks = yield* Pull.catchDone(body, () => Effect.undefined) + if (chunks !== undefined) { + yield* write(() => { + for (const chunk of chunks) encoder.body(chunk) + }) + continue + } + body = undefined + yield* Scope.close(bodyScope!, Exit.void) + yield* write(() => encoder.end()) + continue + } + if (index < entries.length) { + const entry = entries[index++] + // Directories, links and in-memory files have nothing to stream + const bytes = entry.body === Stream.empty ? emptyBody : inMemoryBodies.get(entry.body) + yield* write(() => { + encoder.begin(entry.header) + if (bytes !== undefined) { + encoder.body(bytes) + encoder.end() + } + }) + if (bytes !== undefined) continue + bodyScope = Scope.forkUnsafe(scope) + body = yield* Channel.toPullScoped(entry.body.channel, bodyScope) + continue + } + if (encoder.buffered > 0) return encoder.flush() + if (done !== undefined) return yield* Cause.done(done.value) + const next = yield* Pull.catchDone(upstream, (value) => { + done = { value: value as Done } + encoder.finish() + return Effect.undefined + }) + if (next !== undefined) { + entries = next + index = 0 + } + } + }) + }) + ) +} + +const invalidHeader = (message: string) => new TarError({ kind: "InvalidHeader", message }) + +const truncated = (at: number) => + new TarError({ kind: "Truncated", message: `unexpected end of tar archive at byte ${at}` }) + +const unsupported = (message: string) => new TarError({ kind: "Unsupported", message }) + +const sizeMismatch = (header: Header, written: number) => + new TarError({ + kind: "SizeMismatch", + message: `body of ${JSON.stringify(header.path)} has ${ + written > header.size ? "at least" : "only" + } ${written} bytes, but its size is ${header.size}` + }) + +const BLOCK = 512 +const PERMISSION_BITS = 0o7777 + +const padding = (size: number): number => (BLOCK - (size % BLOCK)) % BLOCK + +const isZeroBlock = (block: Uint8Array): boolean => { + for (let i = 0; i < BLOCK; i++) { + if (block[i] !== 0) return false + } + return true +} + +const sumBlock = (block: Uint8Array): number => { + // Four sums let the loop run faster than one + let a = 0 + let b = 0 + let c = 0 + let d = 0 + for (let i = 0; i < BLOCK; i += 4) { + a += block[i] + b += block[i + 1] + c += block[i + 2] + d += block[i + 3] + } + return a + b + c + d +} + +const USTAR_MAGIC = [0x75, 0x73, 0x74, 0x61, 0x72, 0x00] // "ustar\0" +const GNU_MAGIC = [0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00] // "ustar \0" +const STAR_TRAILER = [0x74, 0x61, 0x72, 0x00] // "tar\0" +const USTAR_MAGIC_VERSION = new Uint8Array([0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30, 0x30]) // "ustar\0" "00" + +const hasBytes = (block: Uint8Array, offset: number, bytes: ReadonlyArray): boolean => { + for (let i = 0; i < bytes.length; i++) { + if (block[offset + i] !== bytes[i]) return false + } + return true +} + +const typeflags: Record, string> = { + File: "0", + Hardlink: "1", + Symlink: "2", + CharDevice: "3", + BlockDevice: "4", + Directory: "5", + Fifo: "6" +} + +const types: Record = { + ...Object.fromEntries(Object.entries(typeflags).map(([type, typeflag]) => [typeflag, type])), + // Contiguous files + "7": "File" +} + +const typeOf = (typeflag: string, path: string): EntryType => { + // Legacy archives mark directories with a trailing slash only + if (typeflag === "\0") return path.endsWith("/") ? "Directory" : "File" + return types[typeflag] ?? "Other" +} + +const typeflagOf = (header: Header): string => header.type === "Other" ? header.typeflag : typeflags[header.type] + +// Entries of these types never carry body bytes, whatever their size field says +const isHeaderOnly = (type: EntryType): boolean => type !== "File" && type !== "Other" + +const textEncoder = new TextEncoder() +// Keeps a leading byte order mark as part of the decoded field +const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }) + +const isAscii = (s: string): boolean => { + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i) + if (c === 0 || c >= 0x80) return false + } + return true +} + +// Keeps the ASCII characters of a value that is also stored in a PAX record +const toAscii = (s: string): string => { + let out = "" + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i) + if (c > 0 && c < 0x80) out += s[i] + } + return out +} + +const fits = (value: string, length: number): boolean => value.length <= length && isAscii(value) + +const paxStringKeys = new Set(["path", "linkpath", "uname", "gname"]) +const paxHeaderKeys = new Set(["path", "linkpath", "size", "uid", "gid", "uname", "gname", "mtime"]) + +const noPax: Readonly> = Object.freeze({}) + +// Parses records of the form " =\n", where the length counts +// the bytes of the whole record +const parsePax = (data: Uint8Array): Map => { + const records = new Map() + let offset = 0 + while (offset < data.length) { + let space = offset + while (space < data.length && space - offset < 16 && data[space] !== 0x20) space++ + const lengthField = String.fromCharCode.apply(null, data.subarray(offset, space) as unknown as Array) + const length = /^\d+$/.test(lengthField) ? Number(lengthField) : Number.NaN + const end = offset + length + if (!(length >= 5) || end > data.length || data[space] !== 0x20 || space + 1 >= end || data[end - 1] !== 0x0a) { + throw invalidHeader("malformed PAX record") + } + const record = data.subarray(space + 1, end - 1) + const equals = record.indexOf(0x3d) + if (equals <= 0) { + throw invalidHeader("malformed PAX record") + } + const key = textDecoder.decode(record.subarray(0, equals)) + const value = textDecoder.decode(record.subarray(equals + 1)) + if (key.includes("\0") || (paxStringKeys.has(key) && value.includes("\0"))) { + throw invalidHeader(`invalid PAX record ${key}`) + } + records.set(key, value) + offset = end + } + return records +} + +const paxInteger = (key: string, value: string): number => { + const n = /^\d+$/.test(value) ? Number(value) : Number.NaN + if (!Number.isSafeInteger(n)) { + throw invalidHeader(`invalid PAX ${key} ${JSON.stringify(value)}`) + } + return n +} + +// Parses a PAX time of decimal seconds into milliseconds, rounded down. The +// digits are added up separately, because `Number(value) * 1000` is inexact. +const paxMillis = (value: string): number => { + const match = /^(-?)(\d+)(?:\.(\d+))?$/.exec(value) + if (match === null) { + throw invalidHeader(`invalid PAX mtime ${JSON.stringify(value)}`) + } + const [, sign, seconds, fraction = ""] = match + const millis = Number(seconds) * 1000 + Number(fraction.slice(0, 3).padEnd(3, "0")) + if (sign === "") return millis + // A negative time with digits below a millisecond rounds down past them + return -millis - (/[1-9]/.test(fraction.slice(3)) ? 1 : 0) +} + +// Applies the records that override header fields and returns the rest. An +// empty value keeps the field from the header block. +const applyPax = (header: RawHeader, records: Map): Record => { + const rest: Record = {} + for (const [key, value] of records) { + if (key.startsWith("GNU.sparse.")) { + throw unsupported("GNU sparse files are not supported") + } + if (!paxHeaderKeys.has(key)) { + // Defined rather than assigned, so that a `__proto__` key stays a record + Object.defineProperty(rest, key, { value, enumerable: true, writable: true, configurable: true }) + continue + } + if (value === "") continue + switch (key) { + case "path": + header.path = value + break + case "linkpath": + header.linkTarget = value + break + case "uname": + case "gname": + header[key] = value + break + case "uid": + case "gid": + case "size": + header[key] = paxInteger(key, value) + break + case "mtime": + header.mtimeMillis = paxMillis(value) + } + } + return rest +} + +const paxRecord = (key: string, value: string): Uint8Array => { + const rest = textEncoder.encode(` ${key}=${value}\n`) + // The length counts its own digits + const length = rest.length + String(rest.length + String(rest.length).length).length + const out = new Uint8Array(length) + writeString(out, 0, length, String(length)) + out.set(rest, length - rest.length) + return out +} + +const paxHeaderName = (path: string): string => { + const trimmed = path.replace(/\/+$/, "") + const i = trimmed.lastIndexOf("/") + const name = i === -1 ? `PaxHeaders.0/${trimmed}` : `${trimmed.slice(0, i)}/PaxHeaders.0/${trimmed.slice(i + 1)}` + return toAscii(name).slice(0, 100) +} + +const readString = (block: Uint8Array, offset: number, length: number): string => { + let end = offset + let ascii = true + const limit = offset + length + while (end < limit && block[end] !== 0) { + if (block[end] >= 0x80) ascii = false + end++ + } + if (end === offset) return "" + if (!ascii || end - offset > 256) return textDecoder.decode(block.subarray(offset, end)) + // Most header strings are short ASCII, which is cheaper to build directly + return String.fromCharCode.apply(null, block.subarray(offset, end) as unknown as Array) +} + +const readOctal = (block: Uint8Array, offset: number, length: number): number => { + let start = offset + let end = offset + length + while (start < end && (block[start] === 0 || block[start] === 0x20)) start++ + while (end > start && (block[end - 1] === 0 || block[end - 1] === 0x20)) end-- + let value = 0 + for (let i = start; i < end; i++) { + const c = block[i] + if (c < 0x30 || c > 0x37) { + throw invalidHeader(`invalid octal number in the header field at offset ${offset}`) + } + value = value * 8 + (c - 0x30) + } + return value +} + +// GNU base-256: the high bit of the first byte is set, and the next bit is the +// sign of a two's complement big-endian number +const readNumber = (block: Uint8Array, offset: number, length: number): number => { + if ((block[offset] & 0x80) === 0) return readOctal(block, offset, length) + const negative = (block[offset] & 0x40) !== 0 + // Negative numbers are read as the bitwise inverse of their magnitude minus one + let value = 0 + for (let i = 0; i < length; i++) { + let c = block[offset + i] + if (negative) c ^= 0xff + if (i === 0) c &= 0x7f + value = value * 256 + c + if (value > Number.MAX_SAFE_INTEGER) { + throw invalidHeader(`base-256 number in the header field at offset ${offset} is out of range`) + } + } + return negative ? -value - 1 : value +} + +const readUnsigned = (block: Uint8Array, offset: number, length: number, field: string): number => { + const value = readNumber(block, offset, length) + if (value < 0) { + throw invalidHeader(`negative ${field}`) + } + return value +} + +// The checksum is the sum of the block bytes with the checksum field counted as +// spaces. Some old tar writers summed signed bytes, which is accepted too. +const verifyChecksum = (block: Uint8Array): boolean => { + const stored = readOctal(block, 148, 8) + let unsigned = sumBlock(block) + 8 * 0x20 + for (let i = 148; i < 156; i++) unsigned -= block[i] + if (stored === unsigned) return true + let high = 0 + for (let i = 0; i < BLOCK; i++) { + if (block[i] > 0x7f && (i < 148 || i >= 156)) high++ + } + return stored === unsigned - 0x100 * high +} + +interface RawHeader { + readonly typeflag: string + path: string + linkTarget: string + size: number + readonly mode: number + uid: number + gid: number + uname: string + gname: string + mtimeMillis: number + devMajor: number + devMinor: number +} + +const parseHeader = (block: Uint8Array): RawHeader => { + if (!verifyChecksum(block)) { + throw invalidHeader("header checksum mismatch") + } + const isUstar = hasBytes(block, 257, USTAR_MAGIC) + const isGnu = !isUstar && hasBytes(block, 257, GNU_MAGIC) + const header: RawHeader = { + typeflag: String.fromCharCode(block[156]), + path: readString(block, 0, 100), + linkTarget: readString(block, 157, 100), + size: readUnsigned(block, 124, 12, "size"), + mode: readUnsigned(block, 100, 8, "mode"), + uid: readUnsigned(block, 108, 8, "uid"), + gid: readUnsigned(block, 116, 8, "gid"), + mtimeMillis: readNumber(block, 136, 12) * 1000, + uname: "", + gname: "", + devMajor: 0, + devMinor: 0 + } + if (!isUstar && !isGnu) return header + header.uname = readString(block, 265, 32) + header.gname = readString(block, 297, 32) + header.devMajor = readUnsigned(block, 329, 8, "devMajor") + header.devMinor = readUnsigned(block, 337, 8, "devMinor") + if (isUstar) { + // star archives end the block with "tar\0" and keep times after a shorter prefix + const prefix = hasBytes(block, 508, STAR_TRAILER) ? readString(block, 345, 131) : readString(block, 345, 155) + if (prefix.length > 0) header.path = `${prefix}/${header.path}` + } + return header +} + +const MAX_OCTAL_7 = 0o7777777 // 8-byte numeric fields +const MAX_OCTAL_11 = 0o77777777777 // 12-byte numeric fields + +const writeString = (block: Uint8Array, offset: number, length: number, value: string): void => { + const n = Math.min(value.length, length) + for (let i = 0; i < n; i++) block[offset + i] = value.charCodeAt(i) +} + +// Writes `length - 1` zero-padded octal digits; the value must fit +const writeOctal = (block: Uint8Array, offset: number, length: number, value: number): void => { + let i = offset + length - 2 + // Bit operations are faster, but only cover 32 bits + for (; value > 0x3fffffff && i >= offset; i--) { + block[i] = 0x30 + (value % 8) + value = Math.floor(value / 8) + } + for (; i >= offset; i--) { + block[i] = 0x30 + (value & 7) + value >>>= 3 + } +} + +// Split at the last slash that fits the 155-byte USTAR prefix. +const splitPath = (path: string): readonly [prefix: string, name: string] | undefined => { + if (path.length <= 100 || !isAscii(path)) return undefined + let length = path.length + if (length > 156) length = 156 + else if (path[length - 1] === "/") length-- + const i = path.lastIndexOf("/", length - 1) + if (i <= 0 || path.length - i - 1 > 100) return undefined + return [path.slice(0, i), path.slice(i + 1)] +} + +interface UstarFields { + readonly name: string + readonly prefix: string + readonly typeflag: string + readonly size: number + readonly mode: number + readonly uid: number + readonly gid: number + readonly mtime: number + readonly linkname: string + readonly uname: string + readonly gname: string + readonly devMajor: number + readonly devMinor: number +} + +const paxHeaderFields: UstarFields = { + name: "", + prefix: "", + typeflag: "x", + size: 0, + mode: 0o644, + uid: 0, + gid: 0, + mtime: 0, + linkname: "", + uname: "", + gname: "", + devMajor: 0, + devMinor: 0 +} + +const writeUstarBlock = (block: Uint8Array, fields: UstarFields): void => { + writeString(block, 0, 100, fields.name) + writeOctal(block, 100, 8, fields.mode) + writeOctal(block, 108, 8, fields.uid) + writeOctal(block, 116, 8, fields.gid) + writeOctal(block, 124, 12, fields.size) + writeOctal(block, 136, 12, fields.mtime) + block[156] = fields.typeflag.charCodeAt(0) + writeString(block, 157, 100, fields.linkname) + block.set(USTAR_MAGIC_VERSION, 257) + writeString(block, 265, 32, fields.uname) + writeString(block, 297, 32, fields.gname) + writeOctal(block, 329, 8, fields.devMajor) + writeOctal(block, 337, 8, fields.devMinor) + writeString(block, 345, 155, fields.prefix) + block.fill(0x20, 148, 156) + // Six digits, a NUL and the space left from the fill + writeOctal(block, 148, 7, sumBlock(block)) + block[154] = 0 +} + +const checkNumber = (field: string, value: number | undefined, max: number): number => { + const n = value ?? 0 + if (!Number.isSafeInteger(n) || n < 0 || n > max) { + throw invalidHeader(`invalid ${field} ${n}`) + } + return n +} + +const checkNoNul = (field: string, value: string): string => { + if (value.includes("\0")) { + throw invalidHeader(`${field} must not contain NUL characters`) + } + return value +} + +// Writes the header blocks of an entry, with a PAX header first when needed, into +// zeroed memory from `alloc`. Throws before allocating when the header is invalid. +const encodeHeader = (header: Header, alloc: (size: number) => Uint8Array): void => { + const path = checkNoNul("path", header.path) + if (path === "") { + throw invalidHeader("path must not be empty") + } + // GNU tar and node-tar read such an entry as a directory + if (header.type === "File" && path.endsWith("/")) { + throw invalidHeader(`File entry ${JSON.stringify(path)} must not end with "/"`) + } + const typeflag = typeflagOf(header) + if (typeflag.length !== 1 || typeflag.charCodeAt(0) >= 0x80 || "xgLKS".includes(typeflag)) { + throw invalidHeader(`invalid typeflag ${JSON.stringify(typeflag)}`) + } + const size = checkNumber("size", header.size, Number.MAX_SAFE_INTEGER) + if (size > 0 && isHeaderOnly(header.type)) { + throw invalidHeader(`${header.type} entry ${JSON.stringify(path)} must have size 0`) + } + const mode = checkNumber("mode", header.mode, Number.MAX_SAFE_INTEGER) & PERMISSION_BITS + const uid = checkNumber("uid", header.uid, Number.MAX_SAFE_INTEGER) + const gid = checkNumber("gid", header.gid, Number.MAX_SAFE_INTEGER) + const devMajor = checkNumber("devMajor", header.devMajor, MAX_OCTAL_7) + const devMinor = checkNumber("devMinor", header.devMinor, MAX_OCTAL_7) + const millis = header.mtime.getTime() + if (Number.isNaN(millis)) { + throw invalidHeader("mtime must be a valid date") + } + const mtime = Math.floor(millis / 1000) + + // Records of the PAX header, created when one is needed. Their keys are + // unique, as custom records cannot use the keys of header fields. + let pax: Array | undefined + for (const key of Object.keys(header.pax)) { + if (key.startsWith("GNU.sparse.")) { + throw invalidHeader(`PAX record ${JSON.stringify(key)} is not supported`) + } + if (paxHeaderKeys.has(key)) { + throw invalidHeader(`PAX record ${JSON.stringify(key)} must be set through its header field`) + } + if (key === "" || key.includes("=") || key.includes("\0")) { + throw invalidHeader(`invalid PAX record ${JSON.stringify(key)}`) + } + pax ??= [] + pax.push([key, header.pax[key]]) + } + + // PAX holds overflow values; USTAR keeps truncated ASCII strings or zero. + const split = splitPath(path) + const linkTarget = checkNoNul("linkTarget", header.linkTarget ?? "") + const uname = checkNoNul("uname", header.uname) + const gname = checkNoNul("gname", header.gname) + const pathFits = split !== undefined || fits(path, 100) + const linkTargetFits = fits(linkTarget, 100) + const unameFits = fits(uname, 32) + const gnameFits = fits(gname, 32) + if (!pathFits) (pax ??= []).push(["path", path]) + if (!linkTargetFits) (pax ??= []).push(["linkpath", linkTarget]) + if (!unameFits) (pax ??= []).push(["uname", uname]) + if (!gnameFits) (pax ??= []).push(["gname", gname]) + if (size > MAX_OCTAL_11) (pax ??= []).push(["size", String(size)]) + if (uid > MAX_OCTAL_7) (pax ??= []).push(["uid", String(uid)]) + if (gid > MAX_OCTAL_7) (pax ??= []).push(["gid", String(gid)]) + if (mtime < 0 || mtime > MAX_OCTAL_11) (pax ??= []).push(["mtime", String(mtime)]) + + const ustar: UstarFields = { + name: split !== undefined ? split[1] : pathFits ? path : toAscii(path).slice(0, 100), + prefix: split !== undefined ? split[0] : "", + typeflag, + size: size > MAX_OCTAL_11 ? 0 : size, + mode, + uid: uid > MAX_OCTAL_7 ? 0 : uid, + gid: gid > MAX_OCTAL_7 ? 0 : gid, + mtime: mtime < 0 || mtime > MAX_OCTAL_11 ? 0 : mtime, + linkname: linkTargetFits ? linkTarget : toAscii(linkTarget).slice(0, 100), + uname: unameFits ? uname : toAscii(uname).slice(0, 32), + gname: gnameFits ? gname : toAscii(gname).slice(0, 32), + devMajor, + devMinor + } + + if (pax === undefined) { + writeUstarBlock(alloc(BLOCK), ustar) + return + } + const records = pax.sort(([a], [b]) => a < b ? -1 : 1).map(([key, value]) => paxRecord(key, value)) + let dataLength = 0 + for (const record of records) dataLength += record.length + const paxBlocks = 1 + Math.ceil(dataLength / BLOCK) + const out = alloc(BLOCK * (paxBlocks + 1)) + writeUstarBlock(out, { ...paxHeaderFields, name: paxHeaderName(path), size: dataLength }) + let offset = BLOCK + for (const record of records) { + out.set(record, offset) + offset += record.length + } + writeUstarBlock(out.subarray(BLOCK * paxBlocks), ustar) +} + +const MAX_META_SIZE = 1024 * 1024 // PAX and GNU long name data +const MAX_DATE_MILLIS = 8.64e15 // the range of `Date` + +// A queue of byte chunks that hands out views without copying where possible. +// Consumed chunks are released right away. +class ByteQueue { + private chunks: Array = [] + private head = 0 + private offset = 0 + length = 0 + consumed = 0 + + push(chunk: Uint8Array): void { + if (chunk.length === 0) return + this.chunks.push(chunk) + this.length += chunk.length + } + + takeSome(max: number): Uint8Array { + const chunk = this.chunks[this.head]! + const available = chunk.length - this.offset + if (max < available) { + const out = chunk.subarray(this.offset, this.offset + max) + this.offset += max + this.length -= max + this.consumed += max + return out + } + const out = this.offset === 0 ? chunk : chunk.subarray(this.offset) + this.chunks[this.head++] = undefined + this.length -= available + this.consumed += available + this.offset = 0 + if (this.head === this.chunks.length) { + this.chunks = [] + this.head = 0 + } else if (this.head >= 1024) { + this.chunks = this.chunks.slice(this.head) + this.head = 0 + } + return out + } + + // Takes exactly `n` bytes; the queue must hold at least `n` + take(n: number): Uint8Array { + if (n === 0) return new Uint8Array(0) + const first = this.takeSome(n) + if (first.length === n) return first + const out = new Uint8Array(n) + out.set(first) + let filled = first.length + while (filled < n) { + const part = this.takeSome(n - filled) + out.set(part, filled) + filled += part.length + } + return out + } + + clear(): void { + this.chunks = [] + this.head = 0 + this.offset = 0 + this.length = 0 + } +} + +// Returned by the decoder when the buffered input is not enough to continue +const NeedInput: unique symbol = Symbol("effect/encoding/Tar/NeedInput") + +// `next` skips unread body bytes; `read` consumes the current body. +// Both preserve progress across `NeedInput` retries. +class Decoder { + private readonly queue = new ByteQueue() + private ended = false + private finished = false + private closed = false + // Incremented whenever the current body is invalidated + generation = 0 + private remaining = 0 + private pad = 0 + // Bytes to discard before the next header + private skip = 0 + private zeroBlock = false + // Archive offset of the last header block read, for error messages + private at = 0 + // An extended header whose data is still to be read + private meta: RawHeader | undefined + private pax: Map | undefined + // Records of the PAX global headers read so far + private global: Map | undefined + private longName = "" + private longLink = "" + + push(chunk: Uint8Array): void { + if (!this.finished) this.queue.push(chunk) + } + + end(): void { + this.ended = true + } + + close(): void { + this.closed = true + this.generation++ + this.queue.clear() + } + + // Returns the next entry header, or `undefined` once the archive and the + // input have ended. Input after the end of the archive is discarded. + next(): Header | undefined | typeof NeedInput { + try { + return this.advance() + } catch (error) { + // Header errors are thrown where the archive offset is not known + if (isTarError(error) && (error.kind === "InvalidHeader" || error.kind === "Unsupported")) { + throw new TarError({ kind: error.kind, message: `${error.message} (header at byte ${this.at})` }) + } + throw error + } + } + + private advance(): Header | undefined | typeof NeedInput { + this.generation++ + this.skip += this.remaining + this.pad + this.remaining = 0 + this.pad = 0 + const queue = this.queue + while (true) { + if (this.finished) return this.finish() + while (this.skip > 0) { + if (queue.length === 0) return this.needInput() + this.skip -= queue.takeSome(this.skip).length + } + if (this.meta !== undefined) { + const { size, typeflag } = this.meta + const total = size + padding(size) + if (queue.length < total) return this.needInput() + const data = queue.take(total).subarray(0, size) + this.meta = undefined + if (typeflag === "x") this.pax = parsePax(data) + else if (typeflag === "g") this.addGlobal(parsePax(data)) + else if (typeflag === "L") this.longName = readString(data, 0, size) + else this.longLink = readString(data, 0, size) + continue + } + if (queue.length < BLOCK) { + if (queue.length === 0 && this.ended) return this.finish() + return this.needInput() + } + this.at = queue.consumed + const block = queue.take(BLOCK) + if (isZeroBlock(block)) { + if (this.zeroBlock) return this.finish() + this.zeroBlock = true + continue + } + if (this.zeroBlock) { + throw invalidHeader("non-zero block after an end-of-archive block") + } + const raw = parseHeader(block) + switch (raw.typeflag) { + case "x": + case "g": + case "L": + case "K": { + if (raw.size > MAX_META_SIZE) { + throw invalidHeader(`extended header of ${raw.size} bytes exceeds the limit`) + } + this.meta = raw + continue + } + case "S": { + throw unsupported("GNU sparse files are not supported") + } + } + const header = this.toHeader(raw) + this.remaining = header.size + this.pad = padding(header.size) + return header + } + } + + // Returns the buffered body bytes of the entry from generation `id`, or + // `undefined` once the whole body has been read + read(id: number): Arr.NonEmptyArray | undefined | typeof NeedInput { + if (id !== this.generation) { + throw new TarError({ + kind: "BodyConsumed", + message: this.closed ? + "tar entry body read after the archive stream ended" : + "tar entry body read after moving to the next entry" + }) + } + if (this.remaining === 0) return undefined + if (this.queue.length === 0) return this.needInput() + const chunks = Arr.of(this.queue.takeSome(this.remaining)) + this.remaining -= chunks[0].length + while (this.remaining > 0 && this.queue.length > 0) { + const chunk = this.queue.takeSome(this.remaining) + this.remaining -= chunk.length + chunks.push(chunk) + } + return chunks + } + + private needInput(): typeof NeedInput { + if (this.ended) throw truncated(this.queue.consumed + this.queue.length) + return NeedInput + } + + private finish(): undefined | typeof NeedInput { + // An extended header must be followed by the entry it describes + if (!this.finished && (this.pax !== undefined || this.longName !== "" || this.longLink !== "")) { + throw truncated(this.queue.consumed) + } + this.finished = true + this.queue.clear() + return this.ended ? undefined : NeedInput + } + + // Global records apply to every entry that follows, except the ones that only + // describe a single entry, and an empty value removes a record + private addGlobal(records: Map): void { + for (const [key, value] of records) { + if (key === "path" || key === "linkpath" || key === "size") continue + if (value !== "") (this.global ??= new Map()).set(key, value) + else this.global?.delete(key) + } + if (this.global?.size === 0) this.global = undefined + } + + private toHeader(raw: RawHeader): Header { + // The records of the entry's own extended header override global ones + const records = this.global === undefined + ? this.pax + : this.pax === undefined + ? this.global + : new Map([...this.global, ...this.pax]) + const rest = records === undefined ? noPax : applyPax(raw, records) + // Empty GNU long names are ignored, as in Go + if (this.longName !== "") raw.path = this.longName + if (this.longLink !== "") raw.linkTarget = this.longLink + this.pax = undefined + this.longName = "" + this.longLink = "" + if (!(Math.abs(raw.mtimeMillis) <= MAX_DATE_MILLIS)) { + throw invalidHeader("mtime is out of range") + } + const type = typeOf(raw.typeflag, raw.path) + const header: Types.Mutable
= { + path: raw.path, + type, + typeflag: raw.typeflag, + size: isHeaderOnly(type) ? 0 : raw.size, + mode: raw.mode & PERMISSION_BITS, + uid: raw.uid, + gid: raw.gid, + uname: raw.uname, + gname: raw.gname, + mtime: new Date(raw.mtimeMillis), + pax: rest + } + if (raw.linkTarget !== "") header.linkTarget = raw.linkTarget + if (type === "CharDevice" || type === "BlockDevice") { + header.devMajor = raw.devMajor + header.devMinor = raw.devMinor + } + return header + } +} + +const SLAB_SIZE = 64 * 1024 + +const emptyBody = new Uint8Array(0) + +// Bodies below this size are copied next to their header instead of being +// handed out as chunks of their own +const COPY_LIMIT = 16 * 1024 + +// Pack headers, padding and small body chunks into slabs; pass large chunks +// through without copying. Flushed slab regions are never overwritten. +class Encoder { + private slab = new Uint8Array(0) + // Start of the bytes in `slab` that have not been flushed, and their end + private start = 0 + private offset = 0 + private chunks: Array = [] + buffered = 0 + // The entry whose body is being written, and the body bytes written so far + private header: Header | undefined + private written = 0 + + begin(header: Header): void { + encodeHeader(header, this.alloc) + this.header = header + this.written = 0 + } + + body(chunk: Uint8Array): void { + const header = this.header! + this.written += chunk.length + if (this.written > header.size) { + throw sizeMismatch(header, this.written) + } + if (chunk.length < COPY_LIMIT) { + // `reserve` can start a new slab, so it must run before `slab` is read + const offset = this.reserve(chunk.length) + this.slab.set(chunk, offset) + return + } + this.cut() + this.chunks.push(chunk) + this.buffered += chunk.length + } + + end(): void { + const header = this.header! + this.header = undefined + if (this.written !== header.size) { + throw sizeMismatch(header, this.written) + } + this.reserve(padding(header.size)) + } + + finish(): void { + this.reserve(BLOCK * 2) + } + + flush(): Arr.NonEmptyArray { + this.cut() + const chunks = this.chunks as Arr.NonEmptyArray + this.chunks = [] + this.buffered = 0 + return chunks + } + + // Ends the current run of slab bytes as a chunk + private cut(): void { + if (this.offset > this.start) { + this.chunks.push(this.slab.subarray(this.start, this.offset)) + this.start = this.offset + } + } + + // Returns `size` zeroed bytes of the output + private alloc = (size: number): Uint8Array => { + const offset = this.reserve(size) + return this.slab.subarray(offset, offset + size) + } + + // Reserves `size` zeroed bytes of the output in `slab`, and returns their offset + private reserve(size: number): number { + if (this.offset + size > this.slab.length) { + this.cut() + this.slab = new Uint8Array(Math.max(SLAB_SIZE, size)) + this.start = 0 + this.offset = 0 + } + const offset = this.offset + this.offset += size + this.buffered += size + return offset + } +} diff --git a/repos/effect/packages/effect/src/encoding/Toml.ts b/packages/effect/src/encoding/Toml.ts similarity index 99% rename from repos/effect/packages/effect/src/encoding/Toml.ts rename to packages/effect/src/encoding/Toml.ts index b95d711e1..f9ff653b1 100644 --- a/repos/effect/packages/effect/src/encoding/Toml.ts +++ b/packages/effect/src/encoding/Toml.ts @@ -169,9 +169,9 @@ class TomlParser { this.advance() } key = this.input.slice(start, this.index) - } - if (key.length === 0) { - this.fail("Expected a key") + if (key.length === 0) { + this.fail("Expected a key") + } } keys.push(key) this.skipInlineWhitespace() diff --git a/packages/effect/src/encoding/Xml.ts b/packages/effect/src/encoding/Xml.ts new file mode 100644 index 000000000..3c55b738d --- /dev/null +++ b/packages/effect/src/encoding/Xml.ts @@ -0,0 +1,2037 @@ +/** + * Decodes and encodes XML 1.0 documents as streams of events in Effect + * channels. + * + * `decode` and `decodeString` turn UTF-8 bytes or text into events, and + * `encode` and `encodeString` write events back as XML. `elements` collects + * repeated elements, such as the items of a feed, into small trees, so that a + * large document can be processed one record at a time. + * + * The decoder checks that documents are well-formed. It never reads external + * resources and never expands entities declared in a DOCTYPE, so external + * entity (XXE) and entity expansion attacks do not apply. It does not validate + * documents against a DTD or schema, and reports names as written, without + * resolving namespaces. + * + * @stability experimental + * @since 4.0.6 + */ +import * as Arr from "../Array.ts" +import * as Cause from "../Cause.ts" +import * as Channel from "../Channel.ts" +import * as Data from "../Data.ts" +import * as Effect from "../Effect.ts" +import * as Predicate from "../Predicate.ts" +import * as Pull from "../Pull.ts" + +const XmlErrorTypeId = "~effect/encoding/Xml/XmlError" + +/** + * Error raised when XML cannot be decoded or encoded. + * + * **Details** + * + * The `kind` field identifies the failure: + * + * - `Syntax`: the input is not well-formed XML or not valid UTF-8. + * - `Unsupported`: the input uses a feature this module does not process, such + * as an entity declared in a DOCTYPE or a byte encoding other than UTF-8. + * - `Limit`: the input exceeds `maxTokenSize`, `maxDepth` or `maxElementSize`. + * - `InvalidEvent`: the events do not form a well-formed document, such as an + * end tag that does not match its start tag. + * + * Errors from `decode` and `decodeString` carry the one-based `line` and + * `column` where the problem was found, also appended to `message`. Columns + * count UTF-16 code units, and lines end at LF, CR or CRLF. + * + * @stability experimental + * @category errors + * @since 4.0.6 + */ +export class XmlError extends Data.TaggedError("XmlError")<{ + readonly kind: "Syntax" | "Unsupported" | "Limit" | "InvalidEvent" + readonly message: string + readonly line?: number | undefined + readonly column?: number | undefined +}> { + /** + * Marks this value as an XML error for runtime guards. + * + * @stability experimental + * @since 4.0.6 + */ + readonly [XmlErrorTypeId] = XmlErrorTypeId +} + +/** + * Checks whether a value is an `XmlError`. + * + * @stability experimental + * @category guards + * @since 4.0.6 + */ +export const isXmlError = (u: unknown): u is XmlError => Predicate.hasProperty(u, XmlErrorTypeId) + +/** + * Event for the XML declaration at the start of a document, such as + * ``. + * + * **Details** + * + * `encoding` and `standalone` are present only when the declaration sets them. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Declaration { + readonly _tag: "Declaration" + readonly version: string + readonly encoding?: string | undefined + readonly standalone?: boolean | undefined +} + +/** + * Event for a document type declaration, such as ``. + * + * **Details** + * + * `publicId` and `systemId` are present only when the declaration names an + * external DTD; the DTD is never read. Whitespace in `publicId` is collapsed + * to single spaces and trimmed. The internal subset is not reported. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Doctype { + readonly _tag: "Doctype" + readonly name: string + readonly publicId?: string | undefined + readonly systemId?: string | undefined +} + +/** + * Event for a start tag, with its attributes in document order. + * + * **Details** + * + * `name` is the qualified name as written and is the only element-name field + * the encoder reads. + * + * Attribute values have their references replaced and whitespace normalized as + * XML requires. Decoded attributes inherit nothing, so only the attributes of + * the tag are present and `__proto__` is an ordinary key. Their key order is + * document order, as XML names are never integer-like. A `structuredClone` of + * a decoded map gets `Object.prototype` back. A self-closing tag such as + * `
` is decoded as a `StartElement` followed by an `EndElement`. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface StartElement { + readonly _tag: "StartElement" + readonly name: string + readonly attributes: Readonly> +} + +/** + * Event for an end tag. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface EndElement { + readonly _tag: "EndElement" + readonly name: string +} + +/** + * Event for character data inside the root element. + * + * **Details** + * + * Adjacent text, references and CDATA sections are decoded into one `Text` + * event that ends at the next tag, comment or processing instruction, however + * the input is chunked. Line endings are normalized to `\n`. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Text { + readonly _tag: "Text" + readonly text: string +} + +/** + * Event for a comment, holding the text between ``. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Comment { + readonly _tag: "Comment" + readonly text: string +} + +/** + * Event for a processing instruction, such as + * ``. + * + * **Details** + * + * `data` is the text after the target, without leading whitespace. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface ProcessingInstruction { + readonly _tag: "ProcessingInstruction" + readonly target: string + readonly data: string +} + +/** + * Event produced by the decoders and consumed by the encoders. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export type Event = + | Declaration + | Doctype + | StartElement + | EndElement + | Text + | Comment + | ProcessingInstruction + +/** + * Element collected by `elements`, with its attributes and children. + * + * **Details** + * + * Children are elements and strings of character data, in document order. + * Adjacent text is merged into one string; comments and processing + * instructions are dropped. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Element { + readonly name: string + readonly attributes: Readonly> + readonly children: ReadonlyArray +} + +/** + * Options for the XML decoders. + * + * **Details** + * + * - `maxTokenSize`: the maximum length of a single token in UTF-16 code units, + * as written in the input: a tag with its attributes, a comment, a processing + * instruction, a DOCTYPE, or a text node including its references and CDATA + * sections. Defaults to 10 MiB; `Infinity` disables the limit. + * - `maxDepth`: the maximum number of nested elements. Defaults to 1024; + * `Infinity` disables the limit. + * + * Memory per token and for open element names is bounded by `maxTokenSize`. + * Invalid options throw when the channel is created. + * + * @stability experimental + * @category configuration + * @since 4.0.6 + */ +export interface DecodeOptions { + readonly maxTokenSize?: number | undefined + readonly maxDepth?: number | undefined +} + +/** + * Options for `elements`. + * + * **Details** + * + * `maxElementSize` is the maximum size of one collected element, counted as + * the UTF-16 code units of its names, attribute names and values, and text. + * Defaults to 16 MiB; `Infinity` disables the limit. Invalid options throw when + * the channel is created. + * + * @stability experimental + * @category configuration + * @since 4.0.6 + */ +export interface ElementsOptions { + readonly maxElementSize?: number | undefined +} + +/** + * Creates a channel that decodes XML text chunks into events. + * + * **Details** + * + * - Tokens can be split anywhere between chunks. Each output chunk holds the + * events completed by one input chunk. + * - The document must have one root element. Whitespace outside it is not + * reported. + * - A leading byte order mark is skipped, and the encoding named by the XML + * declaration is ignored. + * - Events completed before an error are emitted before the channel fails. + * + * **Gotchas** + * + * - End of input inside the root fails with a `Syntax` error ("unclosed + * element"); interruption ends the stream without this check. + * - A DOCTYPE is reported as a `Doctype` event, and its internal subset is + * skipped without checking its declarations. They are not applied, so + * default attribute values are not added, and parameter entities are not + * expanded. + * - References to entities other than `<`, `>`, `&`, `'` and + * `"` fail with an `Unsupported` `XmlError` when the document has a + * DOCTYPE, and with a `Syntax` `XmlError` otherwise. Character references + * such as `é` are always decoded. + * - Names are reported as written, including namespace prefixes, and `xmlns` + * declarations are ordinary attributes. + * - Tokens or aggregate open element names longer than `maxTokenSize`, and + * elements nested deeper than `maxDepth`, fail with a `Limit` `XmlError`. A + * long token can instead fail with an error found in its first + * `maxTokenSize + 1` characters. + * - Strings in events can share memory with their input chunk, so keeping a + * short string, such as an id from each record, can keep the whole chunk in + * memory. Copy strings that are kept long, for example with + * `structuredClone`. + * + * **Example** (Decoding events) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * import { Xml } from "effect/encoding" + * + * const events = Stream.make("Hi & bye
").pipe( + * Stream.pipeThroughChannel(Xml.decodeString()), + * Stream.map((event) => event._tag), + * Stream.runCollect + * ) + * await Effect.runPromise(events) // => ["StartElement", "Text", "StartElement", "EndElement", "EndElement"] + * ``` + * + * @see {@link decode} for UTF-8 byte input + * @see {@link elements} for collecting repeated elements into trees + * @see {@link encodeString} for the inverse operation + * + * @stability experimental + * @category decoding + * @since 4.0.6 + */ +export const decodeString = (options?: DecodeOptions): Channel.Channel< + Arr.NonEmptyReadonlyArray, + XmlError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => { + const parser = makeParser(options, false) + return makeChannel(() => new TextInput(parser())) +} + +/** + * Creates a channel that decodes UTF-8 XML bytes into events. + * + * **Details** + * + * Works like `decodeString` after decoding the bytes as UTF-8. Multi-byte + * characters can be split between chunks, and a leading byte order mark is + * skipped. + * + * **Gotchas** + * + * - Invalid or truncated UTF-8 fails with a `Syntax` `XmlError`. + * - An initial UTF-16 byte order mark or an XML declaration naming an encoding + * other than UTF-8 or US-ASCII fails with an `Unsupported` `XmlError`. + * Decode such bytes with a `TextDecoder` for that encoding and use `decodeString`. + * + * @see {@link decodeString} for text input and the shared gotchas + * + * @stability experimental + * @category decoding + * @since 4.0.6 + */ +export const decode = (options?: DecodeOptions): Channel.Channel< + Arr.NonEmptyReadonlyArray, + XmlError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => { + const parser = makeParser(options, true) + return makeChannel(() => new Utf8Input(parser())) +} + +/** + * Creates a channel that collects the elements at a path into trees. + * + * **When to use** + * + * Use to process large documents such as feeds, sitemaps or exports one record + * at a time, without holding the whole document in memory. + * + * **Details** + * + * `path` lists element names from the root, and `"*"` matches any name. Each + * element whose ancestors and own name match the path is emitted once its end + * tag arrives, with all of its descendants; everything outside the matched + * elements is skipped. Because paths start at the root, an element nested in a + * matched element is part of its tree and is not emitted separately. + * + * **Gotchas** + * + * - A path that does not start at the root element matches nothing. + * - Whitespace between child elements, such as indentation, is kept as string + * children. + * - A collected element larger than `maxElementSize` fails with a `Limit` + * `XmlError`. The size counts characters, not memory: an element with many + * small children can take many times its size in memory, so use a lower + * limit for untrusted input. + * - Events that end inside an element, or end an element that was never + * started, fail with an `InvalidEvent` `XmlError`. + * - A path that is empty or contains an invalid name throws when the channel is + * created. + * + * **Example** (Reading feed items) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * import { Xml } from "effect/encoding" + * + * const feed = `News + * First + * Second + * ` + * + * const items = await Effect.runPromise( + * Stream.make(feed).pipe( + * Stream.pipeThroughChannel(Xml.decodeString()), + * Stream.pipeThroughChannel(Xml.elements(["rss", "channel", "item"])), + * Stream.runCollect + * ) + * ) + * items.length // => 2 + * items[1] // => { name: "item", attributes: {}, children: [{ name: "title", attributes: {}, children: ["Second"] }] } + * ``` + * + * @see {@link decode} for producing events from bytes + * + * @stability experimental + * @category decoding + * @since 4.0.6 + */ +export const elements = ( + path: ReadonlyArray, + options?: ElementsOptions +): Channel.Channel< + Arr.NonEmptyReadonlyArray, + XmlError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => { + if (path.length === 0) { + throw new Error("Xml: elements path must not be empty") + } + for (const name of path) { + if (name !== "*" && !isName(name)) { + throw new Error(`Xml: invalid element name in path: ${JSON.stringify(name)}`) + } + } + const maxElementSize = checkLimit("maxElementSize", options?.maxElementSize, defaultMaxElementSize) + const segments = Array.from(path) + return makeChannel(() => new Collector(segments, maxElementSize)) +} + +/** + * Creates a channel that writes events as XML text. + * + * **Details** + * + * - Each input chunk is written as one string. + * - Text escapes `&`, `<` and `>`. Attribute values are double-quoted and + * escape `&`, `<`, `"`, tabs and line breaks. Carriage returns are written + * as ` ` so that they survive decoding. + * - Start tags are written completely on their event. Empty elements use + * explicit closing tags, such as `

`, never self-closing tags. + * - Nothing is added between events, so whitespace comes only from `Text` + * events. + * + * **Gotchas** + * + * Events that do not form one well-formed document fail with an + * `InvalidEvent` `XmlError`, such as an end tag that does not match the open + * element, a second root element, non-whitespace text outside the root + * element, invalid names or characters, `--` in a comment, `?>` in processing + * instruction data, or a declaration that is not the first event. The stream + * must end with the root element closed. + * + * **Example** (Writing a document) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * import { Xml } from "effect/encoding" + * + * const events: ReadonlyArray = [ + * { _tag: "StartElement", name: "a", attributes: { href: "x?a=1&b=2" } }, + * { _tag: "Text", text: "1 < 2" }, + * { _tag: "EndElement", name: "a" } + * ] + * + * const xml = Stream.fromIterable(events).pipe( + * Stream.pipeThroughChannel(Xml.encodeString()), + * Stream.mkString + * ) + * await Effect.runPromise(xml) // => '
1 < 2' + * ``` + * + * @see {@link encode} for UTF-8 byte output + * @see {@link decodeString} for the inverse operation + * + * @stability experimental + * @category encoding + * @since 4.0.6 + */ +export const encodeString = (): Channel.Channel< + Arr.NonEmptyReadonlyArray, + XmlError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => makeChannel(() => new Writer(false)) + +/** + * Creates a channel that writes events as UTF-8 XML bytes. + * + * **Details** + * + * Works like `encodeString`, with one byte array per input chunk. + * + * **Gotchas** + * + * A `Declaration` event naming an encoding other than UTF-8 fails + * with an `InvalidEvent` `XmlError`. + * + * @see {@link encodeString} for text output and the shared gotchas + * + * @stability experimental + * @category encoding + * @since 4.0.6 + */ +export const encode = (): Channel.Channel< + Arr.NonEmptyReadonlyArray, + XmlError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => + Channel.map(makeChannel(() => new Writer(true)), Arr.map((text) => textEncoder.encode(text))) + +const defaultMaxTokenSize = 10 * 1024 * 1024 +const defaultMaxDepth = 1024 +const defaultMaxElementSize = 16 * 1024 * 1024 + +const checkLimit = (name: string, value: number | undefined, defaultValue: number): number => { + const limit = value ?? defaultValue + if (!(limit > 0)) { + throw new Error(`Xml: ${name} must be positive, got ${limit}`) + } + return limit +} + +// The per-run state of a channel. `push` and `end` add output to `out`, and +// throw `XmlError`s. +interface Machine { + out: Array + push(input: In): void + end(): void +} + +const makeChannel = ( + make: () => Machine +): Channel.Channel< + Arr.NonEmptyReadonlyArray, + XmlError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => + Channel.fromTransform((upstream: Pull.Pull, IE, Done>, _scope) => + Effect.sync(() => { + const machine = make() + let done: Pull.Pull | undefined + let failure: XmlError | undefined + + // Output completed before an error is emitted before the failure + const emit = ( + inputs: ReadonlyArray | undefined + ): Pull.Pull, XmlError | IE, Done> => { + try { + if (inputs === undefined) machine.end() + else for (let i = 0; i < inputs.length; i++) machine.push(inputs[i]) + } catch (error) { + if (!isXmlError(error)) throw error + failure = error + } + const out = machine.out + if (Arr.isArrayNonEmpty(out)) { + machine.out = [] + return Effect.succeed(out) + } + return pull + } + + const step = Pull.matchEffect(upstream, { + onSuccess: emit, + onFailure: Effect.failCause, + onDone: (value) => { + done = Cause.done(value) + return emit(undefined) + } + }) + const pull: Pull.Pull, XmlError | IE, Done> = Effect.suspend(() => + failure !== undefined ? Effect.fail(failure) : done ?? step + ) + return pull + }) + ) + +const makeParser = (options: DecodeOptions | undefined, bytes: boolean): () => Parser => { + const maxTokenSize = checkLimit("maxTokenSize", options?.maxTokenSize, defaultMaxTokenSize) + const maxDepth = checkLimit("maxDepth", options?.maxDepth, defaultMaxDepth) + return () => new Parser(maxTokenSize, maxDepth, bytes) +} + +const textEncoder = new TextEncoder() + +const TAB = 9 +const LF = 10 +const CR = 13 +const SPACE = 32 +const BANG = 33 +const DQUOTE = 34 +const HASH = 35 +const AMPERSAND = 38 +const SQUOTE = 39 +const DASH = 45 +const SLASH = 47 +const LT = 60 +const EQUALS = 61 +const GT = 62 +const QUESTION = 63 +const LBRACKET = 91 +const RBRACKET = 93 +const LOWER_X = 120 + +const isSpace = (c: number): boolean => c === SPACE || c === LF || c === TAB || c === CR + +// Characters outside the XML `Char` production, including unpaired surrogates +// oxlint-disable-next-line no-control-regex +const invalidChar = /[\0-\x08\x0B\x0C\x0E-\x1F\uFFFE\uFFFF\uD800-\uDFFF]/u + +// Decoded UTF-8 has no unpaired surrogates, and without the `u` flag the +// search runs several times faster on non-ASCII text +// oxlint-disable-next-line no-control-regex +const invalidDecodedChar = /[\0-\x08\x0B\x0C\x0E-\x1F\uFFFE\uFFFF]/ + +// `isWellFormed` (ES2024) finds unpaired surrogates much faster than a `u` +// regex, which is slow on non-ASCII text +const isWellFormed: ((s: string) => boolean) | undefined = + typeof (String.prototype as { isWellFormed?: unknown }).isWellFormed === "function" ? + (s) => (s as unknown as { isWellFormed(): boolean }).isWellFormed() : + undefined + +// Returns the index of the first character outside the XML `Char` production, +// or -1 +const searchInvalid = (s: string): number => + isWellFormed !== undefined && s.search(invalidDecodedChar) === -1 && isWellFormed(s) ? -1 : s.search(invalidChar) + +const isChar = (c: number): boolean => + c === TAB || c === LF || c === CR || (c >= 0x20 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD) || + (c >= 0x10000 && c <= 0x10FFFF) + +const NAME_START = 1 +const NAME_CHAR = 2 +const asciiName = new Uint8Array(128) +for (let c = 0; c < 128; c++) { + if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95 || c === 58) asciiName[c] = NAME_START | NAME_CHAR + else if ((c >= 48 && c <= 57) || c === DASH || c === 46) asciiName[c] = NAME_CHAR +} + +// Name characters of XML 1.0 fifth edition. Supplementary characters are +// accepted by their surrogates, which are paired once input is validated. +const isNameStartCode = (c: number): boolean => + c < 128 ? + (asciiName[c] & NAME_START) !== 0 : + (c >= 0xC0 && c <= 0xD6) || (c >= 0xD8 && c <= 0xF6) || (c >= 0xF8 && c <= 0x2FF) || + (c >= 0x370 && c <= 0x37D) || (c >= 0x37F && c <= 0x1FFF) || (c >= 0x200C && c <= 0x200D) || + (c >= 0x2070 && c <= 0x218F) || (c >= 0x2C00 && c <= 0x2FEF) || (c >= 0x3001 && c <= 0xD7FF) || + (c >= 0xF900 && c <= 0xFDCF) || (c >= 0xFDF0 && c <= 0xFFFD) || (c >= 0xD800 && c <= 0xDB7F) + +const isNameCode = (c: number): boolean => + c < 128 ? + (asciiName[c] & NAME_CHAR) !== 0 : + isNameStartCode(c) || c === 0xB7 || (c >= 0x300 && c <= 0x36F) || (c >= 0x203F && c <= 0x2040) || + (c >= 0xDC00 && c <= 0xDFFF) + +// Returns the end of the name starting at `i`, or `i` when there is none +const scanName = (s: string, i: number, end: number): number => { + if (i >= end || !isNameStartCode(s.charCodeAt(i))) return i + i++ + while (i < end && isNameCode(s.charCodeAt(i))) i++ + return i +} + +// Name characters exclude every invalid character but unpaired surrogates +const isName = (s: string): boolean => + s.length > 0 && scanName(s, 0, s.length) === s.length && + (isWellFormed !== undefined ? isWellFormed(s) : !invalidChar.test(s)) + +const skipSpace = (s: string, i: number, end: number): number => { + while (i < end && isSpace(s.charCodeAt(i))) i++ + return i +} + +const lineEndings = /\r\n?/g +const attributeWhitespace = /\r\n|[\t\n\r]/g +// Characters of an attribute value that need decoding, or are not allowed +const attributeSpecial = /[&<\t\n\r]/ +const attributeWhitespaceChar = /[\t\n\r]/ +// Attribute values shorter than this are scanned with a loop +const shortValue = 24 +const publicIdWhitespace = /[ \r\n]+/g +const decimalDigits = /^[0-9]+$/ +const hexDigits = /^[0-9a-fA-F]+$/ +const versionPattern = /^1\.[0-9]+$/ +const encodingPattern = /^[A-Za-z][A-Za-z0-9._-]*$/ + +const normalizeLines = (s: string): string => s.indexOf("\r") === -1 ? s : s.replace(lineEndings, "\n") + +// Normalizes the line endings and whitespace of a literal attribute value part +const normalizeAttribute = (s: string): string => s.replace(attributeWhitespace, " ") + +const isReservedTarget = (target: string): boolean => target.length === 3 && target.toLowerCase() === "xml" + +const isUtf8 = (encoding: string): boolean => { + const lower = encoding.toLowerCase() + return lower === "utf-8" || lower === "us-ascii" +} + +const declarationPattern = + /^[ \t\r\n]+version[ \t\r\n]*=[ \t\r\n]*(?:"(1\.[0-9]+)"|'(1\.[0-9]+)')(?:[ \t\r\n]+encoding[ \t\r\n]*=[ \t\r\n]*(?:"([A-Za-z][A-Za-z0-9._-]*)"|'([A-Za-z][A-Za-z0-9._-]*)'))?(?:[ \t\r\n]+standalone[ \t\r\n]*=[ \t\r\n]*(?:"(yes|no)"|'(yes|no)'))?[ \t\r\n]*$/ + +const pubidPattern = /^[ \r\na-zA-Z0-9\-'()+,./:=?;!*#@$_%]*$/ + +// States of `Scanner`, which finds where a token split between chunks ends +const S_START = 0 +const S_TEXT = 1 +const S_LT = 2 // after "<" +const S_BANG = 3 // after "= 2) { + if (this.state === S_COMMENT) return i + 1 + this.reset(S_SUBSET) + } else { + this.aux = 0 + } + break + } + case S_CDATA: { + if (c === RBRACKET) this.aux++ + else if (c === GT && this.aux >= 2) return i + 1 + else this.aux = 0 + break + } + case S_PI: + case S_SUBSET_PI: { + if (c === GT && this.aux === 1) { + if (this.state === S_PI) return i + 1 + this.reset(S_SUBSET) + } else { + this.aux = c === QUESTION ? 1 : 0 + } + break + } + case S_TAG: { + // Attribute values cannot contain "<" + if (c === LT) return i + if (this.aux === 0) { + if (c === GT) return i + 1 + if (c === DQUOTE || c === SQUOTE) this.aux = c + } else if (c === this.aux) { + this.aux = 0 + } + break + } + case S_DOCTYPE: + case S_DECLARATION: { + if (this.aux === 0) { + if (c === LT) return i + if (c === GT) { + if (this.state === S_DOCTYPE) return i + 1 + this.reset(S_SUBSET) + } else if (c === LBRACKET && this.state === S_DOCTYPE) { + this.reset(S_SUBSET) + } else if (c === DQUOTE || c === SQUOTE) { + this.aux = c + } + } else if (c === this.aux) { + this.aux = 0 + } + break + } + case S_SUBSET: { + if (c === RBRACKET) this.state = S_AFTER_SUBSET + else if (c === LT) this.state = S_SUBSET_LT + else if (c === DQUOTE || c === SQUOTE || c === GT) return i + break + } + case S_SUBSET_LT: { + if (c === BANG) this.state = S_SUBSET_BANG + else if (c === QUESTION) this.reset(S_SUBSET_PI) + else return i + break + } + case S_SUBSET_BANG: { + if (c === DASH) { + this.state = S_SUBSET_BANG_DASH + } else { + this.reset(S_DECLARATION) + continue + } + break + } + case S_SUBSET_BANG_DASH: { + if (c !== DASH) return i + this.reset(S_SUBSET_COMMENT) + break + } + case S_AFTER_SUBSET: { + if (c === GT) return i + 1 + if (!isSpace(c)) return i + break + } + } + i++ + } + return -1 + } +} + +const PROLOG = 0 +const CONTENT = 1 +const EPILOG = 2 + +const describeToken = (s: string, i: number): string => + s.startsWith("", i + 4) + if (end === -1) return -1 + const dashes = s.indexOf("--", i + 4) + if (dashes < end) throw this.syntax("\"--\" is not allowed in comments", dashes) + this.checkSize(i, end + 3) + this.flushText() + this.out.push({ _tag: "Comment", text: normalizeLines(s.slice(i + 4, end)) }) + return end + 3 + } + + private processingInstruction(s: string, i: number): number { + const end = s.indexOf("?>", i + 2) + if (end === -1) return -1 + const t = scanName(s, i + 2, end) + if (t === i + 2) throw this.syntax("invalid processing instruction target", i + 2) + const target = s.slice(i + 2, t) + if (isReservedTarget(target)) { + if (target === "xml" && this.base + i === 0) return this.declaration(s, i, t, end) + throw this.syntax( + target === "xml" ? + "the XML declaration is only allowed at the start of the document" : + `processing instruction target ${target} is reserved`, + i + ) + } + let data = "" + if (t < end) { + if (!isSpace(s.charCodeAt(t))) throw this.syntax("expected whitespace after the target", t) + data = normalizeLines(s.slice(skipSpace(s, t, end), end)) + } + this.checkSize(i, end + 2) + this.flushText() + this.out.push({ _tag: "ProcessingInstruction", target, data }) + return end + 2 + } + + private declaration(s: string, i: number, t: number, end: number): number { + this.checkSize(i, end + 2) + const match = declarationPattern.exec(s.slice(t, end)) + if (match === null) throw this.syntax("invalid XML declaration", i) + const encoding = match[3] ?? match[4] + const standalone = match[5] ?? match[6] + if (this.bytes && encoding !== undefined && !isUtf8(encoding)) { + throw this.error( + "Unsupported", + `encoding ${encoding} is not supported for byte input, decode the bytes to text first`, + this.base + i + ) + } + this.ascii = this.bytes && encoding?.toLowerCase() === "us-ascii" + const event: { -readonly [K in keyof Declaration]: Declaration[K] } = { + _tag: "Declaration", + version: match[1] ?? match[2] + } + if (encoding !== undefined) event.encoding = encoding + if (standalone !== undefined) event.standalone = standalone === "yes" + this.out.push(event) + return end + 2 + } + + private cdata(s: string, i: number): number { + const end = s.indexOf("]]>", i + 9) + if (end === -1) return -1 + if (this.state !== CONTENT) throw this.syntax("CDATA sections are only allowed inside the root element", i) + this.addText(i, end + 3 - i) + this.text += normalizeLines(s.slice(i + 9, end)) + return end + 3 + } + + private doctypeDeclaration(s: string, i: number): number { + if (this.state !== PROLOG || this.doctype) { + throw this.syntax("a DOCTYPE is only allowed once, before the root element", i) + } + const scanner = new Scanner() + scanner.reset(S_DOCTYPE) + const end = scanner.scan(s, i + 9) + if (end === -1) return -1 + this.checkSize(i, end) + this.doctype = true + let j = this.space(s, i + 9, end) + const nameEnd = this.name(s, j, end) + const event: { -readonly [K in keyof Doctype]: Doctype[K] } = { _tag: "Doctype", name: s.slice(j, nameEnd) } + j = skipSpace(s, nameEnd, end) + if (j > nameEnd && (s.startsWith("PUBLIC", j) || s.startsWith("SYSTEM", j))) { + if (s.charCodeAt(j) === 80) { + // Whitespace in public identifiers is normalized for matching + event.publicId = this.publicLiteral(s, this.space(s, j + 6, end), end).replace(publicIdWhitespace, " ").trim() + j = this.literalEnd + } + event.systemId = normalizeLines( + this.literal(s, this.space(s, j + (event.publicId === undefined ? 6 : 0), end), end) + ) + j = skipSpace(s, this.literalEnd, end) + } + if (s.charCodeAt(j) === LBRACKET) { + // The scanner skips the internal subset, whose declarations are not + // checked, and ends the token early only at malformed markup + if (scanner.state !== S_AFTER_SUBSET) throw this.syntax("invalid markup in the internal subset", end) + j = skipSpace(s, s.lastIndexOf("]", end - 1) + 1, end) + } + if (j !== end - 1 || s.charCodeAt(j) !== GT) throw this.syntax("invalid DOCTYPE", j) + this.out.push(event) + return end + } + + // Skips the whitespace that is required at `i` + private space(s: string, i: number, end: number): number { + if (i >= end || !isSpace(s.charCodeAt(i))) throw this.syntax("expected whitespace", i) + return skipSpace(s, i, end) + } + + // Returns the end of the name that is required at `i` + private name(s: string, i: number, end: number): number { + const nameEnd = scanName(s, i, end) + if (nameEnd === i) throw this.syntax("expected a name", i) + return nameEnd + } + + // Reads the quoted literal at `i`, and sets `literalEnd` to the index after it + private literal(s: string, i: number, end: number): string { + const quote = s.charCodeAt(i) + if (quote !== DQUOTE && quote !== SQUOTE) throw this.syntax("expected a quoted literal", i) + const close = s.indexOf(quote === DQUOTE ? "\"" : "'", i + 1) + // Unreachable while the scanner ends a DOCTYPE only after its literals close + if (close === -1 || close >= end) throw this.syntax("unterminated literal", i) + this.literalEnd = close + 1 + return s.slice(i + 1, close) + } + + private publicLiteral(s: string, i: number, end: number): string { + const literal = this.literal(s, i, end) + if (!pubidPattern.test(literal)) throw this.syntax("invalid public identifier", i) + return literal + } +} + +const indexOrEnd = (s: string, search: string, from: number): number => { + const i = s.indexOf(search, from) + return i === -1 ? s.length : i +} + +class TextInput implements Machine { + private readonly parser: Parser + // A high surrogate at the end of a chunk, which pairs with the next chunk + private surrogate = "" + + constructor(parser: Parser) { + this.parser = parser + } + + get out(): Array { + return this.parser.out + } + set out(out: Array) { + this.parser.out = out + } + + push(chunk: string): void { + if (this.surrogate !== "") { + chunk = this.surrogate + chunk + this.surrogate = "" + } + const last = chunk.charCodeAt(chunk.length - 1) + if (last >= 0xD800 && last <= 0xDBFF) { + this.surrogate = chunk.slice(-1) + chunk = chunk.slice(0, -1) + } + this.parser.feed(chunk) + } + + end(): void { + // An unpaired high surrogate fails as an invalid character + this.parser.feed(this.surrogate) + this.parser.end() + } +} + +const utf8Length = (lead: number): number => lead >= 0xF0 ? 4 : lead >= 0xE0 ? 3 : lead >= 0xC0 ? 2 : 1 + +// Returns the length of the valid UTF-8 prefix of `bytes` +const validUtf8 = (bytes: Uint8Array): number => { + let i = 0 + while (i < bytes.length) { + const lead = bytes[i] + if (lead < 0x80) { + i++ + continue + } + const length = lead >= 0xC2 && lead <= 0xF4 ? utf8Length(lead) : 0 + if (length === 0 || i + length > bytes.length) return i + let code = lead & (0xFF >> (length + 1)) + for (let k = 1; k < length; k++) { + const c = bytes[i + k] + if ((c & 0xC0) !== 0x80) return i + code = (code << 6) | (c & 0x3F) + } + const min = length === 2 ? 0x80 : length === 3 ? 0x800 : 0x10000 + if (code < min || code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF)) return i + i += length + } + return i +} + +const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }) + +// Decodes UTF-8 without streaming state, so that the position of invalid bytes +// is known: an incomplete sequence at the end of a chunk is carried over. +class Utf8Input implements Machine { + private readonly parser: Parser + private carry: Uint8Array | undefined + private started = false + + constructor(parser: Parser) { + this.parser = parser + } + + get out(): Array { + return this.parser.out + } + set out(out: Array) { + this.parser.out = out + } + + push(chunk: Uint8Array): void { + if (!this.started && chunk.length > 0) { + const first = this.carry === undefined ? chunk[0] : this.carry[0] + const second = this.carry === undefined ? chunk[1] : chunk[0] + if ((first === 0xFE && second === 0xFF) || (first === 0xFF && second === 0xFE)) { + throw this.parser.error( + "Unsupported", + "UTF-16 is not supported for byte input, decode the bytes to text first", + 0 + ) + } + // Keep a lone BOM prefix pending in the UTF-8 carry below + this.started = second !== undefined || (first !== 0xFE && first !== 0xFF) + } + let start = 0 + if (this.carry !== undefined) { + const need = utf8Length(this.carry[0]) - this.carry.length + const head = new Uint8Array(this.carry.length + Math.min(need, chunk.length)) + head.set(this.carry) + head.set(chunk.subarray(0, need), this.carry.length) + if (chunk.length < need) { + this.carry = head + return + } + this.carry = undefined + this.decode(head) + start = need + } + const n = chunk.length + let end = n + for (let k = 1; k <= 3 && n - k >= start; k++) { + const b = chunk[n - k] + if ((b & 0xC0) === 0x80) continue + if (b >= 0xC0 && k < utf8Length(b)) end = n - k + break + } + if (end < n) this.carry = chunk.slice(end) + if (end > start) this.decode(start === 0 && end === n ? chunk : chunk.subarray(start, end)) + } + + end(): void { + // The carried bytes may also be an invalid sequence, such as a single 0xFF + if (this.carry !== undefined) { + this.parser.feed("", true) + throw this.parser.error("Syntax", "invalid UTF-8", this.parser.received) + } + this.parser.end() + } + + private decode(bytes: Uint8Array): void { + let text: string + try { + text = utf8Decoder.decode(bytes) + } catch { + this.parser.feed(utf8Decoder.decode(bytes.subarray(0, validUtf8(bytes))), true) + throw this.parser.error("Syntax", "invalid UTF-8", this.parser.received) + } + this.parser.feed(text) + } +} + +interface MutableElement { + readonly name: string + readonly attributes: Readonly> + readonly children: Array +} + +class Collector implements Machine { + out: Array = [] + private depth = 0 + // Number of open elements, from the root, that match the path + private matched = 0 + // The element being collected and its open descendants + private readonly stack: Array = [] + private size = 0 + private readonly path: ReadonlyArray + private readonly maxElementSize: number + + constructor(path: ReadonlyArray, maxElementSize: number) { + this.path = path + this.maxElementSize = maxElementSize + } + + push(event: Event): void { + switch (event._tag) { + case "StartElement": { + const depth = this.depth++ + if (this.stack.length === 0) { + const segment = this.path[depth] + if (this.matched !== depth || (segment !== "*" && segment !== event.name)) return + if (++this.matched < this.path.length) return + this.size = 0 + } + const element: MutableElement = { name: event.name, attributes: event.attributes, children: [] } + if (this.stack.length > 0) this.stack[this.stack.length - 1].children.push(element) + this.stack.push(element) + let size = event.name.length + for (const key in event.attributes) size += key.length + event.attributes[key].length + this.grow(size) + return + } + case "EndElement": { + if (this.depth === 0) throw invalidEvent(`end tag without a start tag`) + if (this.stack.length > 0) { + const element = this.stack.pop()! + if (this.stack.length === 0) this.out.push(element) + } + if (this.matched === this.depth) this.matched-- + this.depth-- + return + } + case "Text": { + if (this.stack.length === 0 || event.text.length === 0) return + this.grow(event.text.length) + const children = this.stack[this.stack.length - 1].children + const last = children.length - 1 + if (last >= 0 && typeof children[last] === "string") children[last] += event.text + else children.push(event.text) + return + } + } + } + + end(): void { + if (this.depth > 0) throw invalidEvent("events ended inside an element") + } + + private grow(size: number): void { + this.size += size + if (this.size > this.maxElementSize) { + throw positionless("Limit", `element <${this.stack[0].name}> exceeds maxElementSize of ${this.maxElementSize}`) + } + } +} + +// JavaScriptCore gives every Error its own source `line` and `column`, which +// would pass for an XML position, so errors without one set both to undefined +const positionless = (kind: XmlError["kind"], message: string) => + new XmlError({ kind, message, line: undefined, column: undefined }) + +const invalidEvent = (message: string) => positionless("InvalidEvent", message) + +const textSpecial = /[&<>\r]/ +const attributeSpecials = /[&<"\t\n\r]/ + +// The regex finds the first special character, which is fast when there is +// none; the rest is copied in runs between escapes. Invalid characters are +// kept, so that the writer can check them in the output. +const escapeText = (s: string): string => { + const first = s.search(textSpecial) + if (first === -1) return s + let out = "" + let last = 0 + for (let i = first; i < s.length; i++) { + const c = s.charCodeAt(i) + const escaped = c === AMPERSAND ? "&" : c === LT ? "<" : c === GT ? ">" : c === CR ? " " : undefined + if (escaped !== undefined) { + out += s.slice(last, i) + escaped + last = i + 1 + } + } + return out + s.slice(last) +} + +const escapeAttribute = (s: string): string => { + const first = s.search(attributeSpecials) + if (first === -1) return s + let out = "" + let last = 0 + for (let i = first; i < s.length; i++) { + const c = s.charCodeAt(i) + const escaped = c === AMPERSAND ? + "&" : + c === LT ? + "<" : + c === DQUOTE ? + """ : + c === TAB ? + " " : + c === LF ? + " " : + c === CR ? + " " : + undefined + if (escaped !== undefined) { + out += s.slice(last, i) + escaped + last = i + 1 + } + } + return out + s.slice(last) +} + +const checkChars = (what: string, s: string): void => { + const invalid = searchInvalid(s) + if (invalid !== -1) { + const code = s.codePointAt(invalid)!.toString(16).toUpperCase().padStart(4, "0") + throw invalidEvent(`${what} contains the invalid character U+${code}`) + } +} + +const checkName = (what: string, name: string): void => { + if (typeof name !== "string" || !isName(name)) throw invalidEvent(`invalid ${what} ${JSON.stringify(name)}`) +} + +const isWhitespace = (s: string): boolean => { + for (let i = 0; i < s.length; i++) { + if (!isSpace(s.charCodeAt(i))) return false + } + return true +} + +const NOTHING_WRITTEN = 3 + +class Writer implements Machine { + out: Array = [] + private readonly stack: Array = [] + private state = NOTHING_WRITTEN + private doctype = false + private readonly bytes: boolean + // Element and attribute names already checked, which repeat in most + // documents. Cleared when full, so that it stays small. + private readonly names = new Set() + + constructor(bytes: boolean) { + this.bytes = bytes + } + + private checkName(what: string, name: string): void { + if (!this.isName(name)) checkName(what, name) + } + + private isName(name: string): boolean { + if (this.names.has(name)) return true + if (typeof name !== "string" || !isName(name)) return false + if (this.names.size >= 1024) this.names.clear() + this.names.add(name) + return true + } + + // Checks attributes one by one, in order, after a batched check has failed + private checkAttributes(attributes: Readonly>): void { + for (const name in attributes) { + if (!Object.hasOwn(attributes, name)) continue + const value = attributes[name] + this.checkName("attribute name", name) + if (typeof value !== "string") throw invalidEvent(`attribute ${name} must be a string`) + checkChars(`attribute ${name}`, value) + } + } + + push(event: Event): void { + const text = this.write(event) + if (text.length > 0) { + const out = this.out + if (out.length > 0) out[0] += text + else out.push(text) + } + } + + end(): void { + if (this.stack.length > 0) throw invalidEvent(`unclosed element <${this.stack[this.stack.length - 1]}>`) + if (this.state !== EPILOG) throw invalidEvent("no root element") + } + + private write(event: Event): string { + if (event._tag === "EndElement") { + const open = this.stack.length > 0 ? this.stack[this.stack.length - 1] : undefined + if (open === undefined || event.name !== open) { + // Only valid names are written into messages + checkName("element name", event.name) + throw invalidEvent( + open === undefined + ? `end tag without a start tag` + : `end tag does not match <${open}>` + ) + } + this.stack.pop() + if (this.stack.length === 0) this.state = EPILOG + return `` + } + switch (event._tag) { + case "StartElement": { + this.checkName("element name", event.name) + if (this.state === EPILOG) throw invalidEvent(`second root element <${event.name}>`) + let tag = "<" + event.name + let valid = true + for (const name in event.attributes) { + if (!Object.hasOwn(event.attributes, name)) continue + const value = event.attributes[name] + if (typeof value !== "string" || !this.isName(name)) { + valid = false + break + } + tag += ` ${name}="${escapeAttribute(value)}"` + } + // Escaping keeps invalid characters, so one search of the tag checks + // every value; the first error is then found in attribute order + if (!valid || (tag.length > event.name.length + 1 && searchInvalid(tag) !== -1)) { + this.checkAttributes(event.attributes) + } + this.stack.push(event.name) + this.state = CONTENT + return tag + ">" + } + case "Text": { + if (this.state !== CONTENT) { + checkChars("text", event.text) + if (!isWhitespace(event.text)) throw invalidEvent("text outside the root element must be whitespace") + if (this.state === NOTHING_WRITTEN && event.text.length > 0) this.state = PROLOG + return event.text + } + checkChars("text", event.text) + return escapeText(event.text) + } + case "Comment": { + checkChars("comment", event.text) + if (event.text.includes("--") || event.text.endsWith("-")) { + throw invalidEvent("comments cannot contain \"--\" or end with \"-\"") + } + if (this.state === NOTHING_WRITTEN) this.state = PROLOG + return `` + } + case "ProcessingInstruction": { + checkName("processing instruction target", event.target) + if (isReservedTarget(event.target)) { + throw invalidEvent(`processing instruction target ${event.target} is reserved`) + } + checkChars("processing instruction", event.data) + if (event.data.includes("?>")) throw invalidEvent("processing instruction data cannot contain \"?>\"") + if (this.state === NOTHING_WRITTEN) this.state = PROLOG + return `` + } + case "Declaration": { + if (this.state !== NOTHING_WRITTEN) throw invalidEvent("the XML declaration must be the first event") + if (!versionPattern.test(event.version)) { + throw invalidEvent(`invalid XML version ${JSON.stringify(event.version)}`) + } + let declaration = `" + } + case "Doctype": { + if (this.state === CONTENT || this.state === EPILOG || this.doctype) { + throw invalidEvent("a DOCTYPE must appear once, before the root element") + } + checkName("DOCTYPE name", event.name) + let doctype = `" + } + } + } +} diff --git a/repos/effect/packages/effect/src/encoding/Yaml.ts b/packages/effect/src/encoding/Yaml.ts similarity index 78% rename from repos/effect/packages/effect/src/encoding/Yaml.ts rename to packages/effect/src/encoding/Yaml.ts index e553d226b..66de40842 100644 --- a/repos/effect/packages/effect/src/encoding/Yaml.ts +++ b/packages/effect/src/encoding/Yaml.ts @@ -4,9 +4,13 @@ * Supports YAML 1.2 block and flow collections, indentless sequence values, * multiline plain and quoted scalars, block scalars, anchors, and aliases. * + * Unlike YAML 1.2, the closing bracket of a multiline flow collection may sit + * at its parent's indentation. + * * Unsupported: compact nested block sequences (`- - value` on one line), - * multiline flow collections, explicit complex keys, tag/directive processing, - * recursive aliases, and document streams. Mapping keys are JavaScript strings. + * multiline quoted scalars in flow collections, explicit complex keys, + * tag/directive processing, recursive aliases, and document streams. Mapping + * keys are JavaScript strings. * * @stability unstable * @since 4.0.0 @@ -37,6 +41,7 @@ type Line = { readonly text: string readonly indent: number readonly number: number + readonly tabIndent: boolean } const hasOwn = Object.prototype.hasOwnProperty @@ -63,8 +68,8 @@ const closingQuote = (input: string, quote: string, start: number): number => { return -1 } -const findIndicator = (input: string, indicator: ":" | "#"): number => { - let depth = 0 +// `depth` is the number of flow collections already open before `input`. +const findIndicator = (input: string, indicator: ":" | "#", depth = 0): number => { let tokenStart = true for (let index = 0; index < input.length; index++) { const character = input[index] @@ -95,8 +100,8 @@ const findIndicator = (input: string, indicator: ":" | "#"): number => { return -1 } -const stripComment = (input: string): string => { - const index = findIndicator(input, "#") +const stripComment = (input: string, depth = 0): string => { + const index = findIndicator(input, "#", depth) return index === -1 ? input : input.slice(0, index).trimEnd() } @@ -106,6 +111,13 @@ const isSequenceEntry = (text: string): boolean => text === "-" || text.startsWi const foldLineBreak = (blankLines: number): string => blankLines === 0 ? " " : "\n".repeat(blankLines) +// Folds the line breaks of a multiline plain flow scalar. +const foldFlowLines = (input: string): string => + input.replace( + /[ \t]*\n((?:[ \t]*\n)*)[ \t]*/g, + (_, blankLines: string) => foldLineBreak(blankLines.split("\n").length - 1) + ) + const endsWithEscape = (text: string): boolean => /\\*$/.exec(text)![0].length % 2 === 1 // Escaped trailing spaces are content, not folding whitespace. @@ -205,13 +217,16 @@ const parseKey = (input: string): string => { } class FlowParser { - private readonly input: string - private readonly anchors: ReadonlyMap + private input: string + private readonly anchors: Map + private readonly nextLine: (depth: number) => string | undefined private index = 0 + private depth = 0 - constructor(input: string, anchors: ReadonlyMap) { + constructor(input: string, anchors: Map, nextLine: (depth: number) => string | undefined) { this.input = input this.anchors = anchors + this.nextLine = nextLine } parse(): unknown { @@ -226,8 +241,12 @@ class FlowParser { private parseValue(): unknown { this.skipWhitespace() const character = this.peek() - if (character === "[") return this.parseSequence() - if (character === "{") return this.parseMapping() + if (character === "[" || character === "{") { + this.depth++ + const collection = character === "[" ? this.parseSequence() : this.parseMapping() + this.depth-- + return collection + } if (character === "\"" || character === "'") return parseScalar(this.readQuoted(character)) if (character === "*") { this.index++ @@ -235,7 +254,17 @@ class FlowParser { if (!this.anchors.has(name)) this.fail(`Unknown alias '*${name}'`) return this.anchors.get(name) } - return parseScalar(this.readUntil(/[,\]}]/).trim()) + if (character === "&") { + this.index++ + const name = this.readUntil(/[\s,[\]{}]/) + if (name === "" || /[[{]/.test(this.peek())) this.fail("Invalid anchor") + this.skipWhitespace() + if (this.peek() === "&") this.fail("Multiple anchors on one node") + const value = this.parseValue() + this.anchors.set(name, value) + return value + } + return parseScalar(foldFlowLines(this.readUntil(/[,\]}]/)).trim()) } private parseSequence(): Array { @@ -296,7 +325,7 @@ class FlowParser { if (character === "\"" || character === "'") { return parseKey(this.readQuoted(character)) } - return this.readUntil(/:/).trim() + return foldFlowLines(this.readUntil(/:/)).trim() } private readQuoted(quote: string): string { @@ -309,7 +338,7 @@ class FlowParser { private readUntil(stop: RegExp): string { const start = this.index - while (this.index < this.input.length && !stop.test(this.peek())) this.index++ + while (!stop.test(this.peek()) && this.index < this.input.length) this.index++ return this.input.slice(start, this.index) } @@ -323,6 +352,11 @@ class FlowParser { } private peek(): string { + // Inside a collection, the end of the line continues on the next line. + if (this.index === this.input.length && this.depth > 0) { + const line = this.nextLine(this.depth) + if (line !== undefined) this.input += `\n${line}` + } return this.input[this.index] ?? "" } @@ -441,10 +475,24 @@ class YamlParser { if (!this.anchors.has(name)) throw new SyntaxError(`Unknown YAML alias '*${name}'`) return this.anchors.get(name) } - if (text.startsWith("[") || text.startsWith("{")) return new FlowParser(text, this.anchors).parse() + if (text.startsWith("[") || text.startsWith("{")) return this.parseFlowValue(text, parentIndent) return this.parsePlainValue(text, parentIndent) } + private parseFlowValue(text: string, parentIndent: number): unknown { + return new FlowParser(text, this.anchors, (depth) => { + const next = this.lines[this.index] + if (next === undefined || (next.indent === 0 && /^(?:---|\.\.\.)(?:\s|$)/.test(next.text))) return undefined + // Continuation lines are more indented than the parent; a closing bracket may sit at its indentation. + const content = next.text.trim().length > 0 + if (content && next.indent < parentIndent) return undefined + if (content && next.indent === parentIndent && !/^[\]}]/.test(next.text)) return undefined + this.index++ + // Strip comments again knowing the open collections, so quoted `#` after a comma is kept. + return stripComment(next.raw.slice(next.indent), depth) + }).parse() + } + private parsePlainValue(value: string, parentIndent: number): unknown { const first = this.lines[this.index - 1] if (/:(?:\s|$)/.test(value)) this.fail(first, "Invalid colon in plain scalar") @@ -454,12 +502,14 @@ class YamlParser { const blankLines = this.skipBlankLines() const next = this.lines[this.index] if (next === undefined || next.indent <= parentIndent) break + this.rejectTabIndent(next) // Quotes in plain continuations are literal. const content = next.raw.slice(next.indent) const comment = content.search(/(?:^|[ \t])#/) const text = (comment === -1 ? content : content.slice(0, comment)).trimEnd() if (text.length === 0 || (next.indent === 0 && /^(?:---|\.\.\.)(?:\s|$)/.test(text))) break if (/:(?:\s|$)/.test(text)) this.fail(next, "Invalid colon in plain scalar") + this.rejectTabIndentedBlankLines(blankLines, parentIndent) output += foldLineBreak(blankLines) + text this.index++ terminated = comment !== -1 @@ -483,10 +533,13 @@ class YamlParser { if (quote === "\"" && endsWithEscape(line)) { output += line.slice(0, -1) } else { - output += trimFoldedEnd(line, quote) + foldLineBreak(this.skipBlankLines()) + const blankLines = this.skipBlankLines() + this.rejectTabIndentedBlankLines(blankLines, parentIndent) + output += trimFoldedEnd(line, quote) + foldLineBreak(blankLines) } const next = this.lines[this.index++] if (next === undefined) throw new SyntaxError("Unterminated quoted YAML scalar") + this.rejectTabIndent(next) if (next.raw.trim().length > 0 && next.indent <= parentIndent) { this.fail(next, "Invalid quoted scalar indentation") } @@ -500,27 +553,35 @@ class YamlParser { const explicitIndent = Number.parseInt(indicator.replace(/[^1-9]/g, ""), 10) const start = this.index let end = start - let contentIndent = Number.isNaN(explicitIndent) ? Number.POSITIVE_INFINITY : parentIndent + explicitIndent + let contentIndent = Number.isNaN(explicitIndent) ? undefined : parentIndent + explicitIndent + let hasContent = false while (end < this.lines.length) { const line = this.lines[end] - if (line.raw.trim().length !== 0 && line.indent <= parentIndent) break - if (line.raw.trim().length !== 0) contentIndent = Math.min(contentIndent, line.indent) + // Only spaces make a line empty; a tab after the indentation is content. + if (/[^ ]/.test(line.raw)) { + if (contentIndent === undefined && line.indent > parentIndent) { + // The first non-empty line sets the indentation, which no leading empty line may exceed. + const deeper = this.lines.slice(start, end).find((empty) => empty.indent > line.indent) + if (deeper !== undefined) { + this.fail(deeper, "Leading empty lines cannot be more indented than the block scalar content") + } + contentIndent = line.indent + } + if (contentIndent !== undefined && line.indent >= contentIndent) { + hasContent = true + } else { + this.rejectTabIndent(line) + // A less indented line or comment ends the scalar. + if (line.indent <= parentIndent || line.text.length === 0) break + this.fail(line, `Expected block scalar indentation of ${contentIndent} spaces`) + } + } end++ } - if (contentIndent === Number.POSITIVE_INFINITY) contentIndent = parentIndent + 1 - const content: Array = [] - for (let index = start; index < end; index++) { - const line = this.lines[index] - if (line.raw.trim().length === 0) { - content.push("") - } else if (line.indent < contentIndent) { - this.fail(line, `Expected block scalar indentation of ${contentIndent} spaces`) - } else { - content.push(line.raw.slice(contentIndent)) - } - } + // Whitespace past the indentation is content only once the scalar has a content line. + const content = this.lines.slice(start, end).map((line) => hasContent ? line.raw.slice(contentIndent) : "") this.index = end let contentEnd = content.length @@ -539,7 +600,7 @@ class YamlParser { if (line.length === 0) { blankLines++ } else { - const moreIndented = line.startsWith(" ") + const moreIndented = line.startsWith(" ") || line.startsWith("\t") const hardBreak = moreIndented || previousMoreIndented === true if (previousMoreIndented === undefined) output += "\n".repeat(blankLines) else if (blankLines === 0) output += hardBreak ? "\n" : " " @@ -554,6 +615,17 @@ class YamlParser { return chomp === "clip" && contentEnd > 0 && trailingBreaks > 0 ? `${output}\n` : output } + private rejectTabIndent(line: Line): void { + if (line.tabIndent) this.fail(line, "Tabs cannot be used for YAML indentation") + } + + private rejectTabIndentedBlankLines(count: number, parentIndent: number): void { + for (let index = this.index - count; index < this.index; index++) { + const line = this.lines[index] + if (line.indent <= parentIndent) this.rejectTabIndent(line) + } + } + private skipBlankLines(): number { const start = this.index while (this.index < this.lines.length && this.lines[this.index].raw.trim().length === 0) this.index++ @@ -577,6 +649,7 @@ class YamlParser { this.fail(this.lines[this.index], "Multiple YAML documents are not supported") } } else { + this.rejectTabIndent(line) return } } @@ -603,14 +676,13 @@ export const parse = (input: string): unknown => { const source = input.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n") const lines = source.split("\n").map((raw, index): Line => { const indentation = /^( *)/.exec(raw)![1].length - if (raw.slice(0, indentation + 1).includes("\t")) { - throw new SyntaxError(`Tabs cannot be used for YAML indentation at line ${index + 1}`) - } return { raw, text: stripComment(raw.slice(indentation)), indent: indentation, - number: index + 1 + number: index + 1, + // Rejected where the line is read as structure; block scalar content may hold tabs. + tabIndent: raw[indentation] === "\t" } }) return new YamlParser(lines).parse() diff --git a/packages/effect/src/encoding/Zip.ts b/packages/effect/src/encoding/Zip.ts new file mode 100644 index 000000000..cb9eadd45 --- /dev/null +++ b/packages/effect/src/encoding/Zip.ts @@ -0,0 +1,1558 @@ +/** + * Encodes and decodes zip archives as streams in Effect channels. + * + * `encode` writes each entry as it arrives and adds the central directory once + * the input ends; `decode` reads entries in archive order from their local + * headers. `decode` never buffers an entire entry body, and `encode` only + * collects deflated bodies with a `size` of at most 64 KiB. + * + * Entries are stored or compressed with deflate, the methods every zip tool + * reads. Compression uses the runtime's `CompressionStream("deflate-raw")`; + * decompression is implemented here, so that `decode` can find the end of + * entries whose sizes follow their data. ZIP64 fields are written when sizes, + * offsets or the entry count need them, and read from local headers and data + * descriptors. + * Encryption, other compression methods and archives split across files are + * not supported. + * + * This module is experimental and may receive breaking changes in patch + * releases. + * + * @stability experimental + * @since 4.0.6 + */ +import * as Arr from "../Array.ts" +import * as Cause from "../Cause.ts" +import * as Channel from "../Channel.ts" +import * as Data from "../Data.ts" +import * as Effect from "../Effect.ts" +import * as Exit from "../Exit.ts" +import { ByteQueue, InflateError, InflateMemory, Inflater, NeedInput } from "../internal/inflate.ts" +import * as Predicate from "../Predicate.ts" +import * as Pull from "../Pull.ts" +import * as Scope from "../Scope.ts" +import * as Semaphore from "../Semaphore.ts" +import * as Stream from "../Stream.ts" + +const ZipErrorTypeId = "~effect/encoding/Zip/ZipError" + +/** + * Error raised when a zip archive cannot be encoded or decoded. + * + * **Details** + * + * The `kind` field identifies the failure: + * + * - `InvalidHeader`: a header has an unknown signature or a malformed field, or + * an entry has a value that cannot be written. + * - `InvalidData`: the compressed data of an entry is corrupt. + * - `Truncated`: the input ended inside an entry or before the central + * directory. + * - `Checksum`: the data of an entry does not match its CRC-32. + * - `SizeMismatch`: an entry body produced a byte count other than its declared + * `size`, or the data of an entry does not match the sizes in the archive. + * - `Unsupported`: the archive uses a feature this module does not read, such + * as encryption or a compression method other than store and deflate. + * - `BodyConsumed`: an entry body was read after the decoder had moved past it. + * + * @stability experimental + * @category errors + * @since 4.0.6 + */ +export class ZipError extends Data.TaggedError("ZipError")<{ + readonly kind: + | "InvalidHeader" + | "InvalidData" + | "Truncated" + | "Checksum" + | "SizeMismatch" + | "Unsupported" + | "BodyConsumed" + readonly message: string +}> { + /** + * Marks this value as a zip error for runtime guards. + * + * @stability experimental + * @since 4.0.6 + */ + readonly [ZipErrorTypeId] = ZipErrorTypeId +} + +/** + * Checks whether a value is a `ZipError`. + * + * @stability experimental + * @category guards + * @since 4.0.6 + */ +export const isZipError = (u: unknown): u is ZipError => Predicate.hasProperty(u, ZipErrorTypeId) + +/** + * Kind of object described by a zip entry. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export type EntryType = "File" | "Directory" + +/** + * Compression method of a zip entry. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export type Method = "Store" | "Deflate" + +/** + * Metadata of a zip entry. + * + * **Details** + * + * `path` is the archive path, with `/` separators; directory paths end with + * `/`. `size` is the uncompressed byte length when it is known before the body: + * decoded entries whose sizes follow their data have no `size`. `mode` holds + * Unix permission bits and is written to the central directory, which `decode` + * does not read, so decoded headers have no `mode`. `mtime` has a resolution of + * one second when the archive has an extended timestamp and two seconds + * otherwise. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Header { + readonly path: string + readonly type: EntryType + readonly method: Method + readonly mtime: Date + readonly size?: number | undefined + readonly mode?: number | undefined +} + +/** + * Zip entry: a header and a stream of the uncompressed body bytes. + * + * **Details** + * + * `decode` returns these, and `fileStream` builds them. `encode` writes the + * sizes and CRC-32 of a streamed body in a data descriptor after the data. + * + * @see {@link ContentEntry} for entries whose content is held in memory + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface Entry { + readonly header: Header + readonly body: Stream.Stream + readonly content?: undefined +} + +/** + * Zip entry whose uncompressed content is held in memory. + * + * **Details** + * + * `file` and `directory` build these. Because the content is known before the + * entry is written, `encode` puts its sizes and CRC-32 in the local header and + * writes no data descriptor, which every zip reader accepts. `content` and + * `body` exclude each other, so an entry whose body is replaced must drop + * `content`. + * + * @see {@link Entry} for entries whose content is streamed + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface ContentEntry { + readonly header: Header + readonly content: Uint8Array + readonly body?: undefined +} + +/** + * Options for the entry constructors. + * + * **Details** + * + * Unset fields default to an mtime of 1980-01-01T00:00:00Z, the earliest time + * a zip header can hold, and mode `0o644` (`0o755` for directories). The + * defaults make archives reproducible. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface EntryOptions { + readonly mtime?: Date | undefined + readonly mode?: number | undefined +} + +/** + * Options for file entries. + * + * **Details** + * + * `method` defaults to `"Deflate"`. Use `"Store"` for content that is already + * compressed, such as images or archives. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export interface FileOptions extends EntryOptions { + readonly method?: Method | undefined +} + +/** + * Options for file entries whose content is streamed. + * + * **Details** + * + * `size` is the exact byte length of the body. It is required for `Store`, + * because streaming readers can only find the end of stored data from its + * size, and optional for `Deflate`, where it is checked against the body. + * + * @stability experimental + * @category models + * @since 4.0.6 + */ +export type FileStreamOptions = + & EntryOptions + & ( + | { readonly method?: "Deflate" | undefined; readonly size?: number | undefined } + | { readonly method: "Store"; readonly size: number } + ) + +const MIN_MTIME = Date.UTC(1980, 0, 1) + +const makeHeader = ( + path: string, + type: EntryType, + method: Method, + options: EntryOptions | undefined, + size: number | undefined +): Header => ({ + path, + type, + method, + mtime: options?.mtime ?? new Date(MIN_MTIME), + mode: options?.mode ?? (type === "Directory" ? 0o755 : 0o644), + ...(size === undefined ? undefined : { size }) +}) + +/** + * Creates a file entry from in-memory content. + * + * **Details** + * + * String content is encoded as UTF-8, and `size` is the byte length of the + * content. Because the content is known up front, its sizes and CRC-32 are + * written in the local header, which every zip reader accepts. + * + * @see {@link fileStream} for content that is streamed instead of held in memory + * @see {@link encode} for writing entries to an archive + * + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const file = (path: string, content: string | Uint8Array, options?: FileOptions): ContentEntry => { + const bytes = typeof content === "string" ? textEncoder.encode(content) : content + return { header: makeHeader(path, "File", options?.method ?? "Deflate", options, bytes.length), content: bytes } +} + +/** + * Creates a file entry whose content is streamed from `body`. + * + * **When to use** + * + * Use when the content is too large to hold in memory, or is read from a file + * or another stream. + * + * **Details** + * + * `body` is not run until the entry is encoded. Its sizes and CRC-32 are + * written in a data descriptor after the data. + * + * **Gotchas** + * + * - A stored entry without `size` fails with an `InvalidHeader` `ZipError`. + * - When `size` is set, `encode` fails with a `SizeMismatch` `ZipError` if the + * body produces fewer or more bytes. + * - Non-empty stored entries are followed by a data descriptor, which some + * streaming readers mishandle. Java's `ZipInputStream` rejects them, and + * libarchive reading from a pipe (as `bsdtar` does) ends them at the first + * bytes that look like a descriptor, so content can cut the entry short and + * add entries. Use `Deflate` or an in-memory `file` for untrusted content + * and for those readers. + * + * @see {@link file} for in-memory content + * @see {@link encode} for writing entries to an archive + * + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const fileStream = ( + path: string, + body: Stream.Stream, + options?: FileStreamOptions +): Entry => ({ + header: makeHeader(path, "File", options?.method ?? "Deflate", options, options?.size), + body +}) + +/** + * Creates a directory entry. + * + * **Details** + * + * A trailing `/` is added to a non-empty `path` when missing, as zip tools + * expect for directories. + * + * @stability experimental + * @category constructors + * @since 4.0.6 + */ +export const directory = (path: string, options?: EntryOptions): ContentEntry => ({ + // An empty path stays empty, so that `encode` rejects it + header: makeHeader(path === "" || path.endsWith("/") ? path : `${path}/`, "Directory", "Store", options, 0), + content: emptyBody +}) + +/** + * Creates a channel that decodes zip archive bytes into entries. + * + * **Details** + * + * Entries are read in archive order from their local headers, so archives can + * be decoded as they arrive, for example from an HTTP response. Each output + * chunk holds one entry, and each body streams the uncompressed bytes of the + * entry. Pulling the next entry skips whatever is left of the current body. + * Stored and deflated entries are read, including ZIP64 sizes and deflated + * entries whose sizes follow their data in a data descriptor. Every body is + * checked against its CRC-32 and sizes. Reading stops at the central directory. + * + * Paths use the UTF-8 flag when set, and otherwise the first Info-ZIP Unicode + * path field when it matches the name, as Info-ZIP and 7-Zip do; other paths + * are read as UTF-8 when valid and as CP437 otherwise. Times are read from the + * extended timestamp field when present, and otherwise from the DOS time and + * date, which have no time zone and are read as UTC. + * + * **Gotchas** + * + * - Read each body before pulling the next entry: a body read afterwards fails + * with a `BodyConsumed` `ZipError`. Operators that pull ahead, such as + * `Stream.buffer`, therefore can't sit between `decode` and the body reads. + * - Read bodies while the entry stream runs, for example in `Stream.mapEffect`: + * once it has ended, as after `Stream.runCollect`, `Stream.runHead` or + * `Stream.take`, every body fails with a `BodyConsumed` `ZipError`. + * - A body reads the entry's bytes once: running it again continues where the + * previous run stopped. + * - The central directory is not read, so `mode`, comments and entries that + * only the central directory lists are not reported. Tools that read the + * central directory can see other entries and paths, for example in + * archives updated by appending. + * - Archives that do not start with an entry or a central directory, such as + * self-extracting archives, fail with an `InvalidHeader` `ZipError`. + * - Stored entries whose sizes only follow their data fail with an + * `Unsupported` `ZipError`. Python's `zipfile` writes them for stored + * entries on unseekable output, and Info-ZIP's `zip` for empty files when + * writing to a pipe. Searching for their end would let the content of an + * entry forge the entries after it. + * - A body that grows past its `size` fails with a `SizeMismatch` `ZipError` + * before the extra bytes are emitted, so `size` can be checked before reading. + * - Deflated data can expand about a thousand times, and pulling the next + * entry inflates the rest of the current body to find its end. To bound the + * work on untrusted archives, check `size`, count the bytes of bodies + * without one, and end the stream at an entry that is too large rather than + * skip it. + * - Every `ZipError` ends the archive: later entry and body reads fail with + * the same error. + * - The channel completes only when its input ends: input after the central + * directory is read and discarded, so that the upstream done value can be + * passed on. + * - Paths are returned as written in the archive: they can be absolute, + * contain `..`, drive letters, control characters or backslashes, which some + * tools read as separators, and can repeat. Validate them before writing + * entries to disk; `encode` rejects absolute paths, drive letters and + * backslashes. Paths with a NUL character, which other tools take as the end + * of the name, fail with an `InvalidHeader` `ZipError`. + * + * @see {@link encode} for the inverse operation + * + * @stability experimental + * @category decoding + * @since 4.0.6 + */ +export const decode = (): Channel.Channel< + Arr.NonEmptyReadonlyArray>, + ZipError | IE, + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done +> => + Channel.fromTransform((upstream, scope) => + Effect.suspend(() => { + const decoder = new Decoder() + let done: { readonly value: Done } | undefined + let pulls = 0 + + const pullUpstream = upstream.pipe( + Effect.map((chunks) => { + pulls++ + for (const chunk of chunks) decoder.push(chunk) + }), + Pull.catchDone((value) => { + pulls++ + done = { value: value as Done } + decoder.end() + return Effect.void + }) + ) + + // Serialize entry and body pulls; reuse input pulled while waiting for the lock. + const lock = Semaphore.makeUnsafe(1) + const pullInput = (seen: number) => + lock.withPermit(Effect.suspend(() => pulls === seen ? pullUpstream : Effect.void)) + + const run = (f: () => A | undefined | typeof NeedInput, end: () => D): Pull.Pull => + Effect.suspend(function loop(): Pull.Pull { + const seen = pulls + let result: A | undefined | typeof NeedInput + try { + result = f() + } catch (error) { + return isZipError(error) ? Effect.fail(error) : Effect.die(error) + } + if (result === NeedInput) return Effect.flatMap(pullInput(seen), loop) + return result === undefined ? Cause.done(end()) : Effect.succeed(result) + }) + + const body = (id: number): Stream.Stream => + Stream.fromPull(Effect.succeed(run(() => decoder.read(id), () => undefined))) + + const next = run(() => { + const header = decoder.next() + return header === NeedInput || header === undefined + ? header + : Arr.of({ header, body: body(decoder.generation) }) + }, () => done!.value) + + return Effect.as(Scope.addFinalizer(scope, Effect.sync(() => decoder.close())), next) + }) + ) + +/** + * Creates a channel that encodes entries into zip archive bytes. + * + * **Details** + * + * Each entry is written as soon as it arrives: a local header, the stored or + * deflated body and, for entries with a streamed `body`, a data descriptor + * with the CRC-32 and sizes. A `ContentEntry` has them in its local header. + * The central directory is written once the input ends. Paths are written as + * UTF-8, with the UTF-8 flag set when they are not ASCII. `mtime` + * is written both as a DOS time in UTC, which keeps two-second resolution and + * covers 1980 to 2107, and as an extended timestamp in whole seconds when it + * falls between 1970 and 2038. ZIP64 fields are added when a size, an offset + * or the entry count does not fit the classic fields, and to the local header + * of a deflated entry streamed without a `size` or with one near 4 GiB, so + * that readers know the width of the sizes after its data. + * + * Headers and body chunks below 16 KiB are gathered into output chunks of + * about 64 KiB, and larger body chunks pass through without copying. Output is + * handed out before waiting for more entries, but up to 64 KiB of it can be + * held while a body is pulled. Each body chunk of a streamed `Deflate` entry is + * a separate compressor job, so larger chunks compress faster. A `Deflate` + * body with a `size` of at most 64 KiB is instead collected and compressed in + * one job, and still gets a data descriptor; omit `size` to stream it. + * + * **Gotchas** + * + * - Deflated entries need `CompressionStream("deflate-raw")`. On a runtime + * without it, encoding a deflated entry dies with the runtime's error, as + * it does when the compressor fails: check for the API or use `Store`. + * - Deflated bytes come from the runtime's `CompressionStream`, so they can + * differ between runtimes. Archives of stored entries are byte-identical + * everywhere. + * - A body that produces a byte count other than a set `header.size`, or any + * bytes for a directory, fails with a `SizeMismatch` `ZipError`. + * - Directories are always stored, whatever their `method`. + * - Empty paths, paths starting with `/` or a drive letter such as `C:`, paths + * containing `\` or NUL, `File` paths ending with `/`, `Directory` paths not + * ending with `/`, and invalid modes, sizes or times fail with an + * `InvalidHeader` `ZipError`. Other paths are written as given, including + * `..` segments, so validate untrusted paths before encoding them. + * + * **Example** (Writing and reading an archive) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * import { Zip } from "effect/encoding" + * + * const archive = Stream.make( + * Zip.directory("docs"), + * Zip.file("docs/readme.txt", "Hello, zip!") + * ).pipe(Stream.pipeThroughChannel(Zip.encode())) + * + * const listing = archive.pipe( + * Stream.pipeThroughChannel(Zip.decode()), + * Stream.mapEffect((entry) => + * Stream.mkString(Stream.decodeText(entry.body)).pipe( + * Effect.map((text) => `${entry.header.path} ${JSON.stringify(text)}`) + * ) + * ), + * Stream.runCollect + * ) + * await Effect.runPromise(listing) // => ['docs/ ""', 'docs/readme.txt "Hello, zip!"'] + * ``` + * + * @see {@link decode} for the inverse operation + * @see {@link file} for building entries from in-memory content + * @see {@link fileStream} for building entries from streamed content + * + * @stability experimental + * @category encoding + * @since 4.0.6 + */ +export const encode = < + IE = never, + Done = unknown, + In extends Entry | ContentEntry = Entry | ContentEntry +>(): Channel.Channel< + Arr.NonEmptyReadonlyArray, + ZipError | IE | (In extends Entry ? E : never), + Done, + Arr.NonEmptyReadonlyArray, + IE, + Done, + In extends Entry ? R : never +> => { + // Distribute over entry unions to infer body errors and services. + type E = In extends Entry ? E : never + type R = In extends Entry ? R : never + return Channel.fromTransform((upstream: Pull.Pull, IE, Done>, scope) => + Effect.sync(() => { + const writer = new Writer() + let entries: ReadonlyArray | ContentEntry> = [] + let index = 0 + let done: { readonly value: Done } | undefined + let body: Pull.Pull, E | ZipError, void, R> | undefined + let bodyScope: Scope.Closeable | undefined + // In-memory entries are deflated ahead of the one being written, so the + // deflates overlap + let deflates: Array = [] + + const startDeflates = (from: number): void => { + for (let i = from; i < Math.min(entries.length, from + DEFLATE_AHEAD); i++) { + if (deflates[i] !== undefined) continue + const { content, header } = entries[i] + if (header.type !== "File" || header.method !== "Deflate" || content === undefined) continue + if (i > from && content.length > DEFLATE_AHEAD_SIZE) continue + const deflating: Deflating = { promise: deflateBytes(content), data: undefined } + // Failures surface when the entry is written; deflates left behind by + // a failed encode finish on their own + deflating.promise.then((data) => { + deflating.data = data + }, () => {}) + deflates[i] = deflating + } + } + + const write = (f: () => void): Effect.Effect => { + try { + f() + } catch (error) { + return isZipError(error) ? Effect.fail(error) : Effect.die(error) + } + return Effect.void + } + + return Effect.gen(function*() { + while (true) { + if (writer.buffered >= SLAB_SIZE) return writer.flush() + if (body !== undefined) { + const chunks = yield* Pull.catchDone(body, () => Effect.undefined) + if (chunks !== undefined) { + yield* write(() => writer.data(chunks)) + continue + } + body = undefined + yield* Scope.close(bodyScope!, Exit.void) + yield* write(() => writer.end()) + continue + } + if (index < entries.length) { + const entry = entries[index++] + const { header } = entry + yield* write(() => validate(header)) + const compress = header.type === "File" && header.method === "Deflate" + if (entry.content !== undefined) { + const bytes = entry.content + let data = bytes + if (compress) { + startDeflates(index - 1) + const deflating = deflates[index - 1]! + deflates[index - 1] = undefined + data = deflating.data ?? (yield* Effect.promise(() => deflating.promise)) + } + yield* write(() => writer.entry(header, bytes, data)) + continue + } + yield* write(() => writer.begin(header)) + if (compress && header.size !== undefined && header.size <= DEFLATE_AHEAD_SIZE) { + // A small body costs less to deflate in one piece than as a stream + const size = header.size + const chunks: Array = [] + let length = 0 + yield* Stream.runForEachArray(entry.body, (array) => + write(() => { + for (const chunk of array) { + chunks.push(chunk) + length += chunk.length + } + if (length > size) throw sizeMismatch(header.path, length, size) + writer.update(array) + })) + const data = yield* Effect.promise(() => deflateBytes(concat(chunks, length))) + yield* write(() => { + writer.data([data]) + writer.end() + }) + continue + } + const raw = Stream.mapArray(entry.body, (chunks) => { + writer.update(chunks) + return chunks + }) + bodyScope = Scope.forkUnsafe(scope) + body = yield* Channel.toPullScoped((compress ? deflate(raw) : raw).channel, bodyScope) + continue + } + if (writer.buffered > 0) return writer.flush() + if (done !== undefined) return yield* Cause.done(done.value) + const next = yield* Pull.catchDone(upstream, (value) => { + done = { value: value as Done } + return Effect.undefined + }) + if (next === undefined) { + writer.finish() + continue + } + entries = next + index = 0 + deflates = [] + } + }) + }) + ) +} + +// Compressor failures, including a runtime without `deflate-raw`, are defects +const deflate = (input: Stream.Stream): Stream.Stream => + Stream.unwrap(Effect.map( + Effect.sync(() => new CompressionStream("deflate-raw")), + (compressor) => { + const writer = compressor.writable.getWriter() + const feed = Stream.runForEach( + input, + (chunk) => Effect.promise(() => writer.write(compressible(chunk))) + ).pipe( + Effect.andThen(Effect.promise(() => writer.close())), + Effect.onError(() => + Effect.sync(() => { + writer.abort().catch(() => {}) + }) + ) + ) + const output = Stream.orDie(Stream.fromReadableStream({ + evaluate: () => compressor.readable, + onError: (cause) => cause + })) + return Stream.merge(output, Stream.fromEffectDrain(feed)) + } + )) + +// CompressionStream rejects views of a SharedArrayBuffer +const compressible = (bytes: Uint8Array): Uint8Array => + bytes.buffer instanceof ArrayBuffer ? bytes as Uint8Array : bytes.slice() + +// How many in-memory entries are deflated at once, and the largest one that +// is deflated before its turn +const DEFLATE_AHEAD = 16 +const DEFLATE_AHEAD_SIZE = 64 * 1024 + +// A deflate started ahead, with its data once it is done +interface Deflating { + readonly promise: Promise + data: Uint8Array | undefined +} + +// One promise per in-memory entry costs less than a stream +const deflateBytes = async (bytes: Uint8Array): Promise => { + const compressor = new CompressionStream("deflate-raw") + const writer = compressor.writable.getWriter() + // A failed write errors the readable, so the reads report it + if (bytes.length > 0) writer.write(compressible(bytes)).catch(() => {}) + writer.close().catch(() => {}) + const reader = compressor.readable.getReader() + const chunks: Array = [] + let length = 0 + while (true) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + length += value.length + } + return concat(chunks, length) +} + +const concat = (chunks: ReadonlyArray, length: number): Uint8Array => { + if (chunks.length === 1) return chunks[0] + const out = new Uint8Array(length) + let at = 0 + for (const chunk of chunks) { + out.set(chunk, at) + at += chunk.length + } + return out +} + +const invalidHeader = (message: string) => new ZipError({ kind: "InvalidHeader", message }) + +const invalidData = (message: string) => new ZipError({ kind: "InvalidData", message }) + +const truncated = (at: number) => + new ZipError({ kind: "Truncated", message: `unexpected end of zip archive at byte ${at}` }) + +const unsupported = (message: string) => new ZipError({ kind: "Unsupported", message }) + +const sizeMismatch = (path: string, actual: number, expected: number) => + new ZipError({ + kind: "SizeMismatch", + message: `body of ${JSON.stringify(path)} has ${ + actual > expected ? "at least" : "only" + } ${actual} bytes, but its size is ${expected}` + }) + +const LOCAL = 0x04034b50 +const CENTRAL = 0x02014b50 +const DESCRIPTOR = 0x08074b50 +const END = 0x06054b50 +const END64 = 0x06064b50 +const LOCATOR64 = 0x07064b50 + +const FLAG_ENCRYPTED = 0x0001 +const FLAG_DESCRIPTOR = 0x0008 +const FLAG_STRONG_ENCRYPTION = 0x0040 +const FLAG_UTF8 = 0x0800 + +const EXTRA_ZIP64 = 0x0001 +const EXTRA_TIMESTAMP = 0x5455 +const EXTRA_UNICODE_PATH = 0x7075 + +const VERSION = 20 +const VERSION_ZIP64 = 45 +const CREATOR_UNIX = 3 + +const MAX16 = 0xffff +const MAX32 = 0xffffffff + +const S_IFDIR = 0o040000 +const S_IFREG = 0o100000 +const DOS_DIRECTORY = 0x10 + +const textEncoder = new TextEncoder() +// Paths keep a leading byte order mark, as other zip tools do +const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true }) +const strictTextDecoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }) + +const emptyBody = new Uint8Array(0) + +// May exceed 2^53 - 1; callers check or compare +const readUint64 = (view: DataView, at: number): number => + view.getUint32(at, true) + view.getUint32(at + 4, true) * 0x100000000 + +const getUint64 = (view: DataView, at: number): number => { + const value = readUint64(view, at) + if (!Number.isSafeInteger(value)) throw invalidHeader("a 64-bit field exceeds 2^53 - 1") + return value +} + +const setUint64 = (view: DataView, at: number, value: number): void => { + view.setUint32(at, value % 0x100000000, true) + view.setUint32(at + 4, Math.floor(value / 0x100000000), true) +} + +// DOS times have two-second resolution and cover 1980 to 2107 +const toDos = (mtime: Date): { readonly time: number; readonly date: number } => { + const millis = mtime.getTime() + if (millis < MIN_MTIME) return { time: 0, date: (1 << 5) | 1 } + const date = new Date(millis) + const year = date.getUTCFullYear() + if (year > 2107) return { time: (23 << 11) | (59 << 5) | 29, date: (127 << 9) | (12 << 5) | 31 } + return { + time: (date.getUTCHours() << 11) | (date.getUTCMinutes() << 5) | (date.getUTCSeconds() >> 1), + date: ((year - 1980) << 9) | ((date.getUTCMonth() + 1) << 5) | date.getUTCDate() + } +} + +const fromDos = (time: number, date: number): number => + Date.UTC( + (date >> 9) + 1980, + ((date >> 5) & 0xf) - 1, + date & 0x1f, + time >> 11, + (time >> 5) & 0x3f, + (time & 0x1f) * 2 + ) + +const CP437 = "ÇüéâäàåçêëèïîìÄÅ" + + "ÉæÆôöòûùÿÖÜ¢£¥₧ƒ" + + "áíóúñѪº¿⌐¬½¼¡«»" + + "░▒▓│┤╡╢╖╕╣║╗╝╜╛┐" + + "└┴┬├─┼╞╟╚╔╩╦╠═╬╧" + + "╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀" + + "αßΓπΣσµτΦΘΩδ∞φε∩" + + "≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ " + +const decodeName = (bytes: Uint8Array, utf8: boolean): string => { + if (utf8) return textDecoder.decode(bytes) + try { + return strictTextDecoder.decode(bytes) + } catch { + let name = "" + for (let i = 0; i < bytes.length; i++) { + name += bytes[i] < 0x80 ? String.fromCharCode(bytes[i]) : CP437[bytes[i] - 0x80] + } + return name + } +} + +// CRC-32 (IEEE) lookup tables for sixteen bytes at a time +const CRC_TABLE = (() => { + const table = new Int32Array(16 * 256) + for (let n = 0; n < 256; n++) { + let c = n + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 + table[n] = c + } + for (let n = 0; n < 256; n++) { + let c = table[n] + for (let k = 1; k < 16; k++) { + c = table[c & 0xff] ^ (c >>> 8) + table[k * 256 + n] = c + } + } + return table +})() + +const littleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1 + +const crc32 = (crc: number, bytes: Uint8Array): number => { + const t = CRC_TABLE + let c = ~crc + let i = 0 + const length = bytes.length + // Reading whole words is faster than combining bytes, but needs aligned, + // little-endian words + if (littleEndian && length >= 64) { + for (; ((bytes.byteOffset + i) & 3) !== 0; i++) c = t[(c ^ bytes[i]) & 0xff] ^ (c >>> 8) + const count = ((length - i) >>> 4) << 2 + const words = new Int32Array(bytes.buffer, bytes.byteOffset + i, count) + for (let j = 0; j < count; j += 4) { + const a = c ^ words[j] + const b = words[j + 1] + const d = words[j + 2] + const e = words[j + 3] + c = t[3840 + (a & 0xff)] ^ t[3584 + ((a >>> 8) & 0xff)] ^ t[3328 + ((a >>> 16) & 0xff)] ^ + t[3072 + (a >>> 24)] ^ t[2816 + (b & 0xff)] ^ t[2560 + ((b >>> 8) & 0xff)] ^ + t[2304 + ((b >>> 16) & 0xff)] ^ t[2048 + (b >>> 24)] ^ t[1792 + (d & 0xff)] ^ + t[1536 + ((d >>> 8) & 0xff)] ^ t[1280 + ((d >>> 16) & 0xff)] ^ t[1024 + (d >>> 24)] ^ + t[768 + (e & 0xff)] ^ t[512 + ((e >>> 8) & 0xff)] ^ t[256 + ((e >>> 16) & 0xff)] ^ t[e >>> 24] + } + i += count * 4 + } + for (; i + 16 <= length; i += 16) { + const a = c ^ (bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24)) + c = t[3840 + (a & 0xff)] ^ t[3584 + ((a >>> 8) & 0xff)] ^ t[3328 + ((a >>> 16) & 0xff)] ^ + t[3072 + (a >>> 24)] ^ t[2816 + bytes[i + 4]] ^ t[2560 + bytes[i + 5]] ^ t[2304 + bytes[i + 6]] ^ + t[2048 + bytes[i + 7]] ^ t[1792 + bytes[i + 8]] ^ t[1536 + bytes[i + 9]] ^ t[1280 + bytes[i + 10]] ^ + t[1024 + bytes[i + 11]] ^ t[768 + bytes[i + 12]] ^ t[512 + bytes[i + 13]] ^ t[256 + bytes[i + 14]] ^ + t[bytes[i + 15]] + } + for (; i < length; i++) c = t[(c ^ bytes[i]) & 0xff] ^ (c >>> 8) + return ~c >>> 0 +} + +const validate = (header: Header): void => { + const { method, mode, mtime, path, size, type } = header + if (path === "") throw invalidHeader("path is empty") + const quoted = JSON.stringify(path) + if (path.startsWith("/")) throw invalidHeader(`path ${quoted} is absolute`) + // The zip format forbids these, and libarchive reads them as separators and roots + if (path.includes("\\")) throw invalidHeader(`path ${quoted} contains a backslash`) + if (/^[A-Za-z]:/.test(path)) throw invalidHeader(`path ${quoted} starts with a drive letter`) + if (path.includes("\0")) throw invalidHeader(`path ${quoted} contains a NUL character`) + if (type === "File") { + if (path.endsWith("/")) throw invalidHeader(`file path ${quoted} ends with "/"`) + if (method !== "Store" && method !== "Deflate") throw invalidHeader(`unknown method for ${quoted}`) + } else if (type === "Directory") { + if (!path.endsWith("/")) throw invalidHeader(`directory path ${quoted} does not end with "/"`) + if (size !== undefined && size !== 0) throw invalidHeader(`directory ${quoted} has a size`) + } else { + throw invalidHeader(`unknown entry type for ${quoted}`) + } + if (size !== undefined && !(Number.isSafeInteger(size) && size >= 0)) { + throw invalidHeader(`size of ${quoted} is not a non-negative safe integer`) + } + if (mode !== undefined && !(Number.isInteger(mode) && mode >= 0 && mode <= 0o7777)) { + throw invalidHeader(`mode of ${quoted} is not a permission mode`) + } + if (Number.isNaN(mtime.getTime())) { + throw invalidHeader(`mtime of ${quoted} is not a valid date`) + } +} + +interface EntryRecord { + readonly name: Uint8Array + readonly flags: number + readonly method: number + readonly time: number + readonly date: number + readonly seconds: number | undefined + readonly external: number + readonly offset: number + // Whether the local header has ZIP64 sizes, which then also follow the data + zip64: boolean + crc: number + compressedSize: number + size: number +} + +// An entry whose data is being written; `record` counts its bytes +interface Streaming { + readonly header: Header + readonly record: EntryRecord + readonly descriptor: boolean +} + +const timestampLength = (record: EntryRecord): number => record.seconds === undefined ? 0 : 9 + +const writeTimestamp = (bytes: Uint8Array, view: DataView, at: number, record: EntryRecord): void => { + if (record.seconds === undefined) return + view.setUint16(at, EXTRA_TIMESTAMP, true) + view.setUint16(at + 2, 5, true) + bytes[at + 4] = 1 + view.setUint32(at + 5, record.seconds, true) +} + +const localHeader = (record: EntryRecord): Uint8Array => { + const { zip64 } = record + const extraLength = (zip64 ? 20 : 0) + timestampLength(record) + const bytes = new Uint8Array(30 + record.name.length + extraLength) + const view = new DataView(bytes.buffer) + view.setUint32(0, LOCAL, true) + view.setUint16(4, zip64 ? VERSION_ZIP64 : VERSION, true) + view.setUint16(6, record.flags, true) + view.setUint16(8, record.method, true) + view.setUint16(10, record.time, true) + view.setUint16(12, record.date, true) + view.setUint32(14, record.crc, true) + view.setUint32(18, zip64 ? MAX32 : record.compressedSize, true) + view.setUint32(22, zip64 ? MAX32 : record.size, true) + view.setUint16(26, record.name.length, true) + view.setUint16(28, extraLength, true) + bytes.set(record.name, 30) + let at = 30 + record.name.length + if (zip64) { + view.setUint16(at, EXTRA_ZIP64, true) + view.setUint16(at + 2, 16, true) + setUint64(view, at + 4, record.size) + setUint64(view, at + 12, record.compressedSize) + at += 20 + } + writeTimestamp(bytes, view, at, record) + return bytes +} + +const dataDescriptor = (record: EntryRecord): Uint8Array => { + const wide = record.zip64 + const bytes = new Uint8Array(wide ? 24 : 16) + const view = new DataView(bytes.buffer) + view.setUint32(0, DESCRIPTOR, true) + view.setUint32(4, record.crc, true) + if (wide) { + setUint64(view, 8, record.compressedSize) + setUint64(view, 16, record.size) + } else { + view.setUint32(8, record.compressedSize, true) + view.setUint32(12, record.size, true) + } + return bytes +} + +const centralHeader = (record: EntryRecord): Uint8Array => { + // ZIP64 holds only the fields that overflow, in this order + const wide: Array = [] + if (record.size >= MAX32) wide.push(record.size) + if (record.compressedSize >= MAX32) wide.push(record.compressedSize) + if (record.offset >= MAX32) wide.push(record.offset) + const zip64Length = wide.length === 0 ? 0 : 4 + 8 * wide.length + const extraLength = zip64Length + timestampLength(record) + const version = wide.length === 0 && !record.zip64 ? VERSION : VERSION_ZIP64 + const bytes = new Uint8Array(46 + record.name.length + extraLength) + const view = new DataView(bytes.buffer) + view.setUint32(0, CENTRAL, true) + view.setUint16(4, (CREATOR_UNIX << 8) | version, true) + view.setUint16(6, version, true) + view.setUint16(8, record.flags, true) + view.setUint16(10, record.method, true) + view.setUint16(12, record.time, true) + view.setUint16(14, record.date, true) + view.setUint32(16, record.crc, true) + view.setUint32(20, Math.min(record.compressedSize, MAX32), true) + view.setUint32(24, Math.min(record.size, MAX32), true) + view.setUint16(28, record.name.length, true) + view.setUint16(30, extraLength, true) + view.setUint32(38, record.external, true) + view.setUint32(42, Math.min(record.offset, MAX32), true) + bytes.set(record.name, 46) + let at = 46 + record.name.length + if (wide.length > 0) { + view.setUint16(at, EXTRA_ZIP64, true) + view.setUint16(at + 2, 8 * wide.length, true) + for (let i = 0; i < wide.length; i++) setUint64(view, at + 4 + 8 * i, wide[i]) + at += zip64Length + } + writeTimestamp(bytes, view, at, record) + return bytes +} + +const SLAB_SIZE = 64 * 1024 + +// Room for deflate to grow data that does not compress, when choosing ZIP64 +const DEFLATE_HEADROOM = 16 * 1024 * 1024 + +// Chunks below this size are copied together instead of being handed out alone +const COPY_LIMIT = 16 * 1024 + +class Writer { + private chunks: Array = [] + buffered = 0 + private offset = 0 + private readonly records: Array = [] + private current: Streaming | undefined + + // Hands out the buffered output, joining runs of small chunks; needs `buffered > 0` + flush(): Arr.NonEmptyArray { + const out: Array = [] + let run: Array = [] + let runLength = 0 + const join = () => { + if (run.length === 1) { + out.push(run[0]) + } else if (run.length > 1) { + const joined = new Uint8Array(runLength) + let at = 0 + for (const chunk of run) { + joined.set(chunk, at) + at += chunk.length + } + out.push(joined) + } + run = [] + runLength = 0 + } + for (const chunk of this.chunks) { + if (chunk.length >= COPY_LIMIT) { + join() + out.push(chunk) + } else { + run.push(chunk) + runLength += chunk.length + } + } + join() + this.chunks = [] + this.buffered = 0 + return out as Arr.NonEmptyArray + } + + private push(bytes: Uint8Array): void { + if (bytes.length === 0) return + this.chunks.push(bytes) + this.offset += bytes.length + this.buffered += bytes.length + } + + private record(header: Header, flags: number): EntryRecord { + const name = textEncoder.encode(header.path) + if (name.length > MAX16) throw invalidHeader(`path ${JSON.stringify(header.path)} is longer than 65535 bytes`) + const { date, time } = toDos(header.mtime) + const seconds = Math.floor(header.mtime.getTime() / 1000) + const directory = header.type === "Directory" + const mode = header.mode ?? (directory ? 0o755 : 0o644) + return { + name, + // The UTF-8 byte length equals the string length only for ASCII + flags: name.length === header.path.length ? flags : flags | FLAG_UTF8, + method: header.type === "File" && header.method === "Deflate" ? 8 : 0, + time, + date, + seconds: seconds >= 0 && seconds <= 0x7fffffff ? seconds : undefined, + external: (((directory ? S_IFDIR : S_IFREG) | mode) * 0x10000 + (directory ? DOS_DIRECTORY : 0)) >>> 0, + offset: this.offset, + zip64: false, + crc: 0, + compressedSize: 0, + size: 0 + } + } + + // Writes an entry whose content is known, with its sizes in the local header + entry(header: Header, content: Uint8Array, data: Uint8Array): void { + const size = header.type === "Directory" ? 0 : header.size + if (size !== undefined && size !== content.length) throw sizeMismatch(header.path, content.length, size) + const record = this.record(header, 0) + record.crc = crc32(0, content) + record.size = content.length + record.compressedSize = data.length + record.zip64 = record.size >= MAX32 || record.compressedSize >= MAX32 + this.push(localHeader(record)) + if (data.length > 0) this.push(data) + this.records.push(record) + } + + // Starts an entry whose sizes and CRC-32 follow its data + begin(header: Header): void { + const directory = header.type === "Directory" + const stored = directory || header.method === "Store" + const size = directory ? 0 : header.size + if (stored && size === undefined) { + throw invalidHeader(`stored file ${JSON.stringify(header.path)} is streamed without a size`) + } + // An empty stored entry needs no descriptor: readers would take its zero + // sizes for missing ones + const descriptor = !(stored && size === 0) + const record = this.record(header, descriptor ? FLAG_DESCRIPTOR : 0) + // Streaming readers find the end of stored data from its size + if (stored) { + record.size = size! + record.compressedSize = size! + } + // The local header fixes the width of the sizes after the data, so declare + // ZIP64 whenever they could reach 4 GiB + record.zip64 = size === undefined || size >= (stored ? MAX32 : MAX32 - DEFLATE_HEADROOM) + this.push(localHeader(record)) + record.size = 0 + record.compressedSize = 0 + this.current = { header, record, descriptor } + } + + // Accounts for uncompressed body bytes + update(chunks: ReadonlyArray): void { + const { record } = this.current! + for (const chunk of chunks) { + record.crc = crc32(record.crc, chunk) + record.size += chunk.length + } + } + + // Writes stored or compressed body bytes + data(chunks: ReadonlyArray): void { + const { header, record } = this.current! + for (const chunk of chunks) { + this.push(chunk) + record.compressedSize += chunk.length + } + const size = header.type === "Directory" ? 0 : header.size + if (size !== undefined && record.size > size) throw sizeMismatch(header.path, record.size, size) + if (!record.zip64 && record.compressedSize >= MAX32) { + throw new ZipError({ + kind: "SizeMismatch", + message: `compressed data of ${JSON.stringify(header.path)} reached 4 GiB, ` + + `but its local header has no ZIP64 sizes` + }) + } + } + + end(): void { + const { descriptor, header, record } = this.current! + this.current = undefined + if (header.size !== undefined && record.size !== header.size) { + throw sizeMismatch(header.path, record.size, header.size) + } + if (descriptor) this.push(dataDescriptor(record)) + this.records.push(record) + } + + finish(): void { + const start = this.offset + for (const record of this.records) this.push(centralHeader(record)) + const size = this.offset - start + const count = this.records.length + if (count >= MAX16 || size >= MAX32 || start >= MAX32) { + const end64 = new Uint8Array(56) + const view = new DataView(end64.buffer) + view.setUint32(0, END64, true) + setUint64(view, 4, 44) + view.setUint16(12, (CREATOR_UNIX << 8) | VERSION_ZIP64, true) + view.setUint16(14, VERSION_ZIP64, true) + setUint64(view, 24, count) + setUint64(view, 32, count) + setUint64(view, 40, size) + setUint64(view, 48, start) + const locator = new Uint8Array(20) + const locatorView = new DataView(locator.buffer) + locatorView.setUint32(0, LOCATOR64, true) + setUint64(locatorView, 8, this.offset) + locatorView.setUint32(16, 1, true) + this.push(end64) + this.push(locator) + } + const end = new Uint8Array(22) + const view = new DataView(end.buffer) + view.setUint32(0, END, true) + view.setUint16(8, Math.min(count, MAX16), true) + view.setUint16(10, Math.min(count, MAX16), true) + view.setUint32(12, Math.min(size, MAX32), true) + view.setUint32(16, Math.min(start, MAX32), true) + this.push(end) + } +} + +interface Current { + readonly header: Header + readonly inflater: Inflater | undefined + readonly descriptor: boolean + readonly zip64: boolean + // The values of the local header; sizes are unknown when they follow the data + readonly crc: number + readonly compressedSize: number | undefined + readonly size: number | undefined + // Queue position where the data starts + readonly start: number + remaining: number + dataDone: boolean + actualCrc: number + actualSize: number + actualCompressedSize: number +} + +// `next` skips the rest of the current body; `read` consumes it. Both preserve +// progress across `NeedInput` retries. +class Decoder { + private readonly queue = new ByteQueue() + private ended = false + private finished = false + private closed = false + // Incremented whenever the current body is invalidated + generation = 0 + private current: Current | undefined + private inflateMemory: InflateMemory | undefined + // The error that ended the archive, repeated by every later call + private failure: ZipError | undefined + + push(chunk: Uint8Array): void { + if (!this.finished) this.queue.push(chunk) + } + + end(): void { + this.ended = true + } + + close(): void { + this.closed = true + this.generation++ + this.queue.clear() + } + + // Returns the next entry header, or `undefined` once the entries and the + // input have ended. Input after the entries is discarded. + next(): Header | undefined | typeof NeedInput { + this.generation++ + return this.guard(() => this.advance()) + } + + private guard(f: () => A): A { + if (this.failure !== undefined) throw this.failure + try { + return f() + } catch (error) { + if (isZipError(error)) this.failure = error + throw error + } + } + + private advance(): Header | undefined | typeof NeedInput { + while (this.current !== undefined) { + if (this.readCurrent() === NeedInput) return NeedInput + } + if (this.finished) return this.finish() + const queue = this.queue + if (queue.length < 4) return this.needInput() + const at = queue.consumed + const head = queue.peek(Math.min(queue.length, 30)) + const view = new DataView(head.buffer) + const signature = view.getUint32(0, true) + if (signature === CENTRAL || signature === END || signature === END64) return this.finish() + if (signature !== LOCAL) { + throw invalidHeader(`unexpected signature 0x${signature.toString(16).padStart(8, "0")} at byte ${at}`) + } + if (head.length < 30) return this.needInput() + const length = 30 + view.getUint16(26, true) + view.getUint16(28, true) + if (queue.length < length) return this.needInput() + try { + this.current = this.parseLocal(queue.take(length)) + } catch (error) { + if (isZipError(error)) { + throw new ZipError({ kind: error.kind, message: `${error.message} (header at byte ${at})` }) + } + throw error + } + return this.current.header + } + + // Returns the buffered body bytes of the entry from generation `id`, or + // `undefined` once the whole body has been read and checked + read(id: number): Arr.NonEmptyArray | undefined | typeof NeedInput { + if (id !== this.generation) { + throw new ZipError({ + kind: "BodyConsumed", + message: this.closed ? + "zip entry body read after the archive stream ended" : + "zip entry body read after moving to the next entry" + }) + } + return this.guard(() => this.readCurrent()) + } + + private readCurrent(): Arr.NonEmptyArray | undefined | typeof NeedInput { + const current = this.current + if (current === undefined) return undefined + const queue = this.queue + if (!current.dataDone) { + if (current.inflater === undefined) { + if (current.remaining > 0) { + if (queue.length === 0) return this.needInput() + const chunks: Array = [] + while (current.remaining > 0 && queue.length > 0) { + const chunk = queue.takeSome(current.remaining) + current.remaining -= chunk.length + current.actualCrc = crc32(current.actualCrc, chunk) + current.actualSize += chunk.length + chunks.push(chunk) + } + current.actualCompressedSize = current.actualSize + return chunks as Arr.NonEmptyArray + } + } else { + let chunk: Uint8Array | undefined | typeof NeedInput + try { + chunk = current.inflater.pull() + } catch (error) { + throw error instanceof InflateError ? invalidData(error.message) : error + } + if (chunk === NeedInput) return this.needInput() + if (chunk !== undefined) { + current.actualSize += chunk.length + // Fail before handing out bytes past the reported size, so that + // consumers can limit what a small compressed entry inflates to + const { path, size } = current.header + if (size !== undefined && current.actualSize > size) throw sizeMismatch(path, current.actualSize, size) + current.actualCrc = crc32(current.actualCrc, chunk) + return [chunk] + } + current.actualCompressedSize = queue.consumed - current.start + } + current.dataDone = true + } + if (current.descriptor) { + const descriptor = this.descriptor(current) + if (descriptor === NeedInput) return NeedInput + this.check(current, descriptor.crc, descriptor.compressedSize, descriptor.size) + } else { + this.check(current, current.crc, current.compressedSize!, current.size!) + } + this.current = undefined + return undefined + } + + private check(current: Current, crc: number, compressedSize: number, size: number): void { + if (current.actualCrc === crc && current.actualCompressedSize === compressedSize && current.actualSize === size) { + return + } + const path = JSON.stringify(current.header.path) + if (current.actualCrc !== crc) { + throw new ZipError({ + kind: "Checksum", + message: `CRC-32 of ${path} is ${hex(current.actualCrc)}, but the archive records ${hex(crc)}` + }) + } + throw new ZipError({ + kind: "SizeMismatch", + message: `${path} has ${current.actualSize} bytes (${current.actualCompressedSize} compressed), ` + + `but the archive records ${size} (${compressedSize} compressed)` + }) + } + + // Reads a data descriptor, whose signature is optional and whose sizes are + // 4 bytes unless the local header has a ZIP64 field. Writers that widen the + // sizes only on overflow are handled by matching each layout against the + // data read; among several matches, one followed by a header signature wins. + private descriptor( + current: Current + ): { readonly crc: number; readonly compressedSize: number; readonly size: number } | typeof NeedInput { + const queue = this.queue + if (queue.length < DESCRIPTOR_PEEK && !this.ended) return NeedInput + const bytes = queue.peek(Math.min(queue.length, DESCRIPTOR_PEEK)) + const view = new DataView(bytes.buffer) + const candidates: Array<{ length: number; crc: number; compressedSize: number; size: number }> = [] + const offsets = bytes.length >= 4 && view.getUint32(0, true) === DESCRIPTOR ? [4, 0] : [0] + for (const offset of offsets) { + for (const wide of current.zip64 ? [true, false] : [false, true]) { + const length = offset + (wide ? 20 : 12) + if (length > bytes.length) continue + candidates.push({ + length, + crc: view.getUint32(offset, true), + compressedSize: wide ? readUint64(view, offset + 4) : view.getUint32(offset + 4, true), + size: wide ? readUint64(view, offset + 12) : view.getUint32(offset + 8, true) + }) + } + } + const matches = candidates.filter((candidate) => + candidate.crc === current.actualCrc && candidate.compressedSize === current.actualCompressedSize && + candidate.size === current.actualSize + ) + const match = + matches.find((candidate) => + candidate.length + 4 <= bytes.length && isHeaderSignature(view.getUint32(candidate.length, true)) + ) ?? matches[0] + if (match !== undefined) { + queue.skip(match.length) + return match + } + // A complete archive has at least an end record after the descriptor + if (candidates.length === 0 || (this.ended && bytes.length < DESCRIPTOR_PEEK)) { + throw truncated(queue.consumed + queue.length) + } + queue.skip(candidates[0].length) + return candidates[0] + } + + private parseLocal(bytes: Uint8Array): Current { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length) + const flags = view.getUint16(6, true) + const method = view.getUint16(8, true) + const crc = view.getUint32(14, true) + let compressedSize = view.getUint32(18, true) + let size = view.getUint32(22, true) + const time = view.getUint16(10, true) + const date = view.getUint16(12, true) + const nameLength = view.getUint16(26, true) + const rawName = bytes.subarray(30, 30 + nameLength) + const utf8 = (flags & FLAG_UTF8) !== 0 + let path = decodeName(rawName, utf8) + // As in Info-ZIP and 7-Zip, only the first Unicode path field is read, and + // only without the UTF-8 flag + let readUnicodePath = !utf8 + let seconds: number | undefined + let zip64 = false + let at = 30 + nameLength + while (at + 4 <= bytes.length) { + const id = view.getUint16(at, true) + const start = at + 4 + const end = start + view.getUint16(at + 2, true) + if (end > bytes.length) throw invalidHeader("extra field overflows the header") + if (id === EXTRA_ZIP64) { + zip64 = true + let field = start + if (size === MAX32 && field + 8 <= end) { + size = getUint64(view, field) + field += 8 + } + if (compressedSize === MAX32 && field + 8 <= end) { + compressedSize = getUint64(view, field) + } + } else if (id === EXTRA_TIMESTAMP && end - start >= 5 && (bytes[start] & 1) !== 0) { + // Info-ZIP writes times before 1970 as negative numbers, and others + // write times after 2038 as unsigned ones; the DOS date tells them apart + const value = view.getUint32(start + 1, true) + seconds = value >= 0x80000000 && (date >> 9) + 1980 < 2038 ? value - 0x100000000 : value + } else if (id === EXTRA_UNICODE_PATH && readUnicodePath) { + readUnicodePath = false + if (end - start >= 5 && bytes[start] === 1 && view.getUint32(start + 1, true) === crc32(0, rawName)) { + path = textDecoder.decode(bytes.subarray(start + 5, end)) + } + } + at = end + } + const quoted = JSON.stringify(path) + // Other tools end the name at a NUL character, or reject it + if (path.includes("\0")) throw invalidHeader(`path ${quoted} contains a NUL character`) + if ((flags & (FLAG_ENCRYPTED | FLAG_STRONG_ENCRYPTION)) !== 0) throw unsupported(`entry ${quoted} is encrypted`) + if (method !== 0 && method !== 8) throw unsupported(`entry ${quoted} uses compression method ${method}`) + const descriptor = (flags & FLAG_DESCRIPTOR) !== 0 + // Writers that put sizes after the data usually leave zeros here + const known = !descriptor || compressedSize !== 0 || size !== 0 + // Searching for the descriptor of stored data would let its content forge + // the entries after it + if (method === 0 && !known) throw unsupported(`stored entry ${quoted} has no size before its data`) + if (method === 0 && compressedSize !== size) { + throw invalidHeader(`stored entry ${quoted} has different sizes`) + } + const header: Header = { + path, + type: path.endsWith("/") ? "Directory" : "File", + method: method === 8 ? "Deflate" : "Store", + mtime: new Date(seconds === undefined ? fromDos(time, date) : seconds * 1000), + ...(known && (method === 0 || !descriptor) ? { size } : undefined) + } + return { + header, + inflater: method === 8 + ? new Inflater(this.queue, this.inflateMemory ??= new InflateMemory()) + : undefined, + descriptor, + zip64, + crc, + compressedSize: known ? compressedSize : undefined, + size: known ? size : undefined, + start: this.queue.consumed, + remaining: method === 0 ? compressedSize : 0, + dataDone: false, + actualCrc: 0, + actualSize: 0, + actualCompressedSize: 0 + } + } + + private needInput(): typeof NeedInput { + if (this.ended) throw truncated(this.queue.consumed + this.queue.length) + return NeedInput + } + + private finish(): undefined | typeof NeedInput { + this.finished = true + this.queue.clear() + return this.ended ? undefined : NeedInput + } +} + +const hex = (n: number): string => `0x${n.toString(16).padStart(8, "0")}` + +// A ZIP64 data descriptor with its signature, and the signature after it +const DESCRIPTOR_PEEK = 28 + +const isHeaderSignature = (signature: number): boolean => + signature === LOCAL || signature === CENTRAL || signature === END || signature === END64 diff --git a/repos/effect/packages/effect/src/encoding/index.ts b/packages/effect/src/encoding/index.ts similarity index 73% rename from repos/effect/packages/effect/src/encoding/index.ts rename to packages/effect/src/encoding/index.ts index 7ac4657b0..079e985c7 100644 --- a/repos/effect/packages/effect/src/encoding/index.ts +++ b/packages/effect/src/encoding/index.ts @@ -35,6 +35,12 @@ export * as Hex from "./Hex.ts" */ export * as Ini from "./Ini.ts" +/** + * @stability unstable + * @since 4.0.6 + */ +export * as JsonArray from "./JsonArray.ts" + /** * @stability unstable * @since 4.0.0 @@ -53,14 +59,32 @@ export * as SchemaBinary from "./SchemaBinary.ts" */ export * as Sse from "./Sse.ts" +/** + * @stability unstable + * @since 4.0.5 + */ +export * as Tar from "./Tar.ts" + /** * @stability unstable * @since 4.0.0 */ export * as Toml from "./Toml.ts" +/** + * @stability experimental + * @since 4.0.6 + */ +export * as Xml from "./Xml.ts" + /** * @stability unstable * @since 4.0.0 */ export * as Yaml from "./Yaml.ts" + +/** + * @stability experimental + * @since 4.0.6 + */ +export * as Zip from "./Zip.ts" diff --git a/repos/effect/packages/effect/src/eventlog/Event.ts b/packages/effect/src/eventlog/Event.ts similarity index 100% rename from repos/effect/packages/effect/src/eventlog/Event.ts rename to packages/effect/src/eventlog/Event.ts diff --git a/repos/effect/packages/effect/src/eventlog/EventGroup.ts b/packages/effect/src/eventlog/EventGroup.ts similarity index 100% rename from repos/effect/packages/effect/src/eventlog/EventGroup.ts rename to packages/effect/src/eventlog/EventGroup.ts diff --git a/packages/effect/src/eventlog/EventJournal.ts b/packages/effect/src/eventlog/EventJournal.ts new file mode 100644 index 000000000..ca5624664 --- /dev/null +++ b/packages/effect/src/eventlog/EventJournal.ts @@ -0,0 +1,1223 @@ +/** + * Stores event-log entries and replication state. + * + * `EventJournal` records committed entries, exposes them for replay, publishes + * local changes, and tracks the remote metadata needed to exchange entries with + * other journals. Higher-level event-log schemas and handlers use this service + * to rebuild projections, sync offline clients, import remote changes, and + * coordinate writes per store. This module also defines journal errors, entry + * and remote identifiers, schemas, and in-memory or IndexedDB-backed journal + * layers. + * + * @stability unstable + * @since 4.0.0 + */ +import * as Arr from "../Array.ts" +import type { Brand } from "../Brand.ts" +import * as Clock from "../Clock.ts" +import * as Context from "../Context.ts" +import * as Data from "../Data.ts" +import * as DateTime from "../DateTime.ts" +import * as Effect from "../Effect.ts" +import * as SchemaBinary from "../encoding/SchemaBinary.ts" +import { constVoid } from "../Function.ts" +import * as Uuid from "../internal/uuid.ts" +import * as Layer from "../Layer.ts" +import type { Option } from "../Option.ts" +import * as Order from "../Order.ts" +import * as PubSub from "../PubSub.ts" +import * as Schema from "../Schema.ts" +import type * as SchemaAST from "../SchemaAST.ts" +import type { Scope } from "../Scope.ts" +import * as Semaphore from "../Semaphore.ts" +import type { StoreId } from "./EventLogMessage.ts" +import { ConflictIndex, entryIdOrder } from "./internal/conflicts.ts" + +/** + * Context service for storing and replaying event journal entries. + * + * **Details** + * + * The service writes local entries, imports entries from remote journals, exposes + * a stream of local changes, and provides per-store locking. + * + * Entries written by `EventLog` carry payloads encoded with their event's + * `payloadSchemaBinary`; see {@link Entry} for how to decode them. + * + * @stability unstable + * @category services + * @since 4.0.0 + */ +export class EventJournal extends Context.Service, EventJournalError> + + /** + * Read the stored entries with the given ids. The result is aligned with + * `ids`, with `undefined` for ids that are not in the journal. + */ + readonly entriesById: ( + ids: ReadonlyArray + ) => Effect.Effect, EventJournalError> + + /** + * Write an event to the journal, performing an effect before committing the + * event. + */ + readonly write: (options: { + readonly event: string + readonly primaryKey: string + readonly payload: Uint8Array + readonly effect: (entry: Entry) => Effect.Effect + }) => Effect.Effect + + /** + * Write several events to the journal, performing an effect before + * committing them. + * + * **Details** + * + * `effect` runs once with every entry, in input order and with ascending + * ids. The entries are committed together and then published to `changes` + * only after it succeeds. If it fails, nothing is committed or published. + */ + readonly writeMany: (options: { + readonly entries: Arr.NonEmptyReadonlyArray<{ + readonly event: string + readonly primaryKey: string + readonly payload: Uint8Array + }> + readonly effect: (entries: Arr.NonEmptyReadonlyArray) => Effect.Effect + }) => Effect.Effect + + /** + * Write events from a remote source to the journal. + * + * **Details** + * + * `effect` runs once with every new entry after compaction, sorted by + * `Entry.Order`. Each entry is paired with the stored entries for the same + * event and primary key whose id is strictly greater, in id order. Entries + * in the batch never conflict with each other: applying them in id order + * gives the same result as delivering them one at a time. A compaction + * replacement written by `EventLog` takes the place and conflicts of the + * source entry it is anchored to, not of its own id. + * + * All conflicts are read before `effect` runs and the batch is committed + * after it succeeds, with no journal I/O while it runs. It is not called + * when the batch has no new entries. If `effect` fails, no entry from the + * batch is committed. + */ + readonly writeFromRemote: ( + options: { + readonly remoteId: RemoteId + readonly entries: ReadonlyArray + readonly compact?: + | ((uncommitted: ReadonlyArray) => Effect.Effect, EventJournalError>) + | undefined + readonly effect: ( + entries: ReadonlyArray<{ + readonly entry: Entry + readonly conflicts: ReadonlyArray + }> + ) => Effect.Effect + } + ) => Effect.Effect<{ + readonly duplicateEntries: ReadonlyArray + }, EventJournalError> + + /** + * Run an effect with the uncommitted entries for a remote source. + * + * The effect is not run when there are no uncommitted entries, in which case + * `Option.none()` is returned. Otherwise, its result is wrapped in + * `Option.some()`. + */ + readonly withRemoteUncommited: ( + remoteId: RemoteId, + f: (entries: Arr.NonEmptyReadonlyArray) => Effect.Effect + ) => Effect.Effect, EventJournalError | E, R> + + /** + * Records upload acknowledgements, advancing `nextRemoteSequence` only across + * contiguous sequences from the current cursor or `firstSequence`. + */ + readonly writeRemoteAcks: (options: { + readonly remoteId: RemoteId + readonly acks: ReadonlyArray + readonly firstSequence?: number | undefined + }) => Effect.Effect + + /** + * Retrieve the first unused sequence number for a remote source. + */ + readonly nextRemoteSequence: (remoteId: RemoteId) => Effect.Effect + + /** + * The entries added to the local journal. + */ + readonly changes: Effect.Effect, never, Scope> + + /** + * Remove all data + */ + readonly destroy: Effect.Effect + + /** + * Run an effect with a lock on the journal. + */ + readonly withLock: (storeId: StoreId) => (effect: Effect.Effect) => Effect.Effect +}>()("effect/eventlog/EventJournal") {} + +const TypeId = "effect/eventlog/EventJournal/EventJournalError" as const + +/** + * Error raised by event journal operations. + * + * **Details** + * + * The error records the journal method that failed and the underlying cause. + * + * @stability unstable + * @category errors + * @since 4.0.0 + */ +export class EventJournalError extends Data.TaggedError("EventJournalError")<{ + readonly method: string + readonly cause: unknown +}> { + /** + * Marks this value as an event journal error for runtime guards. + * + * @stability unstable + * @since 4.0.0 + */ + readonly [TypeId] = TypeId +} + +/** + * Brand identifier used for `RemoteId` values. + * + * @stability unstable + * @category type IDs + * @since 4.0.0 + */ +export type RemoteIdTypeId = "effect/eventlog/EventJournal/RemoteId" + +/** + * Runtime brand identifier used for `RemoteId` values. + * + * @stability unstable + * @category type IDs + * @since 4.0.0 + */ +export const RemoteIdTypeId: RemoteIdTypeId = "effect/eventlog/EventJournal/RemoteId" + +/** + * Branded byte identifier for a remote event journal source. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export type RemoteId = Uint8Array & Brand + +/** + * Schema for branded remote event journal identifiers. + * + * @stability unstable + * @category schemas + * @since 4.0.0 + */ +export const RemoteId = Schema.Uint8Array.pipe(Schema.brand(RemoteIdTypeId)) + +/** + * Generates a new random `RemoteId`. + * + * **When to use** + * + * Use when generating a fresh event-log remote id internally and the UUID bytes + * are trusted to satisfy the brand. + * + * **Gotchas** + * + * This is unsafe because the generated UUID bytes are cast to the brand without + * schema validation. + * + * @stability unstable + * @category unsafe + * @since 4.0.0 + */ +export const makeRemoteIdUnsafe = (): RemoteId => Uuid.v4Bytes() as RemoteId + +/** + * Runtime brand identifier used for `EntryId` values. + * + * @stability unstable + * @category type IDs + * @since 4.0.0 + */ +export const EntryIdTypeId: EntryIdTypeId = "effect/eventlog/EventJournal/EntryId" + +/** + * Brand identifier used for `EntryId` values. + * + * @stability unstable + * @category type IDs + * @since 4.0.0 + */ +export type EntryIdTypeId = "effect/eventlog/EventJournal/EntryId" + +/** + * Branded byte identifier for an event journal entry. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export type EntryId = Uint8Array & Brand + +/** + * Schema for branded event journal entry identifiers. + * + * @stability unstable + * @category schemas + * @since 4.0.0 + */ +export const EntryId = (Schema.Uint8Array as Schema.instanceOf>).pipe( + Schema.brand(EntryIdTypeId) +) + +/** + * Provides an Ordering instance for entry identifiers based on their raw UUID bytes. + * + * @stability unstable + * @category ordering + * @since 4.0.0 + */ +export const EntryIdOrder: Order.Order = entryIdOrder + +const maxEntryIdMillis = 2 ** 48 - 1 + +// The millisecond and 12-bit counter of the last id in a sequence of local ids. +interface EntryIdSequence { + millis: number + counter: number +} + +const processEntryIds: EntryIdSequence = { millis: 0, counter: 0 } + +const nextEntryId = (sequence: EntryIdSequence, now: number): EntryId => { + if (now > sequence.millis) { + sequence.millis = now + sequence.counter = 0 + } else if (++sequence.counter > 0xfff) { + sequence.millis++ + sequence.counter = 0 + } + // The sequence only moves forward, so once exhausted it stays exhausted. + if (sequence.millis > maxEntryIdMillis) { + throw new RangeError("The entry id sequence has passed the last UUID v7 millisecond") + } + const bytes = Uuid.v7Bytes(sequence.millis) + // The counter replaces the 12 random `rand_a` bits after the version. + bytes[6] = 0x70 | (sequence.counter >> 8) + bytes[7] = sequence.counter & 0xff + return bytes as EntryId +} + +// The process sequence, or a copy advanced to `floor` when that is later. The +// copy is never written back, so a floor read from uncommitted rows leaves the +// process sequence alone. +const sequenceAfter = (floor: EntryId | undefined): EntryIdSequence => { + if (floor === undefined) return processEntryIds + const millis = entryIdMillis(floor) + // Continuing from the floor's `rand_a` sorts the next id after it. + const counter = ((floor[6] & 0x0f) << 8) | floor[7] + return millis > processEntryIds.millis || (millis === processEntryIds.millis && counter > processEntryIds.counter) + ? { millis, counter } + : processEntryIds +} + +const maxEntryId = (ids: Iterable): EntryId | undefined => { + let latest: EntryId | undefined + for (const id of ids) { + if (latest === undefined || EntryIdOrder(id, latest) === 1) latest = id + } + return latest +} + +/** + * Generates a UUID v7 `EntryId`, optionally using the supplied millisecond + * timestamp. + * + * **When to use** + * + * Use when generating an event-log entry id internally and the UUID v7 bytes + * are trusted to satisfy the brand. + * + * **Details** + * + * Ids taken from the clock are strictly ascending within the current process. + * Ids created in the same millisecond carry an incrementing 12-bit counter. If + * the clock stalls or moves backwards, ids keep the last millisecond, and + * after 4096 ids in one millisecond they move to the next one. + * + * The journals generate local ids from this sequence, or from a copy of it + * advanced past the latest id in their store, so a local id sorts after every + * entry committed to that store, even when the local clock is behind. + * + * **Gotchas** + * + * This is unsafe because the generated UUID bytes are cast to the brand without + * schema validation. + * + * Throws a `RangeError` once the sequence passes the last UUID v7 millisecond. + * Every later call throws too, because any id it could return would sort before + * one already generated. + * + * Ids created with an explicit `msecs` are not part of the sequence, and clamp + * to the UUID v7 range. + * + * @stability unstable + * @category unsafe + * @since 4.0.0 + */ +export const makeEntryIdUnsafe = (options: { msecs?: number } = {}): EntryId => + options.msecs !== undefined + ? Uuid.v7Bytes(options.msecs) as EntryId + : nextEntryId(processEntryIds, Date.now()) + +// Imported ids further ahead of the local clock than this are logged. +const maxClockSkewMillis = 24 * 60 * 60 * 1000 + +/** + * Logs a warning when the latest of the imported `ids` is more than 24 hours + * ahead of the supplied `clock`. Memory and IndexedDB journals call this after + * commit; SQL calls it at the end of its transaction body, so a later rollback + * (including an enclosing transaction rollback) can still leave a warning. + * + * @internal + */ +export const warnIfAhead = (clock: Clock.Clock, ids: Iterable): Effect.Effect => { + const latest = maxEntryId(ids) + if (latest === undefined) return Effect.void + const ahead = entryIdMillis(latest) - clock.currentTimeMillisUnsafe() + if (ahead <= maxClockSkewMillis) return Effect.void + return Effect.logWarning( + `Imported an entry id ${Math.round(ahead / 3_600_000)} hours ahead of the local clock` + ).pipe(Effect.annotateLogs({ service: "EventJournal", entryId: Uuid.stringify(latest) })) +} + +/** + * Extracts the millisecond timestamp encoded in a UUID v7 `EntryId`. + * + * @stability unstable + * @category getters + * @since 4.0.0 + */ +export const entryIdMillis = (entryId: EntryId): number => + // Multiply instead of shifting: bitwise operators are signed 32-bit. + ((((entryId[0] * 256 + entryId[1]) * 256 + entryId[2]) * 256 + entryId[3]) * 256 + entryId[4]) * 256 + + entryId[5] + +/** + * Schema for a committed event journal entry. + * + * **Details** + * + * An entry records its ID, event tag, primary key, and SchemaBinary-encoded + * payload, with helpers for array SchemaBinary encoding and creation timestamps. + * + * **Gotchas** + * + * `EventLog` encodes `payload` with the `payloadSchemaBinary` of the event whose + * tag is `entry.event`, so decode it with that event's codec. A codec with an + * incompatible wire layout, such as a union of a group's tagged payload schemas, + * cannot read it. Entries from remote journals can carry tags the local schema + * does not define. + * + * @stability unstable + * @category schemas + * @since 4.0.0 + */ +export class Entry extends Schema.Class("effect/eventlog/EventJournal/Entry")({ + id: EntryId, + event: Schema.String, + primaryKey: Schema.String, + payload: Schema.Uint8Array +}) { + /** + * SchemaBinary codec for arrays of committed event journal entries. + * + * @stability unstable + * @since 4.0.0 + */ + static arraySchemaBinary = Schema.Array(SchemaBinary.toCodec(Entry)) + + /** + * Encodes arrays of committed entries with the SchemaBinary entry codec. + * + * @stability unstable + * @since 4.0.0 + */ + static encodeArray = Schema.encodeUnknownEffect(Entry.arraySchemaBinary) + + /** + * Decodes arrays of committed entries with the SchemaBinary entry codec. + * + * @stability unstable + * @since 4.0.0 + */ + static decodeArray = ( + input: unknown, + options?: SchemaAST.ParseOptions + ): Effect.Effect, Schema.SchemaError> => + Effect.map(decodeEntryFieldsArray(input, options), Arr.map(entryFromFields)) + + /** + * Ordering for committed entries by their event journal entry id. + * + * @stability unstable + * @since 4.0.0 + */ + static Order = Order.make((a, b) => EntryIdOrder(a.id, b.id)) + + #idString: string | undefined + + /** + * String representation of the entry id. + * + * @stability unstable + * @since 4.0.0 + */ + get idString(): string { + return this.#idString ??= Uuid.stringify(this.id) + } + + /** + * Creation timestamp encoded in the UUID v7 entry id, in epoch milliseconds. + * + * @stability unstable + * @since 4.0.0 + */ + get createdAtMillis(): number { + return entryIdMillis(this.id) + } + + /** + * Creation timestamp encoded in the UUID v7 entry id, as a UTC date-time. + * + * @stability unstable + * @since 4.0.0 + */ + get createdAt(): DateTime.Utc { + return DateTime.makeUnsafe(this.createdAtMillis) + } +} + +/** + * A local entry's remote-assigned sequence. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface RemoteAck { + readonly entryId: EntryId + readonly sequence: number +} + +/** + * Schema for an event journal entry received from a remote source. + * + * **Details** + * + * It pairs the remote sequence number with the journal entry payload. + * + * @stability unstable + * @category schemas + * @since 4.0.0 + */ +export class RemoteEntry extends Schema.Class("effect/eventlog/EventJournal/RemoteEntry")({ + remoteSequence: Schema.Natural, + entry: Entry +}) {} + +// Decoding frames into the plain field structs and then constructing the +// classes from the already validated fields skips the per-instance class +// decode, which costs more than reading the frame. The wire format is the same. + +/** @internal */ +export const EntryFields = Schema.Struct(Entry.fields) + +/** @internal */ +export const entryFromFields = (fields: typeof EntryFields.Type): Entry => Schema.makeClassTrusted(Entry, fields) + +const decodeEntryFieldsArray = Schema.decodeUnknownEffect(Schema.Array(SchemaBinary.toCodec(EntryFields))) + +/** + * Creates local entries with ids after every id in `after`, normally the + * latest id in the journal's store. + * + * @internal + */ +export const makeEntries = ( + entries: Arr.NonEmptyReadonlyArray< + { readonly event: string; readonly primaryKey: string; readonly payload: Uint8Array } + >, + after: Iterable +): Effect.Effect, EventJournalError> => + Effect.try({ + try: () => { + const sequence = sequenceAfter(maxEntryId(after)) + return Arr.map(entries, ({ event, payload, primaryKey }) => + new Entry({ + id: nextEntryId(sequence, Date.now()), + event, + primaryKey, + payload + }, { disableChecks: true })) + }, + catch: (cause) => new EventJournalError({ method: "writeMany", cause }) + }) + +/** + * Creates an in-memory `EventJournal` service. + * + * **Gotchas** + * + * Entries, remote tracking state, and locks live only in the current process and + * are lost when the service is discarded. + * + * @stability unstable + * @category constructors + * @since 4.0.0 + */ +export const makeMemory: Effect.Effect = Effect.gen(function*() { + const journal: Array = [] + // Local writes append in commit order, which unserialized direct callers can + // make differ from id order, leaving the journal unordered until the next + // import re-sorts it. Remote backlogs are subsequences of the journal, so one + // flag covers them too. + let journalSorted = true + // The latest committed id, the floor for local ids. + let latestId: EntryId | undefined + const commitLatest = (ids: Iterable) => { + latestId = maxEntryId(latestId === undefined ? ids : [latestId, ...ids]) + } + const clock = yield* Clock.Clock + const byId = new Map() + const remotes = new Map }>() + const pubsub = yield* PubSub.unbounded() + const storeSemaphores = new Map() + const withLock = (storeId: StoreId) => { + let semaphore = storeSemaphores.get(storeId) + if (!semaphore) { + semaphore = Semaphore.makeUnsafe(1) + storeSemaphores.set(storeId, semaphore) + } + return semaphore.withPermit + } + + const ensureRemote = (remoteId: RemoteId) => { + const remoteIdString = Uuid.stringify(remoteId) + const remote = remotes.get(remoteIdString) + if (remote) return remote + const created = { sequence: 0, missing: journal.slice() } + remotes.set(remoteIdString, created) + return created + } + + const writeMany: EventJournal["Service"]["writeMany"] = ({ effect, entries }) => + Effect.acquireUseRelease( + Effect.suspend(() => makeEntries(entries, latestId === undefined ? [] : [latestId])), + effect, + (entries, exit) => + Effect.suspend(() => { + if (exit._tag === "Failure") return Effect.void + for (const entry of entries) { + const last = journal[journal.length - 1] + if (last !== undefined && Entry.Order(last, entry) > 0) { + journalSorted = false + } + journal.push(entry) + byId.set(entry.idString, entry) + remotes.forEach((remote) => { + remote.missing.push(entry) + }) + } + commitLatest(entries.map((entry) => entry.id)) + return PubSub.publishAll(pubsub, entries) + }) + ) + + return EventJournal.of({ + entries: Effect.sync(() => journal.slice()), + write: ({ effect, ...entry }) => writeMany({ entries: [entry], effect: (entries) => effect(entries[0]) }), + writeMany, + writeFromRemote: Effect.fnUntraced(function*(options) { + const remote = ensureRemote(options.remoteId) + const uncommittedRemotes: Array = [] + const duplicateEntries: Array = [] + // Overlapping batches can repeat an entry; the first copy wins. + const incomingIds = new Set() + for (const remoteEntry of options.entries) { + const idString = remoteEntry.entry.idString + if (byId.has(idString)) { + duplicateEntries.push(remoteEntry.entry) + } else if (!incomingIds.has(idString)) { + incomingIds.add(idString) + uncommittedRemotes.push(remoteEntry) + } + } + + const compacted = options.compact + ? yield* options.compact(uncommittedRemotes) + : uncommittedRemotes.map((remoteEntry) => remoteEntry.entry) + + if (Arr.isReadonlyArrayNonEmpty(compacted)) { + const index = new ConflictIndex(compacted) + const oldest = index.oldest + // Only the suffix with greater ids can conflict, unless local writes + // left the journal unordered. + let i = journalSorted ? journal.length : 0 + while (i > 0 && Entry.Order(journal[i - 1], oldest) > 0) i-- + for (; i < journal.length; i++) { + index.add(journal[i]) + } + yield* options.effect(index.toBatch()) + } + for (const remoteEntry of uncommittedRemotes) { + byId.set(remoteEntry.entry.idString, remoteEntry.entry) + } + // Sequences advance with the commit, so a failed batch is redelivered. + remote.sequence = nextSequenceAfter(options.entries, remote.sequence) + const imported = uncommittedRemotes.map((remoteEntry) => remoteEntry.entry).sort(Entry.Order) + insertOrdered(journal, journalSorted, imported) + remotes.forEach((target) => { + if (target !== remote) { + insertOrdered(target.missing, journalSorted, imported) + } else if (!journalSorted) { + target.missing.sort(Entry.Order) + } + }) + journalSorted = true + commitLatest(imported.map((entry) => entry.id)) + yield* warnIfAhead(clock, imported.map((entry) => entry.id)) + return { + duplicateEntries + } + }), + withRemoteUncommited: (remoteId, f) => + Effect.acquireUseRelease( + Effect.sync(() => ensureRemote(remoteId).missing.slice()), + (entries) => Arr.isReadonlyArrayNonEmpty(entries) ? Effect.asSome(f(entries)) : Effect.succeedNone, + (entries, exit) => + Effect.sync(() => { + if (exit._tag === "Failure") return + const last = entries[entries.length - 1] + if (!last) return + const remote = ensureRemote(remoteId) + for (let i = remote.missing.length - 1; i >= 0; i--) { + if (remote.missing[i].id === last.id) { + remote.missing = remote.missing.slice(i + 1) + break + } + } + }) + ), + writeRemoteAcks: ({ acks, firstSequence, remoteId }) => + Effect.sync(() => { + const remote = ensureRemote(remoteId) + remote.sequence = advanceSequence(remote.sequence, acks, firstSequence) + }), + nextRemoteSequence: (remoteId) => Effect.sync(() => ensureRemote(remoteId).sequence), + entriesById: (ids) => Effect.sync(() => ids.map((id) => byId.get(Uuid.stringify(id)))), + changes: PubSub.subscribe(pubsub), + destroy: Effect.sync(() => { + journal.length = 0 + journalSorted = true + latestId = undefined + byId.clear() + remotes.clear() + }), + withLock + }) +}) + +// Same result as appending `additions` and sorting by id. +const insertOrdered = (entries: Array, sorted: boolean, additions: ReadonlyArray): void => { + let i = entries.length - 1 + for (let n = 0; n < additions.length; n++) { + entries.push(additions[n]) + } + if (!sorted) { + entries.sort(Entry.Order) + return + } + // Merge from the back, so additions sorting after the whole array cost O(additions). + let j = additions.length - 1 + let k = entries.length - 1 + while (j >= 0) { + if (i >= 0 && Entry.Order(entries[i], additions[j]) > 0) { + entries[k--] = entries[i--] + } else { + entries[k--] = additions[j--] + } + } +} + +/** + * Layer that provides an in-memory `EventJournal`. + * + * **Gotchas** + * + * All journal data is stored in process memory and is not persisted across layer + * lifetimes. + * + * @stability unstable + * @category layers + * @since 4.0.0 + */ +export const layerMemory: Layer.Layer = Layer.effect(EventJournal, makeMemory) + +/** + * Creates an `EventJournal` backed by IndexedDB. + * + * **Details** + * + * The journal stores entries and remote replication metadata in the configured + * browser database, publishes local changes, and requires `Scope` so the database + * connection can be closed when the scope ends. + * + * @stability unstable + * @category constructors + * @since 4.0.0 + */ +export const makeIndexedDb = (options?: { + readonly database?: string +}): Effect.Effect => + Effect.gen(function*() { + const database = options?.database ?? "effect_event_journal" + const openRequest = indexedDB.open(database, 2) + openRequest.onupgradeneeded = (event) => { + const db = openRequest.result + const tx = openRequest.transaction! + + if (event.oldVersion < 1) { + const entries = db.createObjectStore("entries", { keyPath: "id" }) + entries.createIndex("id", "id", { unique: true }) + entries.createIndex("event", "event") + db.createObjectStore("remoteEntryId", { keyPath: ["remoteId"] }).createIndex("id", "remoteId", { unique: true }) + } + + const entries = tx.objectStore("entries") + // Bounds conflict reads to one event and primary key. + entries.createIndex("conflict", ["event", "primaryKey", "id"]) + // Entry ids order creation, not insertion, so uploads checkpoint an + // insertion sequence instead. + const sequence = db.createObjectStore("entrySequence", { autoIncrement: true }) + // The remote each imported entry came from, so uploads skip it. + const sources = db.createObjectStore("entrySource") + if (event.oldVersion < 1) return + + // Number the existing entries in creation order. + const entryIds = entries.openKeyCursor() + entryIds.onsuccess = () => { + const cursor = entryIds.result + if (!cursor) return + sequence.add(cursor.primaryKey) + cursor.continue() + } + + // Version 1 kept a `remotes` row per entry per remote and recorded upload + // progress as an entry id, which cannot become a checkpoint. Keep one + // source per entry and, walking each remote's rows in sequence order, its + // download cursor; then drop the store. + const progress = tx.objectStore("remoteEntryId") + progress.clear() + const remotes = tx.objectStore("remotes").index("sequence").openCursor() + remotes.onsuccess = () => { + const cursor = remotes.result + if (!cursor) { + db.deleteObjectStore("remotes") + return + } + const row = cursor.value as { remoteId: IDBValidKey; entryId: IDBValidKey; sequence: number } + sources.put(row.remoteId, row.entryId) + progress.put({ remoteId: row.remoteId, nextSequence: row.sequence + 1 }) + cursor.continue() + } + } + + const db = yield* Effect.acquireRelease( + idbReq("open", () => openRequest), + (db) => Effect.sync(() => db.close()) + ) + + const pubsub = yield* PubSub.unbounded() + const clock = yield* Clock.Clock + + // Other tabs share the database but not the entry id sequence. + const latestEntryId = Effect.callback, EventJournalError>((resume) => { + const tx = db.transaction("entries", "readonly") + const request = tx.objectStore("entries").openKeyCursor(null, "prev") + onTransactionDone(tx, "writeMany", resume, () => { + const key = request.result?.primaryKey as ArrayBuffer | ArrayBufferView | undefined + if (key === undefined) return [] + return [ + (ArrayBuffer.isView(key) + ? new Uint8Array(key.buffer, key.byteOffset, key.byteLength) + : new Uint8Array(key)) as EntryId + ] + }) + return Effect.sync(() => tx.abort()) + }) + + const writeMany: EventJournal["Service"]["writeMany"] = ({ effect, entries }) => + Effect.uninterruptibleMask((restore) => + latestEntryId.pipe( + Effect.flatMap((latest) => makeEntries(entries, latest)), + Effect.flatMap((committed) => + restore(effect(committed)).pipe( + Effect.tap(Effect.callback((resume) => { + const tx = db.transaction(["entries", "entrySequence"], "readwrite") + const store = tx.objectStore("entries") + const sequence = tx.objectStore("entrySequence") + for (const entry of committed) { + store.put(encodeEntryIdb(entry)) + sequence.add(entry.id as IDBValidKey) + } + onTransactionDone(tx, "writeMany", resume, constVoid) + })), + Effect.tap(PubSub.publishAll(pubsub, committed)) + ) + ) + ) + ) + + return EventJournal.of({ + entries: idbReq("entries", () => + db.transaction("entries", "readonly") + .objectStore("entries") + .index("id") + .getAll()).pipe( + Effect.map((rows) => rows.map(entryFromIdb)) + ), + entriesById: (ids) => + Effect.callback, EventJournalError>((resume) => { + const entries: Array = ids.map(() => undefined) + const tx = db.transaction("entries", "readonly") + const store = tx.objectStore("entries") + for (let i = 0; i < ids.length; i++) { + const request = store.get(ids[i] as IDBValidKey) + request.onsuccess = () => { + if (request.result !== undefined) entries[i] = entryFromIdb(request.result) + } + } + onTransactionDone(tx, "entriesById", resume, () => entries) + return Effect.sync(() => tx.abort()) + }), + write: ({ effect, ...entry }) => writeMany({ entries: [entry], effect: (entries) => effect(entries[0]) }), + writeMany, + writeFromRemote: Effect.fnUntraced(function*(options) { + const uncommittedRemotes: Array = [] + const duplicateEntries: Array = [] + // Overlapping batches can repeat an entry; the first copy wins. + const incomingIds = new Set() + + yield* Effect.callback((resume) => { + const tx = db.transaction("entries", "readonly") + const entries = tx.objectStore("entries") + // Requests in a transaction complete in the order they were issued. + for (const remoteEntry of options.entries) { + const request = entries.getKey(remoteEntry.entry.id as IDBValidKey) + request.onsuccess = () => { + const idString = remoteEntry.entry.idString + if (request.result !== undefined) { + duplicateEntries.push(remoteEntry.entry) + } else if (!incomingIds.has(idString)) { + incomingIds.add(idString) + uncommittedRemotes.push(remoteEntry) + } + } + } + onTransactionDone(tx, "writeFromRemote", resume, constVoid) + return Effect.sync(() => tx.abort()) + }) + + const compacted = options.compact + ? yield* options.compact(uncommittedRemotes) + : uncommittedRemotes.map((remoteEntry) => remoteEntry.entry) + + if (Arr.isReadonlyArrayNonEmpty(compacted)) { + const index = new ConflictIndex(compacted) + yield* Effect.callback((resume) => { + const tx = db.transaction("entries", "readonly") + const conflicts = tx.objectStore("entries").index("conflict") + // One read per group of the entries with a greater id than its + // oldest, in id order. + for (const group of index.groups()) { + const request = conflicts.getAll(IDBKeyRange.bound( + [group.event, group.primaryKey, group.oldest.id as IDBValidKey], + [group.event, group.primaryKey, entryIdUpperBound], + true + )) + request.onsuccess = () => { + for (const row of request.result) index.add(entryFromIdb(row)) + } + } + onTransactionDone(tx, "writeFromRemote", resume, constVoid) + return Effect.sync(() => tx.abort()) + }) + yield* options.effect(index.toBatch()) + } + + yield* Effect.callback((resume) => { + const tx = db.transaction(["entries", "entrySource", "entrySequence", "remoteEntryId"], "readwrite") + const entries = tx.objectStore("entries") + const sources = tx.objectStore("entrySource") + const sequence = tx.objectStore("entrySequence") + for (const remoteEntry of uncommittedRemotes) { + entries.add(encodeEntryIdb(remoteEntry.entry)) + sequence.add(remoteEntry.entry.id as IDBValidKey) + sources.put(options.remoteId, remoteEntry.entry.id as IDBValidKey) + } + // Sequences advance with the commit, so a failed batch is redelivered. + advanceProgress( + tx.objectStore("remoteEntryId"), + options.remoteId, + "nextSequence", + () => nextSequenceAfter(options.entries) + ) + onTransactionDone(tx, "writeFromRemote", resume, constVoid) + return Effect.sync(() => tx.abort()) + }) + yield* warnIfAhead(clock, uncommittedRemotes.map((remoteEntry) => remoteEntry.entry.id)) + return { + duplicateEntries + } + }), + withRemoteUncommited: (remoteId, f) => { + const remoteIdKey = remoteId as IDBValidKey + return Effect.callback<{ readonly entries: Array; readonly sequence: number }, EventJournalError>( + (resume) => { + const entries: Array = [] + // The last insertion sequence visited, or 0 when nothing follows the checkpoint. + let sequence = 0 + const tx = db.transaction(["entries", "entrySource", "remoteEntryId", "entrySequence"], "readonly") + const entriesStore = tx.objectStore("entries") + const sourcesStore = tx.objectStore("entrySource") + const checkpoint = tx.objectStore("remoteEntryId").get([remoteIdKey]) as IDBRequest< + RemoteProgress | undefined + > + checkpoint.onsuccess = () => { + const cursorRequest = tx.objectStore("entrySequence").openCursor( + IDBKeyRange.lowerBound(checkpoint.result?.sequence ?? 0, true) + ) + cursorRequest.onsuccess = () => { + const cursor = cursorRequest.result + if (!cursor) return + sequence = cursor.key as number + const entryId = cursor.value as IDBValidKey + const source = sourcesStore.get(entryId) + source.onsuccess = () => { + if (source.result !== undefined && indexedDB.cmp(source.result, remoteIdKey) === 0) return + const entry = entriesStore.get(entryId) + entry.onsuccess = () => { + entries.push(entryFromIdb(entry.result)) + } + } + cursor.continue() + } + } + onTransactionDone(tx, "withRemoteUncommited", resume, () => ({ entries, sequence })) + return Effect.sync(() => tx.abort()) + } + ).pipe( + Effect.flatMap(({ entries, sequence }) => { + if (sequence === 0) return Effect.succeedNone + entries.sort(byEntryId) + return Effect.uninterruptibleMask((restore) => + restore(Arr.isReadonlyArrayNonEmpty(entries) ? Effect.asSome(f(entries)) : Effect.succeedNone).pipe( + Effect.tap(Effect.callback((resume) => { + // Uploads can complete out of order; the checkpoint only moves forward. + const tx = db.transaction("remoteEntryId", "readwrite") + advanceProgress(tx.objectStore("remoteEntryId"), remoteId, "sequence", () => sequence) + onTransactionDone(tx, "withRemoteUncommited", resume, constVoid) + })) + ) + ) + }) + ) + }, + writeRemoteAcks: ({ acks, firstSequence, remoteId }) => + Effect.callback((resume) => { + const tx = db.transaction("remoteEntryId", "readwrite") + advanceProgress( + tx.objectStore("remoteEntryId"), + remoteId, + "nextSequence", + (next) => advanceSequence(next, acks, firstSequence) + ) + onTransactionDone(tx, "writeRemoteAcks", resume, constVoid) + return Effect.sync(() => tx.abort()) + }), + nextRemoteSequence: (remoteId) => + Effect.callback((resume) => { + const tx = db.transaction("remoteEntryId", "readonly") + const progress = tx.objectStore("remoteEntryId").get([remoteId as IDBValidKey]) as IDBRequest< + RemoteProgress | undefined + > + onTransactionDone(tx, "nextRemoteSequence", resume, () => progress.result?.nextSequence ?? 0) + return Effect.sync(() => tx.abort()) + }), + changes: PubSub.subscribe(pubsub), + destroy: Effect.sync(() => { + indexedDB.deleteDatabase(database) + }), + withLock: yield* makeBrowserWithLock(database) + }) + }) + +/** + * The sequence following every entry in a delivered batch. + * + * @internal + */ +export const nextSequenceAfter = (entries: ReadonlyArray, next = 0): number => { + for (const remoteEntry of entries) { + if (remoteEntry.remoteSequence >= next) next = remoteEntry.remoteSequence + 1 + } + return next +} + +/** + * Advances across contiguous acknowledgements without skipping unseen entries. + * + * @internal + */ +export const advanceSequence = ( + next: number, + acks: ReadonlyArray, + firstSequence = 0 +): number => { + next = Math.max(next, firstSequence) + const sequences = acks.map((ack) => ack.sequence).sort((a, b) => a - b) + for (const sequence of sequences) { + if (sequence === next) next++ + else if (sequence > next) break + } + return next +} + +const makeBrowserWithLock = Effect.fnUntraced(function*(key: string) { + if (typeof navigator !== "undefined" && "locks" in navigator) { + return (storeId: StoreId) => (self: Effect.Effect) => + Effect.callback((resume, signal) => { + navigator.locks.request(`${key}/${storeId}`, { signal }, () => + new Promise((resolve) => { + resume(Effect.onExit(self, () => { + resolve() + return Effect.void + })) + })).catch((defect) => resume(Effect.die(defect))) + }) + } + const semaphores = new Map() + return (storeId: StoreId) => { + let semaphore = semaphores.get(storeId) + if (!semaphore) { + semaphore = Semaphore.makeUnsafe(1) + semaphores.set(storeId, semaphore) + } + return semaphore.withPermit + } +}) + +// Progress for a remote: `sequence` is the upload checkpoint and `nextSequence` +// the download cursor. +interface RemoteProgress { + readonly sequence?: number + readonly nextSequence?: number +} + +// Raises one progress field within the store's transaction; progress only moves forward. +const advanceProgress = ( + store: IDBObjectStore, + remoteId: RemoteId, + field: keyof RemoteProgress, + next: (current: number) => number +): void => { + const request = store.get([remoteId as IDBValidKey]) as IDBRequest + request.onsuccess = () => { + const current = request.result?.[field] ?? 0 + const value = next(current) + if (value > current) store.put({ ...request.result, remoteId, [field]: value }) + } +} + +const byEntryId = (a: Entry, b: Entry) => indexedDB.cmp(a.id as IDBValidKey, b.id as IDBValidKey) + +// Greater than or equal to every entry id. +const entryIdUpperBound = new Uint8Array(16).fill(0xff) + +interface EntryIdb { + readonly id: EntryId + readonly event: string + readonly primaryKey: string + readonly payload: Uint8Array +} + +// Rows are written by `encodeEntryIdb`, so they are not validated again. +const entryFromIdb = (row: EntryIdb): Entry => + new Entry({ + id: row.id, + event: row.event, + primaryKey: row.primaryKey, + payload: row.payload + }, { disableChecks: true }) + +const encodeEntryIdb = Schema.encodeSync(Entry) + +/** + * Provides `EventJournal` using the IndexedDB-backed implementation created by + * `makeIndexedDb`. + * + * @stability unstable + * @category layers + * @since 4.0.0 + */ +export const layerIndexedDb = (options?: { + readonly database?: string +}): Layer.Layer => + Layer.effect( + EventJournal, + makeIndexedDb(options) + ) + +// Resumes when the transaction commits, and fails if it errors or aborts. An +// abort, such as a failed commit, does not always dispatch an error first. +const onTransactionDone = ( + tx: IDBTransaction, + method: string, + resume: (effect: Effect.Effect) => void, + onComplete: () => A +): void => { + tx.oncomplete = () => resume(Effect.succeed(onComplete())) + tx.onerror = tx.onabort = () => resume(Effect.fail(new EventJournalError({ method, cause: tx.error }))) +} + +const idbReq = (method: string, evaluate: () => IDBRequest) => + Effect.callback((resume) => { + const request = evaluate() + if (request.readyState === "done") { + resume(Effect.succeed(request.result)) + return + } + request.onsuccess = () => resume(Effect.succeed(request.result)) + request.onerror = () => resume(Effect.fail(new EventJournalError({ method, cause: request.error }))) + }) diff --git a/repos/effect/packages/effect/src/eventlog/EventLog.ts b/packages/effect/src/eventlog/EventLog.ts similarity index 55% rename from repos/effect/packages/effect/src/eventlog/EventLog.ts rename to packages/effect/src/eventlog/EventLog.ts index 4b94239c4..a9909f681 100644 --- a/repos/effect/packages/effect/src/eventlog/EventLog.ts +++ b/packages/effect/src/eventlog/EventLog.ts @@ -10,6 +10,8 @@ * @stability unstable * @since 4.0.0 */ +import * as Arr from "../Array.ts" +import * as Cause from "../Cause.ts" import * as Context from "../Context.ts" import * as Effect from "../Effect.ts" import * as FiberMap from "../FiberMap.ts" @@ -25,6 +27,8 @@ import { Reactivity } from "../reactivity/Reactivity.ts" import * as ReactivityLayer from "../reactivity/Reactivity.ts" import type * as Record from "../Record.ts" import * as Redacted from "../Redacted.ts" +import { CurrentLogAnnotations } from "../References.ts" +import * as Result from "../Result.ts" import * as Schedule from "../Schedule.ts" import * as Schema from "../Schema.ts" import * as SchemaGetter from "../SchemaGetter.ts" @@ -36,6 +40,7 @@ import { Entry, EventJournal, type EventJournalError, makeEntryIdUnsafe, type Re import * as EventLogEncryption from "./EventLogEncryption.ts" import { StoreId } from "./EventLogMessage.ts" import type { EventLogRemote } from "./EventLogRemote.ts" +import { setAnchor } from "./internal/conflicts.ts" /** * Service for writing typed event-log events through registered handlers. @@ -59,6 +64,27 @@ export class EventLog extends Context.Service, Tag>, Event.ErrorWithTag, Tag> | EventJournalError > + /** + * Write several events under one journal lock and one `HandlerTransaction`, + * returning each handler's result in input order. If any handler fails, + * none of the events are committed. + */ + readonly writeMany: < + Groups extends EventGroup.Any, + // Pairs each event tag with its own payload. + Item extends { + readonly [Tag in Event.Tag>]: { + readonly event: Tag + readonly payload: Event.PayloadWithTag, Tag> + } + }[Event.Tag>] + >(options: { + readonly schema: EventLogSchema + readonly events: Arr.NonEmptyReadonlyArray + }) => Effect.Effect< + Arr.NonEmptyArray, Item["event"]>>, + Event.ErrorWithTag, Item["event"]> | EventJournalError + > readonly entries: Effect.Effect, EventJournalError> readonly destroy: Effect.Effect }>()("effect/eventlog/EventLog") {} @@ -83,7 +109,9 @@ export class Registry extends Context.Service readonly effect: (options: { readonly entries: ReadonlyArray - readonly write: (entry: Entry) => Effect.Effect + // Pass an input entry as source to anchor replacements during remote replay. + // Without it, matching IDs retain their position; new IDs use the group start. + readonly write: (entry: Entry, source?: Entry) => Effect.Effect }) => Effect.Effect }) => Effect.Effect @@ -91,7 +119,7 @@ export class Registry extends Context.Service readonly effect: (options: { readonly entries: ReadonlyArray - readonly write: (entry: Entry) => Effect.Effect + readonly write: (entry: Entry, source?: Entry) => Effect.Effect }) => Effect.Effect }> @@ -119,7 +147,7 @@ export const layerRegistry = Layer.effect( readonly events: ReadonlySet readonly effect: (options: { readonly entries: ReadonlyArray - readonly write: (entry: Entry) => Effect.Effect + readonly write: (entry: Entry, source?: Entry) => Effect.Effect }) => Effect.Effect }>() @@ -437,6 +465,68 @@ export class CurrentStoreId extends Context.Reference("effect/eventlog/ defaultValue: constant(StoreId.make("default")) }) {} +/** + * Context reference for a transaction that wraps the event handlers of each + * batch replayed from a remote and of each local `write` or `writeMany`. + * + * **When to use** + * + * Use when handlers project into a store with transactions, such as + * `sql.withTransaction` or an IndexedDB transaction over the projection + * tables, so a batch of N events commits once instead of N times. + * + * **Details** + * + * The journal performs no I/O while the transaction is open: it reads replay + * conflicts before the transaction starts and commits the batch after it + * completes. Defaults to running the handlers without a transaction. + * + * If the transaction fails, the journal commits nothing from the batch. For a + * replayed batch, the remote is subscribed again after a backoff of up to 10 + * seconds and the batch is delivered again, so later batches from that remote + * wait until it succeeds; local writes and other remotes are not blocked. A + * local write fails with the error and is not retried. + * + * **Gotchas** + * + * - A replayed entry that fails to decode, or whose handler fails or dies, + * fails the whole batch, which is retried as above. A failure that repeats + * on every attempt, such as a payload written by a newer schema, stops sync + * from that remote until the app is updated to handle it. + * - In a local write, an error returned by a handler fails the transaction + * and the write, and the journal commits none of its events. + * - Failures that abort the underlying transaction fail the whole batch, even + * when the handler recovers from them. In IndexedDB, any failed request that + * is not prevented aborts the whole transaction, and in PostgreSQL so does a + * failed statement outside a savepoint. + * - Failing the batch does not undo side effects of handlers that already ran. + * Only changes made inside a transaction that rolls back are undone. + * - Replay handlers must tolerate running more than once for the same entry. + * - Unless the transaction and the journal share a database connection, the + * transaction commits before the journal does, and the two commits are not + * atomic. A crash between them replays a remote batch on restart, but a + * local write is lost from the journal while its projection changes remain. + * - An IndexedDB transaction commits once it has no pending requests and + * control returns to the event loop. A custom wrapper must stop the fiber + * from yielding to the scheduler while it is open, by providing + * `References.PreventSchedulerYield` as `withTransaction` from + * `@effect/platform-browser` does. Otherwise it can commit partway through a + * batch, and the remaining handlers fail. + * - The transaction stays open for the whole batch, so other read-write + * transactions on the same tables wait for it. + * - The transaction must not change the error or success of the wrapped + * effect. Convert its own failures into defects. + * + * @stability unstable + * @category services + * @since 4.0.6 + */ +export class HandlerTransaction extends Context.Reference< + (effect: Effect.Effect) => Effect.Effect +>("effect/eventlog/EventLog/HandlerTransaction", { + defaultValue: constant(identity) +}) {} + const RedactedUint8Array = Schema.Uint8ArrayFromBase64.pipe( Schema.decodeTo(Schema.Redacted(Schema.Uint8Array), { decode: SchemaGetter.transform((value) => Redacted.make(value)), @@ -581,28 +671,19 @@ export const group = ( }) ) as any -/** - * Registers a compaction handler for an event group. - * - * **Details** - * - * During remote replay, matching entries are decoded, grouped by primary key, and - * passed to the compaction effect, which may write replacement entries. - * - * @stability unstable - * @category compaction - * @since 4.0.0 - */ -export const groupCompaction = ( +const registerBatchCompaction = ( group: EventGroup.EventGroup, effect: (options: { - readonly primaryKey: string - readonly entries: ReadonlyArray - readonly events: ReadonlyArray> - readonly write: >( + readonly entries: ReadonlyArray<{ + readonly entry: Entry + readonly event: Event.TaggedPayload | undefined + }> + readonly writeFrom: >( + source: Entry, tag: Tag, payload: Event.PayloadWithTag ) => Effect.Effect["EncodingServices"]> + readonly forward: (entry: Entry) => Effect.Effect }) => Effect.Effect ): Layer.Layer["DecodingServices"] | Registry> => Layer.effectDiscard( @@ -616,70 +697,197 @@ export const groupCompaction = ( const isEventTag = (tag: string): tag is Event.Tag => Object.hasOwn(group.events, tag) const decodePayload = >(tag: Tag, payload: Uint8Array) => Schema.decodeUnknownEffect(group.events[tag].payloadSchemaBinary)(payload).pipe( - Effect.updateContext((input) => Context.merge(services, input)), - Effect.orDie - ) as unknown as Effect.Effect> - const writePayload = Effect.fnUntraced(function*>( - timestamp: number, + Effect.updateContext((input) => Context.merge(services, input)) + ) as unknown as Effect.Effect, Schema.SchemaError> + const writeFrom = Effect.fnUntraced(function*>( + source: Entry, tag: Tag, payload: Event.PayloadWithTag ): Effect.fn.Return["EncodingServices"]> { const event = Object.hasOwn(group.events, tag) ? group.events[tag] : undefined! const entry = new Entry({ - id: makeEntryIdUnsafe({ msecs: timestamp }), + id: makeEntryIdUnsafe({ msecs: source.createdAtMillis }), event: tag, payload: yield* Schema.encodeUnknownEffect(event.payloadSchemaBinary)(payload).pipe( Effect.orDie ) as any, primaryKey: event.primaryKey(payload) }, { disableChecks: true }) - yield* write(entry) + yield* write(entry, source) }) + const forward = (entry: Entry) => write(entry) - const byPrimaryKey = new Map< - string, - { - readonly entries: Array - readonly taggedPayloads: Array> - } - >() + const batch: Array<{ + readonly entry: Entry + readonly event: Event.TaggedPayload | undefined + }> = [] for (const entry of entries) { if (!isEventTag(entry.event)) { continue } - const payload = yield* decodePayload(entry.event, entry.payload) - const record = byPrimaryKey.get(entry.primaryKey) - const taggedPayload = { _tag: entry.event, payload } as unknown as Event.TaggedPayload - if (record) { - record.entries.push(entry) - record.taggedPayloads.push(taggedPayload) - } else { - byPrimaryKey.set(entry.primaryKey, { - entries: [entry], - taggedPayloads: [taggedPayload] - }) - } + const payload = yield* Effect.result(decodePayload(entry.event, entry.payload)) + batch.push({ + entry, + event: Result.isFailure(payload) + ? undefined + : { _tag: entry.event, payload: payload.success } as unknown as Event.TaggedPayload + }) } - for (const [primaryKey, { entries, taggedPayloads }] of byPrimaryKey) { - yield* Effect.orDie( - effect({ - primaryKey, - entries, - events: taggedPayloads, - write(tag, payload) { - return Effect.orDie(writePayload(entries[0].createdAtMillis, tag, payload)) - } - }).pipe( - Effect.updateContext((input) => Context.merge(services, input)) - ) - ) as any - } + yield* Effect.orDie( + effect({ entries: batch, writeFrom, forward }).pipe( + Effect.updateContext((input) => Context.merge(services, input)) + ) + ) as any }) }) }) ) +/** + * Registers a batch compaction handler for an event group. + * + * **When to use** + * + * Use when an event under one primary key affects entries under other keys. + * + * **Details** + * + * The compaction effect receives every entry for the group in batch order, with + * its decoded event or `event: undefined` if the payload cannot be decoded. + * + * Only entries that the effect outputs are kept: + * + * - `forward(entry)` keeps an input entry unchanged, with the same id, payload, + * and primary key. + * - `write(tag, payload)` encodes a replacement entry anchored to the first + * entry in the batch, with a fresh id in that entry's millisecond. + * + * Entries that are neither forwarded nor replaced are dropped, including entries + * that could not be decoded. During remote replay, kept entries are replayed in + * the id order of their anchors, then in write order, and a replacement's + * conflicts are the stored entries newer than its anchor. + * + * **Gotchas** + * + * Compaction runs before conflicts are read, so it never sees stored entries. + * Collapsing several entries into one is only safe when no stored entry for + * the same event and primary key has an id between theirs. Otherwise that + * stored entry should have applied between the collapsed ones, and replicas + * that received the entries in separate batches end up different. + * + * @see {@link groupCompaction} for compaction grouped by primary key + * + * @stability unstable + * @category compaction + * @since 4.0.0 + */ +export const groupBatchCompaction = ( + group: EventGroup.EventGroup, + effect: (options: { + readonly entries: ReadonlyArray<{ + readonly entry: Entry + readonly event: Event.TaggedPayload | undefined + }> + readonly write: >( + tag: Tag, + payload: Event.PayloadWithTag + ) => Effect.Effect["EncodingServices"]> + readonly forward: (entry: Entry) => Effect.Effect + }) => Effect.Effect +): Layer.Layer["DecodingServices"] | Registry> => + registerBatchCompaction(group, ({ entries, writeFrom, forward }) => + effect({ + entries, + write: (tag, payload) => writeFrom(entries[0].entry, tag, payload), + forward + })) + +/** + * Registers a compaction handler for an event group. + * + * **Details** + * + * During remote replay, decoded entries are grouped by primary key and passed to + * the compaction effect, which outputs the entries to keep: + * + * - `forward(entry)` keeps an input entry unchanged, with the same id, payload, + * and primary key. + * - `write(tag, payload)` encodes a replacement entry anchored to the first + * decoded entry for the primary key, with a fresh id in that entry's + * millisecond. + * + * Entries that are neither forwarded nor replaced are dropped. Entries that + * cannot be decoded are not passed to the effect and are kept unchanged. + * During remote replay, kept entries are replayed in the id order of their + * anchors, then in write order, and a replacement's conflicts are the stored + * entries newer than its anchor. + * + * **Gotchas** + * + * Compaction runs before conflicts are read, so it never sees stored entries. + * Collapsing several entries into one is only safe when no stored entry for + * the same event and primary key has an id between theirs. Otherwise that + * stored entry should have applied between the collapsed ones, and replicas + * that received the entries in separate batches end up different. + * + * @see {@link groupBatchCompaction} for compaction across primary keys + * + * @stability unstable + * @category compaction + * @since 4.0.0 + */ +export const groupCompaction = ( + group: EventGroup.EventGroup, + effect: (options: { + readonly primaryKey: string + readonly entries: ReadonlyArray + readonly events: ReadonlyArray> + readonly write: >( + tag: Tag, + payload: Event.PayloadWithTag + ) => Effect.Effect["EncodingServices"]> + readonly forward: (entry: Entry) => Effect.Effect + }) => Effect.Effect +): Layer.Layer["DecodingServices"] | Registry> => + registerBatchCompaction( + group, + Effect.fnUntraced(function*({ entries, writeFrom, forward }) { + const byPrimaryKey = new Map< + string, + { + readonly entries: Array + readonly events: Array> + } + >() + for (const { entry, event } of entries) { + if (event === undefined) { + yield* forward(entry) + continue + } + const record = byPrimaryKey.get(entry.primaryKey) + if (record) { + record.entries.push(entry) + record.events.push(event) + } else { + byPrimaryKey.set(entry.primaryKey, { entries: [entry], events: [event] }) + } + } + + for (const [primaryKey, { entries, events }] of byPrimaryKey) { + yield* effect({ + primaryKey, + entries, + events, + write: (tag, payload) => writeFrom(entries[0], tag, payload), + forward + }) + } + }) + ) + +type ReactivityKeyName = [keyof ReactivityLayer.Keys] extends [never] ? string : keyof ReactivityLayer.Keys + /** * Registers reactivity keys to invalidate when events from a group are written or * replayed. @@ -695,8 +903,8 @@ export const groupCompaction = ( export const groupReactivity = ( group: EventGroup.EventGroup, keys: - | { readonly [Tag in Event.Tag]?: ReadonlyArray } - | ReadonlyArray + | { readonly [Tag in Event.Tag]?: ReadonlyArray } + | ReadonlyArray ): Layer.Layer => Effect.gen(function*() { const registry = yield* Registry @@ -719,8 +927,10 @@ export const groupReactivity = ( * **Details** * * The returned handler decodes the entry and conflicts with the registered event - * schema, runs the matching handler with the supplied identity and store id, logs - * failures, and invalidates configured reactivity keys. + * schema, runs the matching handler with the supplied identity and store id, and + * invalidates configured reactivity keys. Decode and handler failures are logged + * and fail the effect. Invalidations are deferred until the end of an enclosing + * `Reactivity.withBatch`. * * @stability unstable * @category handlers @@ -736,60 +946,128 @@ export const makeReplayFromRemote = (options: { readonly service: string readonly effect: string } -}) => - Effect.fnUntraced( - function*({ conflicts, entry }: { - readonly entry: Entry - readonly conflicts: ReadonlyArray - }): Effect.fn.Return { +}) => { + const replayBatch = makeReplayBatchFromRemote(options) + return (input: ReplayInput) => replayBatch([input]) +} + +type ReplayInput = { + readonly entry: Entry + readonly conflicts: ReadonlyArray +} + +type DecodedConflict = { entry: Entry; payload: unknown } + +type ReplayBatch = { + readonly context: Context.Context + readonly handlerContexts: Map, Context.Context> + readonly decodedConflicts: Map +} + +/** + * Replays a batch, merging each handler's context with the batch context once + * and decoding each conflict entry shared by several entries once. The batch + * state is created per execution, so transaction retries start fresh. + */ +const makeReplayBatchFromRemote = (options: Parameters[0]) => { + const replayEntry = makeReplayEntryFromRemote(options) + return (batch: ReadonlyArray) => + Effect.contextWith((context: Context.Context) => { + const state: ReplayBatch = { context, handlerContexts: new Map(), decodedConflicts: new Map() } + return Effect.forEach(batch, (input) => replayEntry(input, state), { discard: true }) + }) +} + +// An entry's context is the batch context plus its log annotations, so only +// the annotations need to be copied into the merged handler context. +const withEntryAnnotations = (context: Context.Context, entryContext: Context.Context) => + Context.add(context, CurrentLogAnnotations, Context.get(entryContext, CurrentLogAnnotations)) + +const makeReplayEntryFromRemote = (options: Parameters[0]) => { + // Decoders depend only on the event schema, so they are shared across batches. + const decoders = new WeakMap< + Handlers.Item, + (input: unknown) => Effect.Effect + >() + const decoderFor = (handler: Handlers.Item) => { + let decoder = decoders.get(handler) + if (decoder === undefined) { + decoder = Schema.decodeUnknownEffect(handler.event.payloadSchemaBinary) + decoders.set(handler, decoder) + } + return decoder + } + const contextFor = (batch: ReplayBatch, handler: Handlers.Item) => { + let context = batch.handlerContexts.get(handler) + if (context === undefined) { + context = Context.merge(handler.context, batch.context) + batch.handlerContexts.set(handler, context) + } + return context + } + + return Effect.fnUntraced( + function*( + { conflicts, entry }: ReplayInput, + batch: ReplayBatch + ): Effect.fn.Return { const handler = options.handlers.get(entry.event) as Handlers.Item | undefined if (!handler) { return yield* Effect.logDebug(`Event handler not found for: "${entry.event}"`) } - const decodePayload = Schema.decodeUnknownEffect(handler.event.payloadSchemaBinary) - const decodedConflicts: Array<{ entry: Entry; payload: unknown }> = new Array(conflicts.length) + const decodePayload = decoderFor(handler) + const context = contextFor(batch, handler) + const conflictPayloads: Array = new Array(conflicts.length) for (let i = 0; i < conflicts.length; i++) { - decodedConflicts[i] = { - entry: conflicts[i], - payload: yield* decodePayload(conflicts[i].payload).pipe( - Effect.updateContext((input) => Context.merge(handler.context, input)) - ) as any + const conflict = conflicts[i] + let decoded = batch.decodedConflicts.get(conflict) + if (!decoded) { + decoded = { + entry: conflict, + payload: yield* decodePayload(conflict.payload).pipe( + Effect.updateContext((input) => withEntryAnnotations(context, input)) + ) as any + } + batch.decodedConflicts.set(conflict, decoded) } + conflictPayloads[i] = decoded } + // Conflicts decode with the caller's Identity; the payload and handler use the log's. yield* decodePayload(entry.payload).pipe( Effect.flatMap((payload) => handler.handler({ storeId: options.storeId, payload, entry, - conflicts: decodedConflicts + conflicts: conflictPayloads }) ), - Effect.provideService(Identity, options.identity), - Effect.updateContext((input) => Context.merge(handler.context, input)), + Effect.updateContext((input) => Context.add(withEntryAnnotations(context, input), Identity, options.identity)), Effect.asVoid ) as any - const keys = Object.hasOwn(options.reactivityKeys, entry.event) - ? options.reactivityKeys[entry.event] - : undefined - if (keys) { - for (const key of keys) { - options.reactivity.invalidateUnsafe({ - [key]: [entry.primaryKey] - }) - } - } + yield* invalidateEntry(options.reactivity, options.reactivityKeys, entry) }, - Effect.catchCause(Effect.logError), + Effect.tapCauseIf((cause) => !Cause.hasInterruptsOnly(cause), Effect.logError), (effect, { entry }) => Effect.annotateLogs(effect, { ...options.logAnnotations, entryId: entry.idString }) ) +} + +const invalidateEntry = ( + reactivity: Reactivity, + reactivityKeys: Record>, + entry: Entry +): Effect.Effect => { + const keys = Object.hasOwn(reactivityKeys, entry.event) ? reactivityKeys[entry.event] : undefined + if (!keys) return Effect.void + return Effect.forEach(keys, (key) => reactivity.invalidate({ [key]: [entry.primaryKey] }), { discard: true }) +} const remoteRetrySchedule = Schedule.min([ Schedule.exponential(200, 1.5), @@ -803,7 +1081,8 @@ const make = Effect.gen(function*() { const registry = yield* Registry const reactivity = yield* Reactivity - const replayFromRemote = makeReplayFromRemote({ + const handlerTransaction = yield* HandlerTransaction + const replayFromRemote = makeReplayBatchFromRemote({ handlers: registry.handlers, storeId, identity, @@ -816,78 +1095,80 @@ const make = Effect.gen(function*() { }) const invalidateReactivityEntries = (entries: ReadonlyArray) => - Effect.sync(() => { - for (const entry of entries) { - const keys = Object.hasOwn(registry.reactivityKeys, entry.event) - ? registry.reactivityKeys[entry.event] - : undefined - if (!keys) { - continue - } - for (const key of keys) { - reactivity.invalidateUnsafe({ - [key]: [entry.primaryKey] - }) - } - } - }) + Effect.forEach(entries, (entry) => invalidateEntry(reactivity, registry.reactivityKeys, entry), { discard: true }) const runRemote = Effect.fnUntraced( function*(remote: EventLogRemote["Service"]) { - const startSequence = yield* journal.nextRemoteSequence(remote.id) - yield* Effect.gen(function*() { - const changes = yield* remote.changes({ identity, startSequence, storeId }) + const startSequence = yield* journal.nextRemoteSequence(remote.id) + const changes = yield* remote.changes({ + identity, + startSequence, + storeId, + knownEntries: journal.entriesById + }) while (true) { const entries = yield* Queue.takeAll(changes) yield* journal.writeFromRemote({ remoteId: remote.id, - entries: entries.flat(), + entries, compact: registry.compactors.size > 0 ? Effect.fnUntraced(function*(remoteEntries) { - const finalEntries: Array = [] + // The journal sorts the batch by anchor, so output order is irrelevant. + const output: Array = [] + const inputs = new Map() + const outputCounts = new Map() const compactable = new Map< (options: { readonly entries: ReadonlyArray - readonly write: (entry: Entry) => Effect.Effect + readonly write: (entry: Entry, source?: Entry) => Effect.Effect }) => Effect.Effect, Array >() - for (let i = 0; i < remoteEntries.length; i++) { - const remoteEntry = remoteEntries[i] - const entry = remoteEntry.entry + for (const { entry } of remoteEntries) { + inputs.set(entry.idString, entry) const compactor = registry.compactors.get(entry.event) if (!compactor) { - finalEntries.push(entry) + output.push(entry) continue } - let arr = compactable.get(compactor.effect) - if (!arr) { - arr = [] - compactable.set(compactor.effect, arr) + let group = compactable.get(compactor.effect) + if (!group) { + group = [] + compactable.set(compactor.effect, group) } - arr.push(entry) + group.push(entry) } for (const [compact, entries] of compactable) { yield* compact({ entries, - write(entry) { + write(entry, source) { return Effect.sync(() => { - finalEntries.push(entry) + // Each output is ordered and conflict-checked by its source, + // then by write order, not by its own fresh id. An output + // without a known source stands for the group's first entry. + const anchor = inputs.get((source ?? entry).idString) ?? entries[0] + const index = outputCounts.get(anchor) ?? 0 + outputCounts.set(anchor, index + 1) + setAnchor(entry, anchor, index) + output.push(entry) }) } }) } - return finalEntries.sort(Entry.Order) + return output }) : undefined, - effect: replayFromRemote + // A failed batch is not committed, so the remote redelivers it on retry. + effect: (batch) => handlerTransaction(replayFromRemote(batch)).pipe(Effect.orDie) }).pipe( Effect.tap(({ duplicateEntries }) => invalidateReactivityEntries(duplicateEntries)), - journal.withLock(storeId) + journal.withLock(storeId), + // Outside the lock, so queries re-run after the journal transaction commits. + reactivity.withBatch ) } }).pipe( @@ -901,7 +1182,19 @@ const make = Effect.gen(function*() { Effect.forkScoped ) - const write = journal.withRemoteUncommited(remote.id, (entries) => remote.write({ identity, entries, storeId })) + const write = journal.withRemoteUncommited( + remote.id, + (entries) => + remote.write({ identity, entries, storeId }).pipe( + Effect.flatMap((acks) => + acks === undefined || acks.length === 0 + ? Effect.void + : journal.writeRemoteAcks({ remoteId: remote.id, acks, firstSequence: remote.firstSequence }).pipe( + journal.withLock(storeId) + ) + ) + ) + ) const writeUntilSuccess = write.pipe( Effect.tapCause(Effect.logDebug), Effect.retry(remoteRetrySchedule) @@ -926,60 +1219,49 @@ const make = Effect.gen(function*() { Effect.orDie ) - const writeHandler = Effect.fnUntraced(function*(handler: Handlers.Item, options: { - readonly schema: EventLogSchema - readonly event: string - readonly payload: unknown - }) { - const payload = yield* Schema.encodeUnknownEffect(handler.event.payloadSchemaBinary)(options.payload).pipe( - Effect.orDie - ) - return yield* journal.withLock(storeId)(journal.write({ - event: options.event, - primaryKey: handler.event.primaryKey(options.payload as never), - payload, - effect: (entry) => - handler.handler({ - storeId, - payload: options.payload, - entry, - conflicts: [] - }).pipe( - Effect.updateContext((input) => Context.merge(handler.context, input)), - Effect.provideService(Identity, identity), - Effect.tap(() => { - const keys = Object.hasOwn(registry.reactivityKeys, entry.event) - ? registry.reactivityKeys[entry.event] - : undefined - if (keys) { - for (const key of keys) { - reactivity.invalidateUnsafe({ - [key]: [entry.primaryKey] - }) - } - } - return Effect.void - }) - ) - })) - }) - - const eventLogWrite = (options: { - readonly schema: EventLogSchema - readonly event: string - readonly payload: unknown - }) => { - const handler = registry.handlers.get(options.event) as Handlers.Item | undefined - if (handler === undefined) { - return Effect.die(`Event handler not found for: "${options.event}"`) - } - return writeHandler(handler, options) - } + const writeEvents = ( + events: Arr.NonEmptyReadonlyArray<{ readonly event: string; readonly payload: unknown }> + ): Effect.Effect, unknown> => + Effect.forEach(events, ({ event, payload }) => { + const handler = registry.handlers.get(event) as Handlers.Item | undefined + if (handler === undefined) { + return Effect.die(`Event handler not found for: "${event}"`) + } + return Schema.encodeUnknownEffect(handler.event.payloadSchemaBinary)(payload).pipe( + Effect.orDie, + Effect.map((encoded) => ({ + handler, + payload, + entry: { event, primaryKey: handler.event.primaryKey(payload as never), payload: encoded as Uint8Array } + })) + ) + }).pipe( + Effect.flatMap((writes) => + journal.withLock(storeId)(journal.writeMany({ + entries: Arr.map(writes, (write) => write.entry), + effect: (written) => + handlerTransaction(Effect.forEach(written, (entry, i) => { + const { handler, payload } = writes[i] + return handler.handler({ storeId, payload, entry, conflicts: [] }).pipe( + Effect.updateContext((input) => Context.merge(handler.context, input)), + Effect.provideService(Identity, identity), + Effect.tap(() => invalidateEntry(reactivity, registry.reactivityKeys, entry)) + ) + })) + })) + ), + // Outside the lock, so queries re-run after the handler transaction and + // the journal commit. + reactivity.withBatch + ) as Effect.Effect, unknown> yield* registry.handleRemote(runRemote) return EventLog.of({ - write: eventLogWrite as EventLog["Service"]["write"], + write: ((options: { readonly event: string; readonly payload: unknown }) => + Effect.map(writeEvents([options]), Arr.headNonEmpty)) as EventLog["Service"]["write"], + writeMany: ((options: { readonly events: Arr.NonEmptyReadonlyArray }) => + writeEvents(options.events)) as EventLog["Service"]["writeMany"], entries: journal.entries, destroy: journal.destroy }) @@ -1049,8 +1331,27 @@ export const layer = ( * * **Details** * - * The returned function delegates to the `EventLog` service and preserves each - * event's success and error types. + * The returned function takes one `{ event, payload }` object, typed so the + * payload matches the event, as `writeMany` items are. It delegates to the + * `EventLog` service and preserves each event's success and error types. + * + * **Example** (Writing events with a client) + * + * ```ts + * import { Effect, Schema } from "effect" + * import { EventGroup, EventLog } from "effect/eventlog" + * + * const Users = EventGroup.empty.add({ + * tag: "UserCreated", + * primaryKey: (payload) => payload.id, + * payload: Schema.Struct({ id: Schema.String }) + * }) + * + * export const createUser = Effect.gen(function*() { + * const client = yield* EventLog.makeClient(EventLog.schema(Users)) + * yield* client({ event: "UserCreated", payload: { id: "user-1" } }) + * }) + * ``` * * @stability unstable * @category constructors @@ -1059,24 +1360,23 @@ export const layer = ( export const makeClient = ( schema: EventLogSchema ): Effect.Effect< - (>>( - event: Tag, - payload: Event.PayloadWithTag, Tag> - ) => Effect.Effect< - Event.SuccessWithTag, Tag>, - Event.ErrorWithTag, Tag> | EventJournalError - >), + < + // Pairs each event tag with its own payload. + Item extends { + readonly [Tag in Event.Tag>]: { + readonly event: Tag + readonly payload: Event.PayloadWithTag, Tag> + } + }[Event.Tag>] + >(item: Item) => Effect.Effect< + Event.SuccessWithTag, Item["event"]>, + Event.ErrorWithTag, Item["event"]> | EventJournalError + >, never, EventLog > => Effect.gen(function*() { const log = yield* EventLog - - return >>( - event: Tag, - payload: Event.PayloadWithTag, Tag> - ): Effect.Effect< - Event.SuccessWithTag, Tag>, - Event.ErrorWithTag, Tag> | EventJournalError - > => log.write({ schema, event, payload }) - }) + return (item: { readonly event: string; readonly payload: unknown }) => + log.write({ schema, event: item.event, payload: item.payload } as any) + }) as any diff --git a/packages/effect/src/eventlog/EventLogEncryption.ts b/packages/effect/src/eventlog/EventLogEncryption.ts new file mode 100644 index 000000000..bfc20cf17 --- /dev/null +++ b/packages/effect/src/eventlog/EventLogEncryption.ts @@ -0,0 +1,260 @@ +/** + * Cryptographic service for encrypted event-log replication. + * + * `EventLogEncryption` turns local journal entries into encrypted remote + * payloads and decrypts encrypted changes received from a server. It also + * hashes byte data and creates event-log identities, so remote replication can + * use storage or transport that should not see plaintext event data. + * + * @stability unstable + * @since 4.0.0 + */ +import type { NonEmptyArray } from "../Array.ts" +import * as Context from "../Context.ts" +import * as Effect from "../Effect.ts" +import * as SchemaBinary from "../encoding/SchemaBinary.ts" +import * as Layer from "../Layer.ts" +import * as Redacted from "../Redacted.ts" +import * as Schema from "../Schema.ts" +import * as Transferable from "../workers/Transferable.ts" +import { type Entry, EntryFields, entryFromFields, EntryId, RemoteEntry } from "./EventJournal.ts" +import type { Identity } from "./EventLog.ts" +import { makeGetIdentityRootSecretMaterial } from "./internal/identityRootSecretDerivation.ts" + +/** + * Schema for a batch of journal entries encrypted as one AES-GCM message. + * + * **Details** + * + * `entryIds` lists the ids of the encrypted entries in order. It is sent in + * plaintext so the server can deduplicate batches, and it is authenticated + * together with the store id, so a server cannot alter it or move the batch to + * another store. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export const EncryptedBatch = Schema.Struct({ + entryIds: Schema.NonEmptyArray(EntryId), + iv: Transferable.Uint8Array, + encryptedBatch: Transferable.Uint8Array +}) + +/** + * Type of an encrypted batch. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface EncryptedBatch extends Schema.Schema.Type {} + +/** + * Type of an encrypted batch stored by a remote server, including the remote + * sequence of its first entry. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export interface EncryptedRemoteBatch extends Schema.Schema.Type {} + +/** + * Schema for encrypted batches exchanged with a remote event-log server. + * + * **Details** + * + * The entries of a batch have consecutive remote sequences starting at + * `sequence`. + * + * @stability unstable + * @category models + * @since 4.0.0 + */ +export const EncryptedRemoteBatch = Schema.Struct({ + sequence: Schema.Natural, + ...EncryptedBatch.fields +}) + +const maxBatchEntries = 100 +const maxBatchPayloadBytes = 256 * 1024 + +// A batch holds at most `maxBatchEntries` entries or `maxBatchPayloadBytes` of +// payload, whichever comes first. A single larger entry is a batch on its own. +const toBatches = (entries: ReadonlyArray): Array> => { + const batches: Array> = [] + let current: Array = [] + let bytes = 0 + for (const entry of entries) { + const size = entry.payload.byteLength + if (current.length > 0 && (current.length === maxBatchEntries || bytes + size > maxBatchPayloadBytes)) { + batches.push(current as NonEmptyArray) + current = [] + bytes = 0 + } + current.push(entry) + bytes += size + } + if (current.length > 0) batches.push(current as NonEmptyArray) + return batches +} + +const textEncoder = new TextEncoder() + +// The store id is length-prefixed, followed by the 16-byte entry ids. +const additionalData = (storeId: string, entryIds: ReadonlyArray): Uint8Array => { + const store = textEncoder.encode(storeId) + const data = new Uint8Array(4 + store.byteLength + entryIds.length * 16) + new DataView(data.buffer).setUint32(0, store.byteLength) + data.set(store, 4) + let offset = 4 + store.byteLength + for (const id of entryIds) { + data.set(id, offset) + offset += 16 + } + return data +} + +const equalBytes = (a: Uint8Array, b: Uint8Array): boolean => { + if (a.byteLength !== b.byteLength) return false + for (let i = 0; i < a.byteLength; i++) { + if (a[i] !== b[i]) return false + } + return true +} + +const BatchFromSchemaBinary = SchemaBinary.toCodec(Schema.NonEmptyArray(EntryFields)) +const encodeBatch = Schema.encodeEffect(BatchFromSchemaBinary) +const decodeBatch = Schema.decodeEffect(BatchFromSchemaBinary) + +const toArrayBuffer = (data: Uint8Array): ArrayBuffer => { + const buffer = new ArrayBuffer(data.byteLength) + new Uint8Array(buffer).set(data) + return buffer +} + +/** + * Service that provides identity generation, entry + * encryption and decryption, and SHA-256 hashing for event-log replication. + * + * **When to use** + * + * Use to provide cryptographic operations required by encrypted event-log + * replication. + * + * @stability unstable + * @category services + * @since 4.0.0 + */ +export class EventLogEncryption extends Context.Service + ) => Effect.Effect> + readonly decrypt: ( + identity: Identity["Service"], + storeId: string, + batches: ReadonlyArray + ) => Effect.Effect> + readonly sha256String: (data: Uint8Array) => Effect.Effect + readonly sha256: (data: Uint8Array) => Effect.Effect + readonly generateIdentity: Effect.Effect +}>()("effect/eventlog/EventLogEncryption") {} + +/** + * Creates an `EventLogEncryption` service backed by the Web Crypto `SubtleCrypto` + * APIs from the supplied `Crypto` implementation. + * + * @stability unstable + * @category encryption + * @since 4.0.0 + */ +export const makeEncryptionSubtle = (crypto: Crypto): Effect.Effect => + Effect.sync(() => { + const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(crypto) + + return EventLogEncryption.of({ + encrypt: Effect.fnUntraced(function*(identity, storeId, entries) { + const key = (yield* getIdentityRootSecretMaterial(identity)).encryptionKey + const batches = toBatches(entries) + const data = yield* Effect.orDie(Effect.forEach(batches, (batch) => encodeBatch(batch))) + return yield* Effect.promise(() => + Promise.all(batches.map((batch, i) => { + const entryIds = batch.map((entry) => entry.id) as NonEmptyArray + const iv = crypto.getRandomValues(new Uint8Array(12)) + return crypto.subtle.encrypt( + { name: "AES-GCM", iv, additionalData: additionalData(storeId, entryIds), tagLength: 128 }, + key, + data[i] + ).then((encryptedBatch) => ({ entryIds, iv, encryptedBatch: new Uint8Array(encryptedBatch) })) + })) + ) + }), + decrypt: Effect.fnUntraced(function*(identity, storeId, batches) { + const key = (yield* getIdentityRootSecretMaterial(identity)).encryptionKey + const decrypted = yield* Effect.promise(() => + Promise.all(batches.map((batch) => + crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: batch.iv, + additionalData: additionalData(storeId, batch.entryIds), + tagLength: 128 + }, + key, + batch.encryptedBatch + ) + )) + ) + const result: Array = [] + for (let i = 0; i < batches.length; i++) { + const batch = batches[i] + const fields = yield* Effect.orDie(decodeBatch(new Uint8Array(decrypted[i]))) + const entries = fields.map(entryFromFields) + if ( + entries.length !== batch.entryIds.length || + entries.some((entry, j) => !equalBytes(entry.id, batch.entryIds[j])) + ) { + return yield* Effect.die(new Error("Encrypted batch entries do not match its entry ids")) + } + for (let j = 0; j < entries.length; j++) { + result.push(new RemoteEntry({ remoteSequence: batch.sequence + j, entry: entries[j] })) + } + } + return result + }), + sha256: (data) => + Effect.promise(() => crypto.subtle.digest("SHA-256", toArrayBuffer(data))).pipe( + Effect.map((hash) => new Uint8Array(hash)) + ), + sha256String: (data) => + Effect.map( + Effect.promise(() => crypto.subtle.digest("SHA-256", toArrayBuffer(data))), + (hash) => { + const hashArray = Array.from(new Uint8Array(hash)) + const hashHex = hashArray + .map((bytes) => bytes.toString(16).padStart(2, "0")) + .join("") + return hashHex + } + ), + generateIdentity: Effect.sync(() => ({ + publicKey: crypto.randomUUID(), + privateKey: Redacted.make(crypto.getRandomValues(new Uint8Array(32))) + })) + }) + }) + +/** + * Provides `EventLogEncryption` using `globalThis.crypto`. + * + * @stability unstable + * @category layers + * @since 4.0.0 + */ +export const layerSubtle: Layer.Layer = Layer.effect( + EventLogEncryption, + makeEncryptionSubtle(globalThis.crypto) +) diff --git a/repos/effect/packages/effect/src/eventlog/EventLogMessage.ts b/packages/effect/src/eventlog/EventLogMessage.ts similarity index 82% rename from repos/effect/packages/effect/src/eventlog/EventLogMessage.ts rename to packages/effect/src/eventlog/EventLogMessage.ts index ceea16fd6..92011259a 100644 --- a/repos/effect/packages/effect/src/eventlog/EventLogMessage.ts +++ b/packages/effect/src/eventlog/EventLogMessage.ts @@ -11,16 +11,18 @@ */ import type { NonEmptyArray, NonEmptyReadonlyArray } from "../Array.ts" import type { Brand } from "../Brand.ts" +import * as Effect from "../Effect.ts" import * as SchemaBinary from "../encoding/SchemaBinary.ts" import * as Predicate from "../Predicate.ts" import * as Rpc from "../rpc/Rpc.ts" import * as RpcGroup from "../rpc/RpcGroup.ts" import * as RpcMiddleware from "../rpc/RpcMiddleware.ts" import * as Schema from "../Schema.ts" +import type * as SchemaAST from "../SchemaAST.ts" import * as Transferable from "../workers/Transferable.ts" -import { Entry, RemoteEntry, RemoteId } from "./EventJournal.ts" +import { Entry, EntryFields, entryFromFields, EntryId, RemoteEntry, RemoteId } from "./EventJournal.ts" import type { Identity } from "./EventLog.ts" -import { EncryptedEntry, EncryptedRemoteEntry } from "./EventLogEncryption.ts" +import { EncryptedBatch, EncryptedRemoteBatch } from "./EventLogEncryption.ts" /** * Type-level identifier used to brand event-log store ids. @@ -109,8 +111,8 @@ export class EventLogAuthentication extends RpcMiddleware.Service("effect/eventlog/EventLogRemote/HelloResponse")({ remoteId: RemoteId, - challenge: Transferable.Uint8Array + challenge: Transferable.Uint8Array, + firstSequence: Schema.optional(Schema.Natural) }) {} /** @@ -271,15 +274,45 @@ export class ChunkedMessage } } +/** + * Schema for a written entry's id and a sequence at which the server stores + * it. Entries the server already held keep their stored sequence. When a write + * stores an entry again, as part of a partly known batch, it is acknowledged at + * its new position. + * + * @stability unstable + * @category protocols + * @since 4.0.6 + */ +export const WriteAck = Schema.Struct({ + entryId: EntryId, + sequence: Schema.Natural +}) + +/** + * Acknowledgement of one written entry. + * + * @stability unstable + * @category protocols + * @since 4.0.6 + */ +export type WriteAck = typeof WriteAck.Type + /** * RPC used to send one chunk of a large encoded write payload. * + * **Details** + * + * The completing chunk returns acknowledgements in request order; earlier + * chunks return an empty array. + * * @stability unstable * @category protocols * @since 4.0.0 */ export class WriteChunkedRpc extends Rpc.make("EventLog.WriteChunked", { payload: ChunkedMessage, + success: Schema.Array(WriteAck), error: EventLogProtocolError }).middleware(EventLogAuthentication) {} @@ -288,8 +321,8 @@ export class WriteChunkedRpc extends Rpc.make("EventLog.WriteChunked", { * * **Details** * - * It includes the client public key, target store id, and encrypted entries - * with their AES-GCM initialization vectors. + * It includes the client public key, target store id, and the entries + * encrypted as batches. * * @stability unstable * @category protocols @@ -298,7 +331,7 @@ export class WriteChunkedRpc extends Rpc.make("EventLog.WriteChunked", { export class WriteEntries extends Schema.Class("effect/eventlog/EventLogRemote/WriteEntries")({ publicKey: Schema.String, storeId: StoreId, - encryptedEntries: Schema.Array(EncryptedEntry) + batches: Schema.Array(EncryptedBatch) }) { static FromSchemaBinary = SchemaBinary.toCodec(WriteEntries) static encode = Schema.encodeEffect(this.FromSchemaBinary) @@ -324,15 +357,32 @@ export class WriteEntriesUnencrypted { static FromSchemaBinary = SchemaBinary.toCodec(WriteEntriesUnencrypted) static encode = Schema.encodeEffect(this.FromSchemaBinary) - static decode = Schema.decodeEffect(this.FromSchemaBinary) + static decode = ( + data: Uint8Array, + options?: SchemaAST.ParseOptions + ): Effect.Effect => + Effect.map( + decodeWriteEntriesUnencryptedFields(data, options), + (fields) => + Schema.makeClassTrusted(WriteEntriesUnencrypted, { ...fields, entries: fields.entries.map(entryFromFields) }) + ) get encoded() { return WriteEntriesUnencrypted.encode(this) } } +const decodeWriteEntriesUnencryptedFields = Schema.decodeEffect(SchemaBinary.toCodec(Schema.Struct({ + ...WriteEntriesUnencrypted.fields, + entries: Schema.Array(EntryFields) +}))) + /** * RPC used to send an encoded write payload that fits in one message. * + * **Details** + * + * The response acknowledges every written entry, in request order. + * * @stability unstable * @category protocols * @since 4.0.0 @@ -341,6 +391,7 @@ export class WriteSingleRpc extends Rpc.make("EventLog.WriteSingle", { payload: { data: Transferable.Uint8Array }, + success: Schema.Array(WriteAck), error: EventLogProtocolError }).middleware(EventLogAuthentication) {} @@ -351,7 +402,7 @@ export class WriteSingleRpc extends Rpc.make("EventLog.WriteSingle", { * **Details** * * Responses are encoded as either `SingleMessage` values or `ChunkedMessage` - * parts. + * parts. Entries written on this connection are acknowledged, not echoed. * * @stability unstable * @category protocols @@ -367,7 +418,7 @@ export class ChangesRpc extends Rpc.make("EventLog.Changes", { error: EventLogProtocolError, stream: true }).middleware(EventLogAuthentication) { - static EncryptedFromSchemaBinary = SchemaBinary.toCodec(Schema.NonEmptyArray(EncryptedRemoteEntry)) + static EncryptedFromSchemaBinary = SchemaBinary.toCodec(Schema.NonEmptyArray(EncryptedRemoteBatch)) static UnencryptedFromSchemaBinary = SchemaBinary.toCodec(Schema.NonEmptyArray(RemoteEntry)) static encodeEncrypted = Schema.encodeEffect(ChangesRpc.EncryptedFromSchemaBinary) static decodeEncrypted = Schema.decodeEffect(ChangesRpc.EncryptedFromSchemaBinary) diff --git a/repos/effect/packages/effect/src/eventlog/EventLogRemote.ts b/packages/effect/src/eventlog/EventLogRemote.ts similarity index 77% rename from repos/effect/packages/effect/src/eventlog/EventLogRemote.ts rename to packages/effect/src/eventlog/EventLogRemote.ts index 9ecb39255..7beb1c258 100644 --- a/repos/effect/packages/effect/src/eventlog/EventLogRemote.ts +++ b/packages/effect/src/eventlog/EventLogRemote.ts @@ -24,7 +24,7 @@ import type { RpcClientError } from "../rpc/RpcClientError.ts" import type * as RpcGroup from "../rpc/RpcGroup.ts" import type * as Schema from "../Schema.ts" import type * as Scope from "../Scope.ts" -import type { Entry, RemoteEntry, RemoteId } from "./EventJournal.ts" +import { type Entry, type EventJournal, type EventJournalError, RemoteEntry, type RemoteId } from "./EventJournal.ts" import { type Identity, Registry } from "./EventLog.ts" import { EventLogEncryption, layerSubtle } from "./EventLogEncryption.ts" import { @@ -35,6 +35,7 @@ import { EventLogRemoteRpcs, type HelloResponse, type StoreId, + type WriteAck, WriteEntries, WriteEntriesUnencrypted } from "./EventLogMessage.ts" @@ -54,22 +55,35 @@ import { makeGetIdentityRootSecretMaterial } from "./internal/identityRootSecret * It can write local entries to the remote, stream remote changes from a sequence * number, and run effects only after the supplied identity has authenticated. * + * Built-in remotes acknowledge writes instead of echoing them through `changes`. + * Custom remotes may return `void` from `write` to confirm uploads through + * `changes` instead. + * * @stability unstable * @category services * @since 4.0.0 */ export class EventLogRemote extends Context.Service Effect.Effect, never, Scope.Scope> readonly write: (options: { readonly identity: Identity["Service"] readonly storeId: StoreId readonly entries: ReadonlyArray - }) => Effect.Effect + }) => Effect.Effect | void, EventLogRemoteError> readonly whenAuthenticated: ( effect: Effect.Effect ) => Effect.Effect @@ -172,8 +186,10 @@ export const makeWith = Effect.fnUntraced(function*({ encodeWrite, decodeChanges }) => Effect.Effect, Schema.SchemaError> readonly decodeChanges: ( identity: Identity["Service"], - data: Uint8Array - ) => Effect.Effect, Schema.SchemaError> + storeId: StoreId, + data: Uint8Array, + knownEntries?: EventJournal["Service"]["entriesById"] + ) => Effect.Effect, Schema.SchemaError | EventJournalError> }): Effect.fn.Return { const client = yield* EventLogRemoteClient const registry = yield* Registry @@ -236,6 +252,7 @@ export const makeWith = Effect.fnUntraced(function*({ encodeWrite, decodeChanges const remote = EventLogRemote.of({ id: hello.remoteId, + firstSequence: hello.firstSequence, write: Effect.fnUntraced( function*(options) { yield* ensureAuthenticated(options.identity) @@ -243,9 +260,13 @@ export const makeWith = Effect.fnUntraced(function*({ encodeWrite, decodeChanges if (encoded.byteLength <= ChunkedMessage.chunkSize) { return yield* client["EventLog.WriteSingle"]({ data: encoded }) } - for (const part of ChunkedMessage.split(chunkedIdCounter++, encoded)) { - yield* client["EventLog.WriteChunked"](part) - } + // Only the part the server receives last carries acknowledgements. + const acks = yield* Effect.forEach( + ChunkedMessage.split(chunkedIdCounter++, encoded), + (part) => client["EventLog.WriteChunked"](part), + { concurrency: 4 } + ) + return acks.flat() }, retryForbidden, Effect.mapError((cause) => new EventLogRemoteError({ method: "write", cause })) @@ -263,20 +284,30 @@ export const makeWith = Effect.fnUntraced(function*({ encodeWrite, decodeChanges startSequence: options.startSequence }, { asQueue: true }) + // A batch can start before `startSequence`; its earlier entries are dropped. + const offer = Effect.fnUntraced(function*(data: Uint8Array) { + const entries = yield* decodeChanges(options.identity, options.storeId, data, options.knownEntries) + const first = entries.findIndex((entry) => entry.remoteSequence >= options.startSequence) + if (first === -1) return + yield* Queue.offerAll(outgoing, entries.slice(first)) + }) + while (true) { const parts = yield* Queue.takeAll(incoming) for (let i = 0; i < parts.length; i++) { const part = parts[i] if (part._tag === "Single") { - yield* Queue.offerAll(outgoing, yield* decodeChanges(options.identity, part.data)) + yield* offer(part.data) continue } const data = ChunkedMessage.join(chunkedState, part) if (!data) continue - yield* Queue.offerAll(outgoing, yield* decodeChanges(options.identity, data)) + yield* offer(data) } } }).pipe( + // Forbidden retries resubscribe with the same `startSequence`. Other failures + // propagate to EventLog, which reconnects from the journal's committed sequence. (effect) => retryForbidden(effect, options), Effect.mapError((cause) => { if (cause._tag === "EventLogRemoteError") { @@ -322,25 +353,50 @@ export const makeEncrypted = Effect.gen(function*(): Effect.fn.Return< > { const encryption = yield* EventLogEncryption + // Skip decryption only when every entry in a batch is already stored. + const decodeChanges = Effect.fnUntraced(function*( + identity: Identity["Service"], + storeId: StoreId, + data: Uint8Array, + knownEntries?: EventJournal["Service"]["entriesById"] + ) { + const batches = yield* ChangesRpc.decodeEncrypted(data) + if (knownEntries === undefined) return yield* encryption.decrypt(identity, storeId, batches) + const stored = yield* knownEntries(batches.flatMap((batch) => batch.entryIds)) + let offset = 0 + const knownBatches = batches.map((batch) => { + const entries = stored.slice(offset, offset + batch.entryIds.length) + offset += batch.entryIds.length + return entries.every((entry): entry is Entry => entry !== undefined) ? entries : undefined + }) + const decrypted = yield* encryption.decrypt( + identity, + storeId, + batches.filter((_, i) => knownBatches[i] === undefined) + ) + let next = 0 + return batches.flatMap((batch, i) => { + const entries = knownBatches[i] + return entries === undefined + ? batch.entryIds.map(() => decrypted[next++]) + : entries.map((entry, j) => + new RemoteEntry({ remoteSequence: batch.sequence + j, entry }, { disableChecks: true }) + ) + }) + }) + return yield* makeWith({ encodeWrite: (options) => - encryption.encrypt(options.identity, options.entries).pipe( - Effect.flatMap((encryptedEntries) => + encryption.encrypt(options.identity, options.storeId, options.entries).pipe( + Effect.flatMap((batches) => new WriteEntries({ publicKey: options.identity.publicKey, storeId: options.storeId, - encryptedEntries: encryptedEntries.map((entry, i) => ({ - entryId: options.entries[i].id, - iv: entry.iv, - encryptedEntry: entry.encryptedEntry - })) + batches }).encoded ) ), - decodeChanges: (identity, data) => - ChangesRpc.decodeEncrypted(data).pipe( - Effect.flatMap((entries) => encryption.decrypt(identity, entries)) - ) + decodeChanges }) }) @@ -362,7 +418,7 @@ export const makeUnencrypted: Effect.Effect< storeId: options.storeId, entries: options.entries }).encoded, - decodeChanges: (_identity, data) => ChangesRpc.decodeUnencrypted(data) + decodeChanges: (_identity, _storeId, data) => ChangesRpc.decodeUnencrypted(data) }) /** diff --git a/repos/effect/packages/effect/src/eventlog/EventLogServer.ts b/packages/effect/src/eventlog/EventLogServer.ts similarity index 61% rename from repos/effect/packages/effect/src/eventlog/EventLogServer.ts rename to packages/effect/src/eventlog/EventLogServer.ts index 369ed9ec0..a7690e909 100644 --- a/repos/effect/packages/effect/src/eventlog/EventLogServer.ts +++ b/packages/effect/src/eventlog/EventLogServer.ts @@ -14,16 +14,19 @@ import * as Arr from "../Array.ts" import * as Cache from "../Cache.ts" import * as Context from "../Context.ts" import * as Data from "../Data.ts" +import * as Duration from "../Duration.ts" import * as Effect from "../Effect.ts" import * as Equal from "../Equal.ts" +import * as Exit from "../Exit.ts" import * as Hash from "../Hash.ts" +import * as Uuid from "../internal/uuid.ts" import * as Layer from "../Layer.ts" import * as Option from "../Option.ts" import * as Redacted from "../Redacted.ts" import type * as Rpc from "../rpc/Rpc.ts" import type * as RpcGroup from "../rpc/RpcGroup.ts" import * as Stream from "../Stream.ts" -import type { RemoteId } from "./EventJournal.ts" +import type { EntryId, RemoteId } from "./EventJournal.ts" import * as EventLog from "./EventLog.ts" import { ChunkedMessage, @@ -32,7 +35,8 @@ import { EventLogRemoteRpcs, HelloResponse, SingleMessage, - type StoreId + type StoreId, + type WriteAck } from "./EventLogMessage.ts" import * as EventLogSessionAuth from "./EventLogSessionAuth.ts" @@ -72,12 +76,16 @@ export const layerAuthMiddleware: Layer.Layer< * chunked writes, delegates write and change handling to the supplied callbacks, * and frames large change payloads into chunks. * + * Use the connection's `ConnectionWrites.track` in `onWrite` and + * `ConnectionWrites.filter` in `changes` to suppress its own write echoes. + * * @stability unstable * @category layers * @since 4.0.0 */ export const layerRpcHandlers = (options: { readonly remoteId: RemoteId + readonly firstSequence?: number | undefined readonly authorizeIdentity?: (publicKey: string) => Effect.Effect readonly getOrCreateSessionAuthBinding: ( publicKey: string, @@ -85,12 +93,14 @@ export const layerRpcHandlers = (options: { ) => Effect.Effect> readonly onWrite: ( data: Uint8Array, - authenticatedPublicKeys: ReadonlySet - ) => Effect.Effect + authenticatedPublicKeys: ReadonlySet, + writes: ConnectionWrites + ) => Effect.Effect, EventLogProtocolError> readonly changes: (options: { readonly publicKey: string readonly storeId: StoreId readonly startSequence: number + readonly writes: ConnectionWrites }) => Stream.Stream, unknown> }): Layer.Layer< Rpc.ToHandler> | EventLogAuthentication @@ -103,8 +113,8 @@ export const layerRpcHandlers = (options: { }) let chunkedIdCounter = 0 - const persistedSigningPublicKeys = yield* Cache.make({ - lookup: (key: SessionAuthCacheKey) => + const persistedSigningPublicKeys = yield* Cache.makeWith( + (key: SessionAuthCacheKey) => options.getOrCreateSessionAuthBinding(key.publicKey, key.signingPublicKey).pipe( Effect.catchCause((_) => Effect.fail( @@ -117,15 +127,19 @@ export const layerRpcHandlers = (options: { ) ) ), - capacity: 4096 - }) + { + capacity: 4096, + timeToLive: (exit) => Exit.isSuccess(exit) ? Duration.infinity : Duration.zero + } + ) return EventLogRemoteRpcs.of({ "EventLog.Hello": Effect.fnUntraced(function*(_, { client }) { const challenge = yield* Cache.get(clientChallenges, client.id) return new HelloResponse({ remoteId: options.remoteId, - challenge + challenge, + firstSequence: options.firstSequence }) }), "EventLog.Authenticate": Effect.fnUntraced(function*(request, { client }) { @@ -181,21 +195,22 @@ export const layerRpcHandlers = (options: { .annotate(AuthenticatedIdentities, authenticatedIdentities) .annotate(ChunkedMessageState, new Map()) }), - "EventLog.WriteSingle": Effect.fnUntraced(function*(request, { client }) { - yield* options.onWrite( + "EventLog.WriteSingle": (request, { client }) => + options.onWrite( request.data, - Context.getOrUndefined(client.annotations, AuthenticatedIdentities) ?? new Set() - ) - }), - "EventLog.WriteChunked": Effect.fnUntraced(function*(request, { client }) { + Context.getOrUndefined(client.annotations, AuthenticatedIdentities) ?? new Set(), + getConnectionWrites(client) + ), + "EventLog.WriteChunked": (request, { client }) => { const state = Context.get(client.annotations, ChunkedMessageState) const data = ChunkedMessage.join(state, request) - if (!data) return - yield* options.onWrite( + if (!data) return Effect.succeed([]) + return options.onWrite( data, - Context.getOrUndefined(client.annotations, AuthenticatedIdentities) ?? new Set() + Context.getOrUndefined(client.annotations, AuthenticatedIdentities) ?? new Set(), + getConnectionWrites(client) ) - }), + }, "EventLog.Changes": (request, { client }) => { const authenticatedIdentities = Context.getOrUndefined(client.annotations, AuthenticatedIdentities) if (!authenticatedIdentities?.has(request.publicKey)) { @@ -211,7 +226,8 @@ export const layerRpcHandlers = (options: { return options.changes({ publicKey: request.publicKey, storeId: request.storeId, - startSequence: request.startSequence + startSequence: request.startSequence, + writes: getConnectionWrites(client) }).pipe( Stream.mapArray(Arr.flatMap((data): NonEmptyReadonlyArray => { if (data.byteLength <= ChunkedMessage.chunkSize) { @@ -259,6 +275,132 @@ export class ChunkedMessageState extends Context.Reference< defaultValue: () => new Map() }) {} +/** + * Tracks writes to suppress their echoes on the same RPC connection. + * + * **Details** + * + * Only tracks entries for stores with an open change stream. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export class ConnectionWrites { + private readonly streams = new Map>() + + /** + * Runs a storage write while suppressing its entries on this connection's streams. + * + * @stability unstable + * @since 4.0.6 + */ + track( + options: { + readonly publicKey: string + readonly storeId: StoreId + readonly entryIds: ReadonlyArray + }, + write: Effect.Effect, E, R> + ): Effect.Effect, E, R> { + const open = this.streams.get(makeStreamKey(options.publicKey, options.storeId)) + if (open === undefined || options.entryIds.length === 0) return write + const streams = Array.from(open) + const started = streams.map((stream) => stream.position) + const ids = options.entryIds.map(Uuid.stringify) + // Track before writing: storage can publish an entry before the write + // returns. Each occurrence is counted, as an id can be stored more than once. + for (const stream of streams) { + for (const id of ids) stream.ids.set(id, (stream.ids.get(id) ?? 0) + 1) + } + return Effect.onExit(write, (exit) => + Effect.sync(() => { + for (let i = 0; i < streams.length; i++) { + const stream = streams[i] + if (exit._tag === "Failure") { + for (const id of ids) release(stream.ids, id) + continue + } + // Release occurrences the stream had already passed when the write began. + for (const ack of exit.value) { + if (ack.sequence <= started[i]) release(stream.ids, Uuid.stringify(ack.entryId)) + } + } + })) + } + + /** + * Filters this connection's writes from a store's change stream. An item can + * hold several entries; `sequence` returns its highest one. + * + * @stability unstable + * @since 4.0.6 + */ + filter( + options: { + readonly publicKey: string + readonly storeId: StoreId + readonly entryIds: (item: A) => ReadonlyArray + readonly sequence: (item: A) => number + }, + stream: Stream.Stream + ): Stream.Stream { + return Stream.suspend(() => { + const key = makeStreamKey(options.publicKey, options.storeId) + const state: ConnectionStream = { ids: new Map(), position: -1 } + let open = this.streams.get(key) + if (open === undefined) { + open = new Set() + this.streams.set(key, open) + } + open.add(state) + return stream.pipe( + Stream.filter((item) => { + const sequence = options.sequence(item) + if (sequence > state.position) state.position = sequence + if (state.ids.size === 0) return true + // An item is written by one request, so it is either all or none of ours. + const ids = options.entryIds(item).map(Uuid.stringify) + if (!ids.every((id) => state.ids.has(id))) return true + for (const id of ids) release(state.ids, id) + return false + }), + Stream.ensuring(Effect.sync(() => { + open.delete(state) + if (open.size === 0) this.streams.delete(key) + })) + ) + }) + } +} + +interface ConnectionStream { + // Outstanding occurrences of this connection's writes, by entry id. + readonly ids: Map + position: number +} + +const release = (ids: Map, id: string): void => { + const count = ids.get(id) + if (count === undefined) return + if (count <= 1) ids.delete(id) + else ids.set(id, count - 1) +} + +const makeStreamKey = (publicKey: string, storeId: StoreId): string => `${publicKey}/${storeId}` + +class ConnectionWritesState extends Context.Service()( + "effect/eventlog/EventLogServer/ConnectionWrites" +) {} + +const getConnectionWrites = (client: Rpc.ServerClient): ConnectionWrites => { + const existing = Context.getOrUndefined(client.annotations, ConnectionWritesState) + if (existing !== undefined) return existing + const writes = new ConnectionWrites() + client.annotate(ConnectionWritesState, writes) + return writes +} + /** * Annotation containing the public keys authenticated on an RPC connection. * diff --git a/repos/effect/packages/effect/src/eventlog/EventLogServerEncrypted.ts b/packages/effect/src/eventlog/EventLogServerEncrypted.ts similarity index 55% rename from repos/effect/packages/effect/src/eventlog/EventLogServerEncrypted.ts rename to packages/effect/src/eventlog/EventLogServerEncrypted.ts index d79205efd..bef13abd5 100644 --- a/repos/effect/packages/effect/src/eventlog/EventLogServerEncrypted.ts +++ b/packages/effect/src/eventlog/EventLogServerEncrypted.ts @@ -14,18 +14,24 @@ import * as Arr from "../Array.ts" import * as Context from "../Context.ts" import * as Effect from "../Effect.ts" +import * as Equal from "../Equal.ts" import * as Uuid from "../internal/uuid.ts" import * as Layer from "../Layer.ts" import * as PubSub from "../PubSub.ts" import * as RcMap from "../RcMap.ts" import * as RpcServer from "../rpc/RpcServer.ts" -import * as Schema from "../Schema.ts" import type * as Scope from "../Scope.ts" import * as Stream from "../Stream.ts" -import * as Transferable from "../workers/Transferable.ts" -import { EntryId, makeRemoteIdUnsafe, type RemoteId } from "./EventJournal.ts" -import type { EncryptedRemoteEntry } from "./EventLogEncryption.ts" -import { ChangesRpc, EventLogProtocolError, EventLogRemoteRpcs, type StoreId, WriteEntries } from "./EventLogMessage.ts" +import { type EntryId, makeRemoteIdUnsafe, type RemoteId } from "./EventJournal.ts" +import type { EncryptedBatch, EncryptedRemoteBatch } from "./EventLogEncryption.ts" +import { + ChangesRpc, + EventLogProtocolError, + EventLogRemoteRpcs, + type StoreId, + type WriteAck, + WriteEntries +} from "./EventLogMessage.ts" import * as EventLogServer from "./EventLogServer.ts" /** @@ -33,9 +39,9 @@ import * as EventLogServer from "./EventLogServer.ts" * * **Details** * - * Incoming encrypted write payloads are decoded and persisted through `Storage`; - * change streams read encrypted entries from storage and encode them for the - * remote protocol. + * Decodes and persists writes through `Storage`, acknowledging their stored + * sequences. Change streams encode stored batches, excluding this connection's + * writes. * * @stability unstable * @category layers @@ -47,9 +53,10 @@ export const layerRpcHandlers = Layer.unwrap(Effect.gen(function*() { return EventLogServer.layerRpcHandlers({ remoteId, + firstSequence: storage.firstSequence, getOrCreateSessionAuthBinding: (publicKey, signingPublicKey) => storage.getOrCreateSessionAuthBinding(publicKey, signingPublicKey), - onWrite: Effect.fnUntraced(function*(data, authenticatedPublicKeys) { + onWrite: Effect.fnUntraced(function*(data, authenticatedPublicKeys, writes) { const request = yield* WriteEntries.decode(data).pipe( Effect.mapError((_) => new EventLogProtocolError({ @@ -68,15 +75,11 @@ export const layerRpcHandlers = Layer.unwrap(Effect.gen(function*() { message: "Identity is not authenticated" }) } - if (request.encryptedEntries.length === 0) return - const entries = request.encryptedEntries.map(({ encryptedEntry, entryId, iv }) => - new PersistedEntry({ - entryId, - iv, - encryptedEntry - }) - ) - return yield* storage.write(request.publicKey, request.storeId, entries).pipe( + if (request.batches.length === 0) return [] + const entryIds = request.batches.flatMap((batch) => batch.entryIds) + return yield* storage.write(request.publicKey, request.storeId, request.batches).pipe( + Effect.flatMap((stored) => toWriteAcks(storage, request.publicKey, request.storeId, request.batches, stored)), + (write) => writes.track({ publicKey: request.publicKey, storeId: request.storeId, entryIds }, write), Effect.catchCause((_) => Effect.fail( new EventLogProtocolError({ @@ -89,13 +92,53 @@ export const layerRpcHandlers = Layer.unwrap(Effect.gen(function*() { ) ) }), - changes: ({ publicKey, storeId, startSequence }) => - storage.changes(publicKey, storeId, startSequence).pipe( - Stream.mapArrayEffect((entries) => Effect.map(ChangesRpc.encodeEncrypted(entries), Arr.of)) + changes: ({ publicKey, startSequence, storeId, writes }) => + writes.filter( + { + publicKey, + storeId, + entryIds: (batch) => batch.entryIds, + sequence: (batch) => batch.sequence + batch.entryIds.length - 1 + }, + storage.changes(publicKey, storeId, startSequence) + ).pipe( + Stream.mapArrayEffect((batches) => Effect.map(ChangesRpc.encodeEncrypted(batches), Arr.of)) ) }) })) +// Acknowledges each entry occurrence in request order: at its position in the +// batch this write stored, or, for a skipped batch, where it was first stored. +const toWriteAcks = Effect.fnUntraced(function*( + storage: Storage["Service"], + publicKey: string, + storeId: StoreId, + batches: ReadonlyArray, + stored: ReadonlyArray +) { + // Stored batches keep request order, and each batch has its own random iv. + let next = 0 + const storedAt = batches.map((batch) => { + if (stored[next] === undefined || !Equal.equals(stored[next].iv, batch.iv)) return undefined + return stored[next++].sequence + }) + const skipped = batches.flatMap((batch, i) => storedAt[i] === undefined ? batch.entryIds : []) + const firstStored = new Map() + if (skipped.length > 0) { + for (const ack of yield* storage.entrySequences(publicKey, storeId, skipped)) { + firstStored.set(Uuid.stringify(ack.entryId), ack.sequence) + } + } + const acks: Array = [] + batches.forEach((batch, b) => + batch.entryIds.forEach((entryId, i) => { + const sequence = storedAt[b] === undefined ? firstStored.get(Uuid.stringify(entryId)) : storedAt[b] + i + if (sequence !== undefined) acks.push({ entryId, sequence }) + }) + ) + return acks +}) + /** * Provides an encrypted event-log RPC server using `EventLogRemoteRpcs` and the * encrypted server RPC handlers. @@ -108,7 +151,7 @@ export const layerRpcHandlers = Layer.unwrap(Effect.gen(function*() { * **Details** * * This layer installs `EventLogRemoteRpcs` on the provided RPC server protocol - * and wires those RPCs to `layerRpcHandlers`. Encrypted entries, session + * and wires those RPCs to `layerRpcHandlers`. Encrypted batches, session * authentication bindings, remote ids, and change streams are delegated to * `Storage`. * @@ -124,31 +167,6 @@ export const layer: Layer.Layer = Rp Layer.provide(layerRpcHandlers) ) -/** - * Schema for encrypted entries persisted by the encrypted event-log server. - * - * @stability unstable - * @category models - * @since 4.0.0 - */ -export class PersistedEntry extends Schema.Class( - "effect/eventlog/EventLogServerEncrypted/PersistedEntry" -)({ - entryId: EntryId, - iv: Transferable.Uint8Array, - encryptedEntry: Transferable.Uint8Array -}) { - /** - * String representation of the encrypted entry id. - * - * @stability unstable - * @since 4.0.0 - */ - get entryIdString(): string { - return Uuid.stringify(this.entryId) - } -} - /** * Defines the backing store service used by the encrypted event-log server. * @@ -160,15 +178,27 @@ export class PersistedEntry extends Schema.Class( * **Details** * * It provides the server remote id, stores session authentication bindings, - * persists encrypted entries, and streams encrypted changes for a public key and + * persists encrypted batches, and streams encrypted changes for a public key and * store id. * + * A batch owns one remote sequence per entry, starting at its `sequence`. + * `write` stores a batch unless every one of its entry ids is already stored, + * which makes retried writes idempotent, and returns the batches it stored. + * `entrySequences` returns the sequence each entry was first stored at, so + * skipped batches can still be acknowledged. `changes` streams every batch + * whose last sequence is at or after `startSequence`, followed by newly stored + * batches. + * * @stability unstable * @category services * @since 4.0.0 */ export class Storage extends Context.Service + /** + * Lowest assigned sequence (default 0). Overestimating skips entries. + */ + readonly firstSequence?: number | undefined readonly getOrCreateSessionAuthBinding: ( publicKey: string, signingPublicKey: Uint8Array @@ -176,13 +206,18 @@ export class Storage extends Context.Service - ) => Effect.Effect> + batches: ReadonlyArray + ) => Effect.Effect> + readonly entrySequences: ( + publicKey: string, + storeId: StoreId, + entryIds: ReadonlyArray + ) => Effect.Effect> readonly changes: ( publicKey: string, storeId: StoreId, startSequence: number - ) => Stream.Stream + ) => Stream.Stream }>()("effect/eventlog/EventLogServer/Storage") {} /** @@ -198,28 +233,25 @@ export class Storage extends Context.Service = Effect.gen(function*() { - const knownIds = new Map>() - const journals = new Map>() + const journals = new Map + readonly batches: Array + nextSequence: number + }>() const sessionAuthBindings = new Map>() const remoteId = makeRemoteIdUnsafe() - const ensureKnownIds = (scopeKey: string): Map => { - let storeKnownIds = knownIds.get(scopeKey) - if (storeKnownIds) return storeKnownIds - storeKnownIds = new Map() - knownIds.set(scopeKey, storeKnownIds) - return storeKnownIds - } const ensureJournal = (scopeKey: string) => { let journal = journals.get(scopeKey) if (journal) return journal - journal = [] + journal = { knownIds: new Map(), batches: [], nextSequence: 0 } journals.set(scopeKey, journal) return journal } const pubsubs = yield* RcMap.make({ lookup: (_scopeKey: string) => Effect.acquireRelease( - PubSub.unbounded(), + PubSub.unbounded(), PubSub.shutdown ), idleTimeToLive: 60000 @@ -234,33 +266,42 @@ export const makeStorageMemory: Effect.Effect = [] - for (const entry of entries) { - const idString = entry.entryIdString - if (storeKnownIds.has(idString)) continue - const encrypted: EncryptedRemoteEntry = { - sequence: journal.length, - entryId: entry.entryId, - iv: entry.iv, - encryptedEntry: entry.encryptedEntry - } - encryptedEntries.push(encrypted) - storeKnownIds.set(idString, encrypted.sequence) - journal.push(encrypted) - PubSub.publishUnsafe(pubsub, encrypted) + const stored: Array = [] + for (const batch of batches) { + const ids = batch.entryIds.map(Uuid.stringify) + if (ids.every((id) => journal.knownIds.has(id))) continue + ids.forEach((id, i) => { + if (!journal.knownIds.has(id)) journal.knownIds.set(id, journal.nextSequence + i) + }) + const remoteBatch: EncryptedRemoteBatch = { sequence: journal.nextSequence, ...batch } + journal.nextSequence += ids.length + journal.batches.push(remoteBatch) + stored.push(remoteBatch) + PubSub.publishUnsafe(pubsub, remoteBatch) } - return encryptedEntries + return stored }, Effect.scoped), + entrySequences: (publicKey, storeId, entryIds) => + Effect.sync(() => { + const knownIds = ensureJournal(makeEncryptedScopeKey({ publicKey, storeId })).knownIds + const acks: Array = [] + for (const entryId of entryIds) { + const sequence = knownIds.get(Uuid.stringify(entryId)) + if (sequence !== undefined) acks.push({ entryId, sequence }) + } + return acks + }), changes: Effect.fnUntraced(function*(publicKey, storeId, startSequence) { const scopeKey = makeEncryptedScopeKey({ publicKey, storeId }) const pubsub = yield* RcMap.get(pubsubs, scopeKey) const subscription = yield* PubSub.subscribe(pubsub) - return Stream.fromArray(ensureJournal(scopeKey).slice(startSequence)).pipe( + const batches = ensureJournal(scopeKey).batches + const first = batches.findIndex((batch) => batch.sequence + batch.entryIds.length > startSequence) + return Stream.fromArray(first === -1 ? [] : batches.slice(first)).pipe( Stream.concat(Stream.fromSubscription(subscription)) ) }, Stream.unwrap) diff --git a/repos/effect/packages/effect/src/eventlog/EventLogServerUnencrypted.ts b/packages/effect/src/eventlog/EventLogServerUnencrypted.ts similarity index 79% rename from repos/effect/packages/effect/src/eventlog/EventLogServerUnencrypted.ts rename to packages/effect/src/eventlog/EventLogServerUnencrypted.ts index 173617219..37d956cd7 100644 --- a/repos/effect/packages/effect/src/eventlog/EventLogServerUnencrypted.ts +++ b/packages/effect/src/eventlog/EventLogServerUnencrypted.ts @@ -39,6 +39,7 @@ import { EventLogProtocolError, EventLogRemoteRpcs, type StoreId, + type WriteAck, WriteEntriesUnencrypted } from "./EventLogMessage.ts" import * as EventLogServer from "./EventLogServer.ts" @@ -104,8 +105,9 @@ export const makeWrite = ( * **Details** * * Incoming plaintext entries are authorized, mapped to a server store, checked - * for conflicts, run through registered handlers, and persisted; change streams - * include compacted backlog entries when compactors are registered. + * for conflicts, run through registered handlers, and persisted. Acknowledgements + * include duplicates with their original sequences. Change streams apply registered + * compactors to backlog entries and exclude this connection's writes. * * @stability unstable * @category layers @@ -129,12 +131,12 @@ export const layerRpcHandlers: Layer.Layer< readonly entries: Arr.NonEmptyReadonlyArray }) { const entries = Arr.sort(options.entries, Entry.Order) - let history = yield* storage.entriesAfter(options.storeId, entries[0]) + const stored = yield* storage.entriesAfter(options.storeId, entries[0], entries) + const index = new HistoryIndex(stored.map((remoteEntry) => remoteEntry.entry)) const persistedEntries = Arr.empty() for (const entry of entries) { - const [duplicate, conflicts, newHistory] = toConflicts(history, entry) - if (duplicate) continue - history = newHistory + if (index.has(entry)) continue + const conflicts = index.conflicts(entry) yield* handler({ publicKey: options.publicKey, storeId: options.storeId, @@ -143,11 +145,26 @@ export const layerRpcHandlers: Layer.Layer< }) persistedEntries.push(entry) } - yield* storage.write(options.storeId, persistedEntries) + const committed = yield* storage.write(options.storeId, persistedEntries) + + const sequences = new Map() + for (const remoteEntry of stored) { + sequences.set(remoteEntry.entry.idString, remoteEntry.remoteSequence) + } + for (const remoteEntry of committed) { + sequences.set(remoteEntry.entry.idString, remoteEntry.remoteSequence) + } + const acks: Array = [] + for (const entry of options.entries) { + const sequence = sequences.get(entry.idString) + if (sequence !== undefined) acks.push({ entryId: entry.id, sequence }) + } + return acks }, storage.withTransaction) return EventLogServer.layerRpcHandlers({ remoteId, + firstSequence: storage.firstSequence, authorizeIdentity: (publicKey) => auth.authorizeIdentity({ publicKey }).pipe( Effect.mapError((error) => @@ -161,7 +178,7 @@ export const layerRpcHandlers: Layer.Layer< ), getOrCreateSessionAuthBinding: (publicKey, signingPublicKey) => storage.getOrCreateSessionAuthBinding(publicKey, signingPublicKey), - onWrite: Effect.fnUntraced(function*(data, authenticatedPublicKeys) { + onWrite: Effect.fnUntraced(function*(data, authenticatedPublicKeys, writes) { const request = yield* WriteEntriesUnencrypted.decode(data).pipe( Effect.mapError((_) => new EventLogProtocolError({ @@ -180,7 +197,7 @@ export const layerRpcHandlers: Layer.Layer< message: "Identity is not authenticated" }) } - if (!Arr.isReadonlyArrayNonEmpty(request.entries)) return + if (!Arr.isReadonlyArrayNonEmpty(request.entries)) return [] const resolvedStoreId = yield* mapping.resolve({ publicKey: request.publicKey, @@ -211,11 +228,17 @@ export const layerRpcHandlers: Layer.Layer< }) ) ) - yield* processEntries({ + return yield* processEntries({ publicKey: request.publicKey, storeId: resolvedStoreId, entries: request.entries }).pipe( + (write) => + writes.track({ + publicKey: request.publicKey, + storeId: request.storeId, + entryIds: request.entries.map((entry) => entry.id) + }, write), Effect.catchCause((_) => Effect.fail( new EventLogProtocolError({ @@ -238,11 +261,19 @@ export const layerRpcHandlers: Layer.Layer< publicKey: request.publicKey, storeId }) - return storage.changes({ - storeId, - startSequence: request.startSequence, - compactors: registry.compactors - }).pipe( + return request.writes.filter( + { + publicKey: request.publicKey, + storeId: request.storeId, + entryIds: (remoteEntry) => [remoteEntry.entry.id], + sequence: (remoteEntry) => remoteEntry.remoteSequence + }, + storage.changes({ + storeId, + startSequence: request.startSequence, + compactors: registry.compactors + }) + ).pipe( Stream.mapArrayEffect((entries) => Effect.map(ChangesRpc.encodeUnencrypted(entries), Arr.of)) ) }, Stream.unwrap) @@ -378,7 +409,8 @@ export const layerStoreMappingStatic = (options: { * * It provides the server remote id, stores session authentication bindings, * allocates remote sequence numbers, persists entries, streams changes, and - * exposes a transaction boundary. + * exposes a transaction boundary. `entriesAfter` returns stored entries with + * their sequences, so that duplicate writes can be acknowledged. * * @stability unstable * @category services @@ -386,11 +418,26 @@ export const layerStoreMappingStatic = (options: { */ export class Storage extends Context.Service + /** + * Lowest assigned sequence (default 0). Overestimating skips entries. + */ + readonly firstSequence?: number | undefined readonly getOrCreateSessionAuthBinding: ( publicKey: string, signingPublicKey: Uint8Array ) => Effect.Effect> - readonly entriesAfter: (storeId: StoreId, entry: Entry) => Effect.Effect> + /** + * Returns the stored entries at or after `entry`, in sequence order. + * + * The server also passes the incoming `batch`, sorted and starting with + * `entry`. Implementations may use it to return only the entries that share + * an id, or an event and primary key, with a batch entry. + */ + readonly entriesAfter: ( + storeId: StoreId, + entry: Entry, + batch?: Arr.NonEmptyReadonlyArray + ) => Effect.Effect> readonly write: ( storeId: StoreId, entries: ReadonlyArray @@ -411,43 +458,83 @@ const constEmptyPrivateKey = Redacted.make(new Uint8Array(32)) const makeServerWriteIdentityPublicKey = (storeId: StoreId): string => `effect-eventlog-server-write:${storeId}` -const entriesAfter = (journal: Array, startSequence: number): ReadonlyArray => - journal.filter((entry) => entry.remoteSequence >= startSequence) +// The memory journal is append-only with strictly increasing sequences. +const entriesAfter = (journal: Array, startSequence: number): ReadonlyArray => { + let low = 0 + let high = journal.length + while (low < high) { + const mid = Math.floor((low + high) / 2) + if (journal[mid].remoteSequence >= startSequence) { + high = mid + } else { + low = mid + 1 + } + } + return journal.slice(low) +} -const toConflicts = ( - history: ReadonlyArray, - originEntry: Entry -): [duplicate: boolean, conflicts: Array, newHistory: Array] => { - let duplicate = false +class KeyIndex { + private readonly byEvent = new Map>>() - for (let i = 0; i < history.length; i++) { - const entry = history[i] - if (entry.createdAtMillis < originEntry.createdAtMillis) { - continue - } else if (entry.idString === originEntry.idString) { - duplicate = true - continue + add(event: string, primaryKey: string, value: A): void { + let byKey = this.byEvent.get(event) + if (byKey === undefined) { + byKey = new Map() + this.byEvent.set(event, byKey) + } + const values = byKey.get(primaryKey) + if (values === undefined) { + byKey.set(primaryKey, [value]) + } else { + values.push(value) } + } - const newHistory = history.slice(i) - let conflicts: Array = [] - for (let j = 0; j < newHistory.length; j++) { - const scannedEntry = newHistory[j]! - if (scannedEntry.event === originEntry.event && scannedEntry.primaryKey === originEntry.primaryKey) { - conflicts.push(scannedEntry) - } + get(event: string, primaryKey: string): ReadonlyArray | undefined { + return this.byEvent.get(event)?.get(primaryKey) + } +} + +/** + * Indexes the history returned by `Storage.entriesAfter` for one batch. A + * conflict is a stored entry with the same event and primary key and a + * strictly greater id, reported in id order. Batch entries are never added, so + * they do not conflict with each other. + */ +class HistoryIndex { + private readonly ids = new Set() + private readonly byKey = new KeyIndex() + + constructor(history: ReadonlyArray) { + // Storage returns history in sequence order; conflicts are reported in id order. + for (const entry of Arr.sort(history, Entry.Order)) { + this.ids.add(entry.idString) + this.byKey.add(entry.event, entry.primaryKey, entry) } - return [duplicate, conflicts, newHistory] } - return [duplicate, [], []] + has(entry: Entry): boolean { + return this.ids.has(entry.idString) + } + + conflicts(entry: Entry): Array { + const stored = this.byKey.get(entry.event, entry.primaryKey) + if (stored === undefined) return [] + return stored.filter((candidate) => Entry.Order(candidate, entry) > 0) + } +} + +type MemoryStore = { + readonly journal: Array + readonly byId: Map + readonly byKey: KeyIndex } type RegisteredCompactor = { readonly events: ReadonlySet readonly effect: (options: { readonly entries: ReadonlyArray - readonly write: (entry: Entry) => Effect.Effect + readonly write: (entry: Entry, source?: Entry) => Effect.Effect }) => Effect.Effect } @@ -513,6 +600,12 @@ const toCompactedRemoteEntries = (options: { * entries when the replacement count can be mapped back to increasing remote * sequence numbers; otherwise the original entries are kept. * + * Outputs are ordered by the position of `source ?? entry` within the run, as in + * client replay. Unknown positions go to the start; ties keep write order. + * + * Client replay groups the whole batch per compactor; the server calls each + * compactor per contiguous run, so batch boundaries can differ. + * * @stability unstable * @category compaction * @since 4.0.0 @@ -539,7 +632,6 @@ export const compactBacklog = Effect.fnUntraced(function*(options: { const entries: Array = [remoteEntry.entry] const remoteGroup: Array = [remoteEntry] - const compacted: Array = [] index++ while (index < options.remoteEntries.length) { @@ -553,25 +645,29 @@ export const compactBacklog = Effect.fnUntraced(function*(options: { index++ } + const positions = new Map() + const compacted: Array> = [] + for (let i = 0; i < entries.length; i++) { + positions.set(entries[i].idString, i) + compacted.push([]) + } yield* compactor.effect({ entries, - write(entry) { + write(entry, source) { return Effect.sync(() => { - compacted.push(entry) + compacted[positions.get((source ?? entry).idString) ?? 0].push(entry) }) } }).pipe(Effect.orDie) const projected = toCompactedRemoteEntries({ - compacted, + compacted: compacted.flat(), remoteEntries: remoteGroup }) - if (projected === undefined) { - compactedRemoteEntries.push(...remoteGroup) - continue + for (const entry of projected ?? remoteGroup) { + compactedRemoteEntries.push(entry) } - compactedRemoteEntries.push(...projected) } return compactedRemoteEntries @@ -591,25 +687,16 @@ export const compactBacklog = Effect.fnUntraced(function*(options: { * @since 4.0.0 */ export const makeStorageMemory: Effect.Effect = Effect.gen(function*() { - const knownIds = new Map>() - const journals = new Map>() + const stores = new Map() const sessionAuthBindings = new Map>() const remoteId = makeRemoteIdUnsafe() - const ensureKnownIds = (storeId: StoreId): Map => { - let storeKnownIds = knownIds.get(storeId) - if (storeKnownIds) return storeKnownIds - storeKnownIds = new Map() - knownIds.set(storeId, storeKnownIds) - return storeKnownIds - } - - const ensureJournal = (storeId: StoreId): Array => { - let journal = journals.get(storeId) - if (journal) return journal - journal = [] - journals.set(storeId, journal) - return journal + const ensureStore = (storeId: StoreId): MemoryStore => { + let store = stores.get(storeId) + if (store) return store + store = { journal: [], byId: new Map(), byKey: new KeyIndex() } + stores.set(storeId, store) + return store } const pubsubs = yield* RcMap.make({ @@ -622,15 +709,13 @@ export const makeStorageMemory: Effect.Effect) { - const sequenceNumbers: Array = [] const committed: Array = [] - const storeKnownIds = ensureKnownIds(storeId) - const journal = ensureJournal(storeId) - let lastSequenceNumber = Arr.last(journal).pipe( + const store = ensureStore(storeId) + let lastSequenceNumber = Arr.last(store.journal).pipe( Option.map((entry) => entry.remoteSequence), Option.getOrElse(() => 0) ) - if (entries.some((entry) => storeKnownIds.has(entry.idString))) { + if (entries.some((entry) => store.byId.has(entry.idString))) { return yield* Effect.die("Duplicate entries") } @@ -640,10 +725,10 @@ export const makeStorageMemory: Effect.Effect Effect.sync(() => { const existing = sessionAuthBindings.get(publicKey) @@ -663,10 +749,27 @@ export const makeStorageMemory: Effect.Effect + entriesAfter: (storeId, oldest, batch) => Effect.sync(() => { - const journal = ensureJournal(storeId) - return journal.filter((e) => Entry.Order(e.entry, entry) >= 0).map((e) => e.entry) + const store = ensureStore(storeId) + if (batch === undefined) { + return store.journal + .filter((remoteEntry) => Entry.Order(remoteEntry.entry, oldest) >= 0) + } + const found = new Set() + const scanned = new Set>() + for (const entry of batch) { + const known = store.byId.get(entry.idString) + if (known !== undefined) found.add(known) + const sameKey = store.byKey.get(entry.event, entry.primaryKey) + if (sameKey === undefined || scanned.has(sameKey)) continue + scanned.add(sameKey) + for (const remoteEntry of sameKey) { + if (Entry.Order(remoteEntry.entry, oldest) >= 0) found.add(remoteEntry) + } + } + return Array.from(found) + .sort((a, b) => a.remoteSequence - b.remoteSequence) }), write, changes: Effect.fnUntraced(function*({ storeId, startSequence, compactors }) { @@ -674,7 +777,7 @@ export const makeStorageMemory: Effect.Effect 0 ? backlog[backlog.length - 1].remoteSequence : startSequence - 1 diff --git a/repos/effect/packages/effect/src/eventlog/EventLogSessionAuth.ts b/packages/effect/src/eventlog/EventLogSessionAuth.ts similarity index 100% rename from repos/effect/packages/effect/src/eventlog/EventLogSessionAuth.ts rename to packages/effect/src/eventlog/EventLogSessionAuth.ts diff --git a/packages/effect/src/eventlog/EventRow.ts b/packages/effect/src/eventlog/EventRow.ts new file mode 100644 index 000000000..9d09e9368 --- /dev/null +++ b/packages/effect/src/eventlog/EventRow.ts @@ -0,0 +1,706 @@ +/** + * Mutable rows that converge across event-log replicas. + * + * Each row type has one event with `set`, `patch` and `remove` payloads, keyed + * by one or more of its fields. Pass the result of `make` to `EventGroup.add`. + * Its `set`, `patch` and `remove` constructors return `{ event, payload }`, + * which spreads into `EventLog.write` and serves as an item of + * `EventLog.writeMany` or an argument of an `EventLog.makeClient` client. + * + * `handler` resolves conflicts per top-level field and calls at most one + * storage callback. Replicas converge to the result of applying entries in + * id order. `compaction` drops entries that newer ones shadow. + * + * **Details** + * + * Replay decodes entries and stored conflicts with the current row schema, + * which must accept every encoding previously written: + * + * - New fields must accept absence, for example with + * `Schema.withDecodingDefaultKey`. Nullable fields alone reject old `set` + * entries. Patches never apply defaults. + * - Removing a field is safe. Old entries decode without it. + * - To change a field's type, keep its encoded type and change its decoded + * type with a transformation, such as `Schema.NumberFromString` for a field + * that used to be a string. + * - Making a field required, renaming it, narrowing its type, changing its + * encoded type or changing the key breaks old entries. That includes + * widening a field to a union of its old and new types, because union + * encodings are tagged. A rename keeps old entries readable only if the + * field has an explicit `SchemaBinary.fieldId`. + * - Key-field schemas must decode written values to ones with the same + * primary key, since replicas derive the key from decoded values. + * + * Conflicts are scoped to one row; parent removal does not resolve concurrent + * child writes. For cascading deletion, retain a removed-parent marker and + * delete its children in the parent `remove` callback. The `remove` callback + * writes the marker even when the parent was never stored, since compaction + * or a newer `remove` can skip its `set`, and the marker must not depend on + * the old row. A parent `patch` treats a marker as a missing row. Child `set` + * and `patch` callbacks must skip marked parents but allow parents not yet + * received. Make the parent id part of the child key, as in + * `fields: ["parentId", "id"]`, so a child cannot move between parents. This + * converges only if parent deletion is final (no key reuse). Queries must + * exclude marked parents. Retain markers unless the app defines a safe cutoff + * for removing them. + * + * **Example** (Syncing grocery rows) + * + * ```ts + * import { Effect, Schema } from "effect" + * import { EventGroup, EventLog, EventRow } from "effect/eventlog" + * + * const Grocery = EventRow.make("grocery", { + * key: "id", + * schema: Schema.Struct({ + * id: Schema.String, + * name: Schema.String, + * completed: Schema.Boolean + * }) + * }) + * + * const Groceries = EventGroup.empty.add(Grocery) + * + * const store = new Map() + * + * export const GroceriesLive = EventLog.group(Groceries, (handlers) => + * handlers.handle( + * "grocery", + * EventRow.handler(Grocery, { + * set: (row) => Effect.sync(() => store.set(row.id, row)), + * patch: (id, fields) => + * Effect.sync(() => { + * const row = store.get(id) + * if (row !== undefined) store.set(id, { ...row, ...fields }) + * }), + * remove: (id) => Effect.sync(() => store.delete(id)) + * }) + * )) + * + * export const complete = Effect.fnUntraced(function*(id: string) { + * const log = yield* EventLog.EventLog + * yield* log.write({ + * schema: EventLog.schema(Groceries), + * ...Grocery.patch(id, { completed: true }) + * }) + * }) + * ``` + * + * **Example** (Evolving a row schema) + * + * ```ts import.meta.vitest + * import { Effect, Schema } from "effect" + * import { EventGroup, EventRow } from "effect/eventlog" + * + * const ItemV1 = EventRow.make("item", { + * key: "id", + * schema: Schema.Struct({ id: Schema.String, quantity: Schema.String }) + * }) + * + * // Adds a nullable field and reads the old string quantity as a number. + * const ItemV2 = EventRow.make("item", { + * key: "id", + * schema: Schema.Struct({ + * id: Schema.String, + * quantity: Schema.NumberFromString, + * note: Schema.NullOr(Schema.String).pipe( + * Schema.withDecodingDefaultKey(Effect.succeed(null)) + * ) + * }) + * }) + * + * const v1 = EventGroup.empty.add(ItemV1).events.item.payloadSchemaBinary + * const v2 = EventGroup.empty.add(ItemV2).events.item.payloadSchemaBinary + * + * const written = Schema.encodeUnknownSync(v1)(ItemV1.set({ id: "a", quantity: "3" }).payload) + * const decoded = Schema.decodeUnknownSync(v2)(written) + * decoded // => { _tag: "set", row: { id: "a", quantity: 3, note: null } } + * ``` + * + * @stability unstable + * @since 4.0.6 + */ +import * as Context from "../Context.ts" +import * as Effect from "../Effect.ts" +import * as SchemaBinary from "../encoding/SchemaBinary.ts" +import * as Layer from "../Layer.ts" +import * as Result from "../Result.ts" +import * as Schema from "../Schema.ts" +import * as SchemaGetter from "../SchemaGetter.ts" +import * as SchemaIssue from "../SchemaIssue.ts" +import * as SchemaParser from "../SchemaParser.ts" +import * as Struct from "../Struct.ts" +import type * as Event from "./Event.ts" +import type * as EventGroup from "./EventGroup.ts" +import { Entry } from "./EventJournal.ts" +import * as EventLog from "./EventLog.ts" +import type { StoreId } from "./EventLogMessage.ts" + +/** + * Required fields that can be part of a declared row key. + * + * @stability unstable + * @category utility types + * @since 4.0.6 + */ +export type RequiredField = + & { + [K in keyof Fields]: Fields[K] extends { readonly "~type.optionality": "optional" } ? never : K + }[keyof Fields] + & string + +/** + * Values of the declared key fields of a row. + * + * @stability unstable + * @category utility types + * @since 4.0.6 + */ +export type KeyValues = { + readonly [K in Names]: Schema.Schema.Type +} + +/** + * Required fields whose values are all strings, all numbers or all + * `Uint8Array`s, so their JSON encodings form keys without `derive`. + * + * @stability unstable + * @category utility types + * @since 4.0.6 + */ +export type DefaultKeyField = { + [K in RequiredField]: [Schema.Schema.Type] extends [string] ? K + : [Schema.Schema.Type] extends [number] ? K + : [Schema.Schema.Type] extends [Uint8Array] ? K + : never +}[RequiredField] + +/** + * A row key declared from one or more fields. + * + * **Details** + * + * The primary key is the result of `derive`, which must be deterministic and + * unique per row. Without it, the primary key is the JSON tuple of the + * fields' JSON encodings, in declaration order, which needs every field to be + * a {@link DefaultKeyField}. Other fields can collide in JSON, such as `null` + * and `undefined`, so they require `derive`. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type KeyDeclaration> = + & { readonly fields: readonly [Names, ...Array] } + & ([Names] extends [DefaultKeyField] ? + { readonly derive?: ((fields: KeyValues) => string) | undefined } + : { readonly derive: (fields: KeyValues) => string }) + +/** + * Primary key type of a row: the key value for a string-typed single-field + * key, and `string` otherwise. + * + * @stability unstable + * @category utility types + * @since 4.0.6 + */ +export type PrimaryKey = Schema.Schema.Type extends string + ? Schema.Schema.Type + : string + +/** + * Optional non-key fields of a `patch` payload. + * + * @stability unstable + * @category utility types + * @since 4.0.6 + */ +export type PatchFields = { + readonly [K in Exclude]: Schema.optionalKey +} + +/** + * Schema of a row event's payload, derived from the row schema. + * + * **Details** + * + * - `set` carries the full row, validated by the row schema. + * - `patch` carries the row key and the fields to write. An absent field is + * not written. The key itself cannot be patched. + * - `remove` carries the row key. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type PayloadSchema< + Fields extends Schema.Struct.Fields, + Key extends keyof Fields, + KeySchema extends Schema.Constraint = Fields[Key] +> = Schema.Union< + readonly [ + Schema.TaggedStruct<"set", { readonly row: Schema.Struct }>, + Schema.TaggedStruct<"patch", { + readonly key: KeySchema + readonly fields: Schema.Struct> + }>, + Schema.TaggedStruct<"remove", { readonly key: KeySchema }> + ] +> + +/** + * Decoded payload of a row event. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type Payload< + Fields extends Schema.Struct.Fields, + Key extends keyof Fields, + KeySchema extends Schema.Constraint = Fields[Key] +> = Schema.Schema.Type> + +/** + * A row event to write, as returned by the `set`, `patch` and `remove` + * constructors of a row. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export type Write< + Tag extends string, + Fields extends Schema.Struct.Fields, + Key extends keyof Fields, + KeySchema extends Schema.Constraint = Fields[Key] +> = { + readonly event: Tag + readonly payload: Payload +} + +/** + * A row type that syncs through an `EventLog`. + * + * **Details** + * + * Includes `EventGroup.add` options, the row schema, its key fields and + * constructors for the events to write. The row key is the event primary key; + * success is `void`. + * + * `Key` is the union of the key fields. `KeySchema` is the schema of the key + * in `patch` and `remove` payloads and callbacks: the field's schema for a + * single-field key, and a struct of the key fields for a declared key. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface EventRow< + out Tag extends string, + in out Fields extends Schema.Struct.Fields, + in out Key extends keyof Fields & string, + in out Error extends Schema.Top = typeof Schema.Never, + in out KeySchema extends Schema.Constraint = Fields[Key] +> { + readonly tag: Tag + readonly keyFields: ReadonlyArray + readonly schema: Schema.Struct + readonly primaryKey: (payload: Payload) => string + readonly payload: PayloadSchema + readonly success: typeof Schema.Void + readonly error: Error + + /** + * Computes the primary key of a row from its key fields. + */ + readonly keyOf: (fields: KeyValues) => PrimaryKey + + /** + * Replaces the whole row, creating it if missing. + */ + readonly set: (row: Schema.Struct["Type"]) => Write + + /** + * Writes fields of an existing row. + * + * **Details** + * + * A patch on a missing row does nothing. To clear a value, use a nullable + * field and write `null`. + * + * Absent fields stay unwritten, even with decoding defaults. Values whose + * encoding omits the key, such as `Option.none()` for + * `Schema.OptionFromOptionalKey`, fail encoding: replicas cannot distinguish + * them from unwritten fields. + */ + readonly patch: ( + key: Schema.Schema.Type, + fields: Schema.Struct>["Type"] + ) => Write + + /** + * Deletes the row. + */ + readonly remove: (key: Schema.Schema.Type) => Write +} + +/** + * Defines a row type as a single event. + * + * **Details** + * + * `key` names a required field, or declares required fields with an + * optional `derive` function. `patch`, `remove` and the handler callbacks take + * the key as typed values: the field's value for a single field, and a struct + * of the fields for a declaration. Key fields cannot be patched. To move a + * row, remove it and set it again. + * + * A single-field key must be a {@link DefaultKeyField}. Declare other fields + * with a `derive` function. + * + * `keyOf` computes the primary key that conflicts are grouped by: the value + * of a string single-field key, the result of `derive`, or else the JSON + * tuple of the key fields' JSON encodings. `-0` and `0` are the same key. + * + * Last-writer-wins applies to top-level fields. Nested values are atomic, so + * concurrent updates within one nested value do not merge. + * + * `error` is the error schema of the event. Handler callbacks can fail with it. + * + * **Gotchas** + * + * Do not compact row events with `EventLog.groupCompaction` or + * `EventLog.groupBatchCompaction`. Compaction runs before stored conflicts + * are read, so merging row ops loses or misapplies writes. Use + * {@link compaction}, which only drops entries that newer ones shadow. + * + * For multi-row operations, use `EventLog.writeMany` with one op per row. + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const make = < + const Tag extends string, + Fields extends Schema.Struct.Fields, + const Key extends DefaultKeyField = never, + const Declared extends RequiredField = never, + Error extends Schema.Top = typeof Schema.Never +>(tag: Tag, options: { + readonly key: Key | KeyDeclaration + readonly schema: Schema.Struct + readonly error?: Error | undefined +}): EventRow< + Tag, + Fields, + // A declaration also infers `Key`, as its constraint, so `Declared` decides. + // `NoInfer` keeps a contextual return type from inferring `Declared`. + [NoInfer] extends [never] ? NoInfer : NoInfer, + Error, + [NoInfer] extends [never] ? Fields[NoInfer] + : Schema.Struct<{ readonly [K in NoInfer]: Fields[K] }> +> => { + const fields = options.schema.fields + const key = typeof options.key === "string" + ? scalarKey(options.key, fields[options.key]) + : declaredKey(options.key as KeyDeclaration, fields) + const patchFields: Record = {} + for (const name of Object.keys(fields)) { + if (!key.fields.includes(name)) patchFields[name] = patchField(fields[name]) + } + const payload = Schema.Union([ + Schema.TaggedStruct("set", { row: options.schema }), + Schema.TaggedStruct("patch", { key: key.schema, fields: Schema.Struct(patchFields) }), + Schema.TaggedStruct("remove", { key: key.schema }) + ]) + return { + tag, + keyFields: key.fields, + schema: options.schema, + keyOf: key.primaryKey, + [keyFromId]: key.fromRow, + primaryKey: (payload: Op) => key.primaryKey(payload._tag === "set" ? payload.row : key.toValues(payload.key)), + payload, + success: Schema.Void, + error: options.error ?? Schema.Never, + set: (row: unknown) => ({ event: tag, payload: { _tag: "set", row } }), + patch: (key: unknown, fields: unknown) => ({ event: tag, payload: { _tag: "patch", key, fields } }), + remove: (key: unknown) => ({ event: tag, payload: { _tag: "remove", key } }) + } as any +} + +/** + * Builds the handler for a row event, to pass to `handlers.handle`. + * + * **Details** + * + * Resolves newer conflicts in id order and calls at most one callback with + * typed values and key, never conflicts. + * + * Newer writes take precedence per top-level field. Empty patches and entries + * with nothing left to write skip the callback but are still journaled and synced. + * A late `set` retains newer patches, including when the row is missing. + * + * Decodes stored entry bytes so local writes and replay pass the same values + * to callbacks, stripping keys outside the schema at any depth. Requires the + * payload schema's decoding services, plus any services the callbacks require. + * Callbacks may fail with the event's error type; success is always `void`. + * + * **Gotchas** + * + * Convergence requires this callback contract: + * + * - `set` replaces the whole row, creating it if it is missing. + * - `patch` updates an existing row and does nothing if the row is missing. + * Upserting can resurrect removed rows on only some replicas. + * - `remove` is idempotent. + * + * @stability unstable + * @category handlers + * @since 4.0.6 + */ +export const handler = < + Tag extends string, + Fields extends Schema.Struct.Fields, + Key extends keyof Fields & string, + Error extends Schema.Top, + KeySchema extends Schema.Constraint, + RSet = never, + RPatch = never, + RRemove = never +>( + row: EventRow, + callbacks: { + readonly set: ( + row: Schema.Struct["Type"], + key: Schema.Schema.Type + ) => Effect.Effect + readonly patch: ( + key: Schema.Schema.Type, + fields: Schema.Struct>["Type"] + ) => Effect.Effect + readonly remove: (key: Schema.Schema.Type) => Effect.Effect + } +): (options: { + readonly storeId: StoreId + readonly payload: Payload + readonly entry: Entry + readonly conflicts: ReadonlyArray<{ + readonly entry: Entry + readonly payload: Payload + }> +}) => Effect.Effect> => { + const decode = Schema.decodeUnknownEffect(SchemaBinary.toCodec(row.payload)) + const keyFrom: RowKey["fromRow"] = (row as any)[keyFromId] + return (options) => + Effect.flatMap( + Effect.orDie(decode(options.entry.payload)), + (payload): Effect.Effect => { + const op = resolve(payload as Op, options.conflicts as ReadonlyArray<{ readonly payload: Op }>) + if (op === undefined) return Effect.void + switch (op._tag) { + case "set": + return callbacks.set(op.row as any, keyFrom(op.row) as any) + case "patch": + return Object.keys(op.fields).length === 0 ? Effect.void : callbacks.patch(op.key as any, op.fields as any) + case "remove": + return callbacks.remove(op.key as any) + } + } + ).pipe(Effect.asVoid) +} + +/** + * Registers a compactor for one row event of a group. + * + * **Details** + * + * Drops entries superseded within the same remote batch and row by a newer + * `set` or `remove`. A `patch` is also dropped when newer patches together + * overwrite all its fields. Kept entries retain their ids and stored conflicts. + * + * A `set` or `remove` followed only by patches is kept: without it, the + * patches would apply to a different row, or to none. Entries that fail to + * decode are kept. + * + * Leaves other events' compactors unchanged. Register one compactor per row + * event. + * + * @stability unstable + * @category compaction + * @since 4.0.6 + */ +export const compaction = < + Events extends Event.Any, + Tag extends Event.Tag, + Fields extends Schema.Struct.Fields, + Key extends keyof Fields & string, + Error extends Schema.Top, + KeySchema extends Schema.Constraint +>( + group: EventGroup.EventGroup, + row: EventRow +): Layer.Layer> => + Layer.effectDiscard(Effect.gen(function*() { + const registry = yield* EventLog.Registry + const services = yield* Effect.context>() + const decode = Schema.decodeUnknownEffect(group.events[row.tag].payloadSchemaBinary) + yield* registry.registerCompaction({ + events: [row.tag], + effect: Effect.fnUntraced(function*({ entries, write }) { + const ops = new Map() + for (const entry of entries) { + const payload = yield* Effect.result( + decode(entry.payload).pipe(Effect.updateContext((input) => Context.merge(services, input))) + ) as Effect.Effect> + if (Result.isSuccess(payload)) ops.set(entry, payload.success) + } + const shadowed = shadowedEntries(entries, ops) + for (const entry of entries) { + if (!shadowed.has(entry)) yield* write(entry) + } + }) + }) + })) + +const shadowedEntries = (entries: ReadonlyArray, ops: ReadonlyMap): Set => { + const shadowed = new Set() + const byKey = new Map>() + for (const entry of entries) { + const row = byKey.get(entry.primaryKey) + if (row === undefined) byKey.set(entry.primaryKey, [entry]) + else row.push(entry) + } + for (const row of byKey.values()) { + row.sort(Entry.Order) + let replaced = false + let patched = false + const written = new Set() + for (let i = row.length - 1; i >= 0; i--) { + const op = ops.get(row[i]) + if (op === undefined) continue + if (op._tag !== "patch") { + if (replaced) shadowed.add(row[i]) + replaced = true + continue + } + const names = Object.keys(op.fields) + if (replaced || (patched && names.every((name) => written.has(name)))) shadowed.add(row[i]) + patched = true + for (const name of names) written.add(name) + } + } + return shadowed +} + +const keyFromId = "~effect/eventlog/EventRow/keyFrom" + +type Row = Readonly> + +// How a row's key is carried and derived, for a single key field or a declaration. +interface RowKey { + readonly fields: ReadonlyArray + // Schema of the key in `patch` and `remove` payloads. + readonly schema: Schema.Constraint + // The key as payloads and callbacks carry it. + readonly fromRow: (row: Row) => unknown + // The key field values a carried key stands for. + readonly toValues: (key: unknown) => Row + readonly primaryKey: (values: Row) => string +} + +const scalarKey = (name: string, field: Schema.Constraint): RowKey => ({ + fields: [name], + schema: field, + fromRow: (row) => row[name], + toValues: (key) => ({ [name]: key }), + primaryKey: jsonKey([[name, field]], true) +}) + +const declaredKey = (declaration: KeyDeclaration, fields: Schema.Struct.Fields): RowKey => { + const names = declaration.fields + const derive = declaration.derive + return { + fields: names, + schema: Schema.Struct(Struct.pick(fields, names) as Schema.Struct.Fields), + fromRow: (row) => Struct.pick(row, names), + toValues: (key) => key as Row, + primaryKey: derive !== undefined + ? (row) => derive(Struct.pick(row, names) as any) + : jsonKey(names.map((name) => [name, fields[name]]), false) + } +} + +// A string single-field key is its own primary key. Otherwise the key is the +// JSON tuple of the fields' JSON encodings, distinct for distinct values of +// `DefaultKeyField`s. A field holds one kind of value, so a raw string never +// meets a tuple from the same field. +const jsonKey = (fields: ReadonlyArray, single: boolean) => { + let encoders: ReadonlyArray unknown]> | undefined + return (row: Row): string => { + if (single && typeof row[fields[0][0]] === "string") return row[fields[0][0]] as string + encoders ??= fields.map(([name, field]) => + [name, Schema.encodeUnknownSync(Schema.toCodecJson(field as Schema.Codec))] as const + ) + return JSON.stringify(encoders.map(([name, encode]) => encode(row[name]))) + } +} + +// Run the field codec only for present keys, bypassing missing-key defaults. +const patchField = (field: Schema.Constraint): Schema.Constraint => { + const decode = SchemaParser.decodeUnknownEffect(field) + // The struct wrapper detects encodings that drop a written key. + const encode = SchemaParser.encodeUnknownEffect(Schema.Struct({ value: field })) + return Schema.optionalKey(Schema.toEncoded(field)).pipe(Schema.decodeTo(Schema.optionalKey(Schema.toType(field)), { + decode: SchemaGetter.transformEffect(decode), + encode: SchemaGetter.transformEffect((value, options) => + Effect.flatMap(encode({ value }, options), (encoded) => + Object.hasOwn(encoded, "value") + ? Effect.succeed(encoded.value) + : Effect.fail( + new SchemaIssue.Forbidden({ message: "A patch cannot write a value whose encoding omits the key" }, value) + )) + ) + })) +} + +type DecodingServices< + Fields extends Schema.Struct.Fields, + Key extends keyof Fields, + KeySchema extends Schema.Constraint +> = PayloadSchema["DecodingServices"] + +type Op = + | { readonly _tag: "set"; readonly row: Row } + | { readonly _tag: "patch"; readonly key: unknown; readonly fields: Row } + | { readonly _tag: "remove"; readonly key: unknown } + +const compose = (x: Op, y: Op): Op => { + if (y._tag !== "patch") return y + switch (x._tag) { + case "set": + return { _tag: "set", row: { ...x.row, ...y.fields } } + case "patch": + return { _tag: "patch", key: y.key, fields: { ...x.fields, ...y.fields } } + case "remove": + return x + } +} + +const resolve = (entry: Op, conflicts: ReadonlyArray<{ readonly payload: Op }>): Op | undefined => { + if (conflicts.length === 0) return entry + let newer = conflicts[0].payload + for (let i = 1; i < conflicts.length; i++) { + newer = compose(newer, conflicts[i].payload) + } + if (newer._tag !== "patch") return undefined + switch (entry._tag) { + case "set": + return { _tag: "set", row: { ...entry.row, ...newer.fields } } + case "patch": + return { _tag: "patch", key: entry.key, fields: Struct.omit(entry.fields, Object.keys(newer.fields)) } + case "remove": + return entry + } +} diff --git a/packages/effect/src/eventlog/SqlEventJournal.ts b/packages/effect/src/eventlog/SqlEventJournal.ts new file mode 100644 index 000000000..20b00ba7a --- /dev/null +++ b/packages/effect/src/eventlog/SqlEventJournal.ts @@ -0,0 +1,677 @@ +/** + * SQL-backed event journal with per-remote upload checkpoints. + * + * @stability unstable + * @since 4.0.0 + */ +import * as Arr from "../Array.ts" +import * as Clock from "../Clock.ts" +import * as Effect from "../Effect.ts" +import * as Uuid from "../internal/uuid.ts" +import * as Layer from "../Layer.ts" +import * as PubSub from "../PubSub.ts" +import * as Schema from "../Schema.ts" +import * as SchemaTransformation from "../SchemaTransformation.ts" +import * as SqlClient from "../sql/SqlClient.ts" +import * as SqlError from "../sql/SqlError.ts" +import * as SqlSchema from "../sql/SqlSchema.ts" +import * as EventJournal from "./EventJournal.ts" +import { ConflictIndex } from "./internal/conflicts.ts" + +type WriteFromRemoteOptions = Parameters[0] + +/** + * Creates an `EventJournal` backed by a SQL database. + * + * **Details** + * + * Maintains `entryTable`, `${entryTable}_sequence`, `${entryTable}_sources`, + * `${remotesTable}_checkpoints` and `remoteCursorsTable` (default + * `${remotesTable}_cursors`) using the configured `SqlClient`. + * + * **Gotchas** + * + * Creating or migrating the schema requires a writable connection. Migrated + * databases can be reopened read-only. + * + * Journals from earlier releases keep one `remotesTable` row per entry per + * remote. Opening one replaces those rows with entry sources and drops the + * table; an entry known to several remotes keeps one of them as its source. + * + * Upload acknowledgements do not advance past unseen entries. + * + * @stability unstable + * @category constructors + * @since 4.0.0 + */ +export const make = (options?: { + readonly entryTable?: string + readonly remotesTable?: string + readonly remoteCursorsTable?: string +}): Effect.Effect< + EventJournal.EventJournal["Service"], + SqlError.SqlError, + SqlClient.SqlClient +> => + Effect.gen(function*() { + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + + const entryTable = options?.entryTable ?? "effect_event_journal" + const remotesTable = options?.remotesTable ?? "effect_event_remotes" + const remoteCursorsTable = options?.remoteCursorsTable ?? `${remotesTable}_cursors` + + const sequenceTable = `${entryTable}_sequence` + const sourcesTable = `${entryTable}_sources` + const checkpointsTable = `${remotesTable}_checkpoints` + + const entryTableSql = sql(entryTable) + const remotesTableSql = sql(remotesTable) + const sequenceTableSql = sql(sequenceTable) + const sourcesTableSql = sql(sourcesTable) + const checkpointsTableSql = sql(checkpointsTable) + const remoteCursorsTableSql = sql(remoteCursorsTable) + + // to_regclass folds unquoted names to lowercase, so pass the quoted identifier. + const quoted = (table: string) => sql`${sql(table)}`.compile(true)[0] + + const tableExists = (table: string) => + sql.onDialectOrElse({ + // Reading the catalog refreshes cached names after concurrent DDL. + pg: () => sql`SELECT 1 AS found FROM pg_class WHERE oid = to_regclass(${quoted(table)})`, + mysql: () => + sql`SELECT 1 AS found FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${table}`, + mssql: () => sql`SELECT 1 AS found WHERE OBJECT_ID(${table}, 'U') IS NOT NULL`, + orElse: () => sql`SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = ${table}` + }).pipe(Effect.map((rows) => rows.length > 0)) + + const createSchema = Effect.gen(function*() { + const eventIndex = sql(`${entryTable}_event_idx`) + const timestampIndex = sql(`${entryTable}_timestamp_idx`) + + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${entryTableSql} ( + id BYTEA PRIMARY KEY, + event TEXT NOT NULL, + primary_key TEXT NOT NULL, + payload BYTEA NOT NULL, + timestamp BIGINT NOT NULL + )`, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${entryTableSql} ( + id BINARY(16) PRIMARY KEY, + event TEXT NOT NULL, + primary_key TEXT NOT NULL, + payload BLOB NOT NULL, + timestamp BIGINT NOT NULL + )`, + mssql: () => + sql` + CREATE TABLE IF NOT EXISTS ${entryTableSql} ( + id UNIQUEIDENTIFIER PRIMARY KEY, + event NVARCHAR(MAX) NOT NULL, + primary_key NVARCHAR(MAX) NOT NULL, + payload VARBINARY(MAX) NOT NULL, + timestamp BIGINT NOT NULL + )`, + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${entryTableSql} ( + id BLOB PRIMARY KEY, + event TEXT NOT NULL, + primary_key TEXT NOT NULL, + payload BLOB NOT NULL, + timestamp INTEGER NOT NULL + )` + }) + + // Insertion sequences let uploads include older entries imported later. + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${sequenceTableSql} ( + sequence BIGSERIAL PRIMARY KEY, + entry_id BYTEA NOT NULL UNIQUE + )`, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${sequenceTableSql} ( + sequence BIGINT AUTO_INCREMENT PRIMARY KEY, + entry_id BINARY(16) NOT NULL UNIQUE + )`, + mssql: () => + sql` + CREATE TABLE ${sequenceTableSql} ( + sequence BIGINT IDENTITY(1,1) PRIMARY KEY, + entry_id UNIQUEIDENTIFIER NOT NULL UNIQUE + )`, + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${sequenceTableSql} ( + sequence INTEGER PRIMARY KEY, + entry_id BLOB NOT NULL UNIQUE + )` + }) + + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${checkpointsTableSql} ( + remote_id BYTEA PRIMARY KEY, + sequence BIGINT NOT NULL + )`, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${checkpointsTableSql} ( + remote_id BINARY(16) PRIMARY KEY, + sequence BIGINT NOT NULL + )`, + mssql: () => + sql` + CREATE TABLE ${checkpointsTableSql} ( + remote_id UNIQUEIDENTIFIER PRIMARY KEY, + sequence BIGINT NOT NULL + )`, + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${checkpointsTableSql} ( + remote_id BLOB PRIMARY KEY, + sequence INTEGER NOT NULL + )` + }) + + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${remoteCursorsTableSql} ( + remote_id BYTEA PRIMARY KEY, + next_sequence INT NOT NULL + )`, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${remoteCursorsTableSql} ( + remote_id BINARY(16) PRIMARY KEY, + next_sequence INT NOT NULL + )`, + mssql: () => + sql` + CREATE TABLE ${remoteCursorsTableSql} ( + remote_id UNIQUEIDENTIFIER PRIMARY KEY, + next_sequence INT NOT NULL + )`, + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${remoteCursorsTableSql} ( + remote_id BLOB PRIMARY KEY, + next_sequence INT NOT NULL + )` + }) + + // The remote each imported entry came from, so uploads skip it. + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${sourcesTableSql} ( + entry_id BYTEA PRIMARY KEY, + remote_id BYTEA NOT NULL + )`, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${sourcesTableSql} ( + entry_id BINARY(16) PRIMARY KEY, + remote_id BINARY(16) NOT NULL + )`, + mssql: () => + sql` + CREATE TABLE ${sourcesTableSql} ( + entry_id UNIQUEIDENTIFIER PRIMARY KEY, + remote_id UNIQUEIDENTIFIER NOT NULL + )`, + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${sourcesTableSql} ( + entry_id BLOB PRIMARY KEY, + remote_id BLOB NOT NULL + )` + }) + + yield* sql.onDialectOrElse({ + // MySQL indexes TEXT columns by prefix only. + mysql: () => + Effect.andThen( + sql`CREATE INDEX ${eventIndex} ON ${entryTableSql} (event(191), primary_key(191), timestamp)`, + sql`CREATE INDEX ${timestampIndex} ON ${entryTableSql} (timestamp)` + ), + // B-tree entries are limited to about 2.7 KB, so index hashes of the + // unbounded columns. + pg: () => + Effect.andThen( + sql`CREATE INDEX IF NOT EXISTS ${eventIndex} ON ${entryTableSql} (md5(event), md5(primary_key), timestamp)`, + sql`CREATE INDEX IF NOT EXISTS ${timestampIndex} ON ${entryTableSql} (timestamp)` + ), + // NVARCHAR(MAX) columns cannot be index keys. + mssql: () => sql`CREATE INDEX ${timestampIndex} ON ${entryTableSql} (timestamp)`, + orElse: () => + Effect.andThen( + sql`CREATE INDEX IF NOT EXISTS ${eventIndex} ON ${entryTableSql} (event, primary_key, timestamp)`, + sql`CREATE INDEX IF NOT EXISTS ${timestampIndex} ON ${entryTableSql} (timestamp)` + ) + }) + + // Number existing entries in creation order. `WHERE true` keeps SQLite + // from parsing ON CONFLICT as a join constraint. + yield* sql` + INSERT INTO ${sequenceTableSql} (entry_id) + SELECT id FROM ${entryTableSql} WHERE true ORDER BY timestamp ASC, id ASC + ON CONFLICT DO NOTHING + ` + }) + + // Earlier releases kept a remotes row per entry per remote. An entry known + // to several remotes keeps one of them as its source, and a remote without + // a cursor resumes after its highest downloaded sequence. + const importRemotes = Effect.gen(function*() { + yield* sql` + INSERT INTO ${sourcesTableSql} (entry_id, remote_id) + SELECT entry_id, remote_id FROM ${remotesTableSql} WHERE true + ON CONFLICT DO NOTHING + ` + yield* sql` + INSERT INTO ${remoteCursorsTableSql} (remote_id, next_sequence) + SELECT remote_id, MAX(sequence) + 1 FROM ${remotesTableSql} GROUP BY remote_id + ON CONFLICT DO NOTHING + ` + yield* sql`DROP TABLE ${remotesTableSql}` + }) + + // Both steps run in one transaction, so a complete schema has the sources + // table and no remotes table. Checking that first lets complete databases + // open read-only; PostgreSQL also locks and rechecks so concurrent opens + // migrate once. + const schemaState = Effect.all({ sources: tableExists(sourcesTable), remotes: tableExists(remotesTable) }) + const isComplete = (state: { readonly sources: boolean; readonly remotes: boolean }) => + state.sources && !state.remotes + yield* Effect.gen(function*() { + if (isComplete(yield* schemaState)) return + yield* Effect.gen(function*() { + yield* sql.onDialectOrElse({ + pg: () => sql`SELECT pg_advisory_xact_lock(hashtext(${quoted(entryTable)}))`, + orElse: () => Effect.void + }) + const state = yield* schemaState + if (!state.sources) yield* createSchema + if (state.remotes) yield* importRemotes + }).pipe(sql.withTransaction) + }).pipe(withTracerDisabled) + + const decodeEntryRows = Schema.decodeUnknownEffect(EntryRowArray) + const decodeEntryIdRows = Schema.decodeUnknownEffect(EntryIdRowArray) + const toEntries = (rows: ReadonlyArray): ReadonlyArray => rows.map(toEntry) + // Sort timestamp ties by id bytes consistently across dialects. + const toOrderedEntries = (rows: ReadonlyArray): ReadonlyArray => + rows.map(toEntry).sort(EventJournal.Entry.Order) + + const insertEntries = SqlSchema.void({ + Request: EntryRowArray, + execute: (entries) => sql`INSERT INTO ${entryTableSql} ${sql.insert(entries)} ON CONFLICT DO NOTHING` + }) + const insertSources = (entries: ReadonlyArray, remoteId: EventJournal.RemoteId) => + sql`INSERT INTO ${sourcesTableSql} ${ + sql.insert(entries.map((entry) => ({ entry_id: entry.id, remote_id: remoteId }))) + } ON CONFLICT DO NOTHING` + // Serialize writers so checkpoints cannot skip out-of-order commits. + // Lock before callbacks in both paths to avoid projection-row deadlocks. + const lockWriters = sql.onDialectOrElse({ + pg: () => Effect.asVoid(sql`LOCK TABLE ${sequenceTableSql} IN SHARE ROW EXCLUSIVE MODE`), + orElse: () => Effect.void + }) + const insertSequence = (entries: ReadonlyArray) => + sql`INSERT INTO ${sequenceTableSql} ${ + sql.insert(entries.map((entry) => ({ entry_id: entry.id }))) + } ON CONFLICT DO NOTHING` + + const decodeCursorRows = Schema.decodeUnknownEffect(CursorRowArray) + + const readCursor = (remoteId: EventJournal.RemoteId) => + sql`SELECT next_sequence FROM ${remoteCursorsTableSql} WHERE remote_id = ${remoteId}`.pipe( + Effect.flatMap(decodeCursorRows), + Effect.map((rows) => rows.length > 0 ? rows[0].next_sequence : 0) + ) + + // Cursors only move forward. + const writeCursor = (remoteId: EventJournal.RemoteId, next: number) => + sql` + INSERT INTO ${remoteCursorsTableSql} ${sql.insert({ remote_id: remoteId, next_sequence: next })} + ON CONFLICT (remote_id) DO UPDATE SET next_sequence = excluded.next_sequence + WHERE ${remoteCursorsTableSql}.next_sequence < excluded.next_sequence + ` + + const pubsub = yield* PubSub.unbounded() + const clock = yield* Clock.Clock + + const writeFromRemote = Effect.fnUntraced(function*(options: WriteFromRemoteOptions): Effect.fn.Return< + { + readonly duplicateEntries: ReadonlyArray + }, + EventJournal.EventJournalError | Schema.SchemaError | SqlError.SqlError + > { + const entries = options.entries.map((remoteEntry) => remoteEntry.entry) + + yield* lockWriters + + const existingIds = new Set() + if (entries.length > 0) { + yield* sql`SELECT id FROM ${entryTableSql} WHERE ${ + sql.in( + "id", + entries.map((entry) => entry.id) + ) + }`.pipe( + Effect.flatMap(decodeEntryIdRows), + Effect.tap((rows) => + Effect.sync(() => { + for (const row of rows) { + existingIds.add(Uuid.stringify(row.id)) + } + }) + ) + ) + } + // Overlapping batches can repeat an entry; the first copy wins. + const incomingIds = new Set() + const uncommitted = options.entries.filter((entry) => { + const idString = entry.entry.idString + if (existingIds.has(idString) || incomingIds.has(idString)) return false + incomingIds.add(idString) + return true + }) + const duplicateEntries = options.entries + .filter((entry) => existingIds.has(entry.entry.idString)) + .map((entry) => entry.entry) + const compacted = options.compact + ? yield* options.compact(uncommitted) + : uncommitted.map((remoteEntry) => remoteEntry.entry) + + if (Arr.isReadonlyArrayNonEmpty(compacted)) { + const index = new ConflictIndex(compacted) + const primaryKeys = Array.from(new Set(compacted.map((entry) => entry.primaryKey))) + // Match the hash index; exact predicates below rule out hash collisions. + const hashed = sql.onDialectOrElse({ + pg: () => [ + sql`md5(event) IN (${sql.csv(index.events.map((event) => sql`md5(${event})`))})`, + sql`md5(primary_key) IN (${sql.csv(primaryKeys.map((primaryKey) => sql`md5(${primaryKey})`))})` + ], + orElse: () => [] + }) + const conflicts = sql.and([ + ...hashed, + sql.in("event", index.events), + sql.in("primary_key", primaryKeys), + // A coarse filter: the index compares ids exactly. + sql`timestamp >= ${index.oldestMillis}` + ]) + const stored = yield* sql`SELECT * FROM ${entryTableSql} WHERE ${conflicts}`.pipe( + Effect.flatMap(decodeEntryRows), + Effect.map(toEntries) + ) + for (const entry of stored) { + index.add(entry) + } + yield* options.effect(index.toBatch()) + } + + // Keep the incoming batch out of conflict queries and persist only after + // compaction and every callback have succeeded. + if (uncommitted.length > 0) { + const imported = uncommitted.map((remoteEntry) => remoteEntry.entry) + yield* insertEntries(imported.map(toEntryRow)) + yield* insertSequence(imported) + yield* insertSources(imported, options.remoteId) + } + // The change stream is ordered, so every earlier sequence is now known. + if (options.entries.length > 0) { + yield* writeCursor(options.remoteId, EventJournal.nextSequenceAfter(options.entries)) + } + yield* EventJournal.warnIfAhead(clock, uncommitted.map((remoteEntry) => remoteEntry.entry.id)) + + return { + duplicateEntries + } + }) + + // Other processes share the database but not the entry id sequence. Ids in + // the latest millisecond are few, and comparing them here avoids ordering + // id bytes in SQL. + const latestEntryIds = sql` + SELECT id FROM ${entryTableSql} WHERE timestamp = (SELECT MAX(timestamp) FROM ${entryTableSql}) + `.pipe( + Effect.flatMap(decodeEntryIdRows), + Effect.map((rows) => rows.map((row) => row.id)) + ) + + const writeMany: EventJournal.EventJournal["Service"]["writeMany"] = ({ effect, entries }) => + Effect.uninterruptibleMask((restore) => + Effect.gen(function*() { + yield* lockWriters + // Rows visible here include an enclosing transaction's uncommitted + // ones, which local ids must also sort after. + const committed = yield* EventJournal.makeEntries(entries, yield* latestEntryIds) + const result = yield* restore(effect(committed)).pipe( + Effect.mapError((error) => new CallbackFailure(error)) + ) + yield* insertEntries(committed.map(toEntryRow)) + yield* insertSequence(committed) + return { result, committed } + }).pipe( + sql.withTransaction, + Effect.mapError((error) => + error instanceof CallbackFailure + ? error.error + : error instanceof EventJournal.EventJournalError + ? error + : new EventJournal.EventJournalError({ cause: error, method: "writeMany" }) + ), + Effect.flatMap(({ committed, result }) => Effect.as(PubSub.publishAll(pubsub, committed), result)) + ) + ).pipe(withTracerDisabled) + + return EventJournal.EventJournal.of({ + entries: sql`SELECT * FROM ${entryTableSql} ORDER BY timestamp ASC`.pipe( + withTracerDisabled, + Effect.flatMap(decodeEntryRows), + Effect.map(toOrderedEntries), + Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "entries" })) + ), + entriesById: (ids) => + ids.length === 0 ? Effect.succeed([]) : sql`SELECT * FROM ${entryTableSql} WHERE ${sql.in("id", ids)}`.pipe( + withTracerDisabled, + Effect.flatMap(decodeEntryRows), + Effect.map((rows) => { + const byId = new Map(toEntries(rows).map((entry) => [entry.idString, entry])) + return ids.map((id) => byId.get(Uuid.stringify(id))) + }), + Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "entriesById" })) + ), + write: ({ effect, ...entry }) => writeMany({ entries: [entry], effect: (entries) => effect(entries[0]) }), + writeMany, + writeFromRemote: (options) => + writeFromRemote(options).pipe( + sql.withTransaction, + withTracerDisabled, + Effect.catchIf( + (e) => e._tag !== "EventJournalError", + (cause) => Effect.fail(new EventJournal.EventJournalError({ cause, method: "writeFromRemote" })) + ) + ), + withRemoteUncommited: Effect.fnUntraced( + function*(remoteId, f) { + const mapError = Effect.mapError((cause: unknown) => + new EventJournal.EventJournalError({ cause, method: "withRemoteUncommited" }) + ) + // Capture an upper bound so entries committed during upload remain pending. + const [{ checkpoint, latest }] = yield* sql` + SELECT + COALESCE((SELECT sequence FROM ${checkpointsTableSql} WHERE remote_id = ${remoteId}), 0) AS checkpoint, + COALESCE((SELECT MAX(sequence) FROM ${sequenceTableSql}), 0) AS latest + `.pipe(Effect.flatMap(decodeUploadBounds), mapError) + if (latest <= checkpoint) return yield* Effect.succeedNone + const entries = yield* sql` + SELECT e.* + FROM ${sequenceTableSql} s + JOIN ${entryTableSql} e ON e.id = s.entry_id + WHERE s.sequence > ${checkpoint} AND + s.sequence <= ${latest} AND + NOT EXISTS ( + SELECT 1 FROM ${sourcesTableSql} src WHERE src.entry_id = s.entry_id AND src.remote_id = ${remoteId} + ) + `.pipe( + Effect.flatMap(decodeEntryRows), + Effect.map(toOrderedEntries), + mapError + ) + // Uploads can complete out of order; the checkpoint only moves forward. + const saveCheckpoint = sql` + INSERT INTO ${checkpointsTableSql} ${sql.insert({ remote_id: remoteId, sequence: latest })} + ON CONFLICT (remote_id) DO UPDATE SET sequence = excluded.sequence + WHERE ${checkpointsTableSql}.sequence < excluded.sequence + `.pipe(mapError) + return yield* Effect.uninterruptibleMask((restore) => + restore(Arr.isReadonlyArrayNonEmpty(entries) ? Effect.asSome(f(entries)) : Effect.succeedNone).pipe( + Effect.tap(saveCheckpoint) + ) + ) + }, + withTracerDisabled + ), + writeRemoteAcks: Effect.fnUntraced( + function*({ acks, firstSequence, remoteId }) { + if (acks.length === 0) return + const next = yield* readCursor(remoteId) + yield* writeCursor(remoteId, EventJournal.advanceSequence(next, acks, firstSequence)) + }, + (_) => sql.withTransaction(_), + (_) => withTracerDisabled(_), + Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "writeRemoteAcks" })) + ), + nextRemoteSequence: (remoteId) => + readCursor(remoteId).pipe( + withTracerDisabled, + Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "nextRemoteSequence" })) + ), + changes: PubSub.subscribe(pubsub), + destroy: Effect.gen(function*() { + yield* sql`DROP TABLE ${entryTableSql}` + yield* sql`DROP TABLE ${sequenceTableSql}` + yield* sql`DROP TABLE ${sourcesTableSql}` + yield* sql`DROP TABLE ${checkpointsTableSql}` + yield* sql`DROP TABLE ${remoteCursorsTableSql}` + }).pipe( + withTracerDisabled, + Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "destroy" })) + ), + withLock(_storeId) { + return (effect) => + sql.withTransaction(effect).pipe( + Effect.catchIf(SqlError.isSqlError, Effect.die) + ) + } + }) + }) + +/** + * Provides a SQL-backed `EventJournal` from an existing `SqlClient` service. + * + * **Gotchas** + * + * Creating or migrating the schema requires a writable connection. + * + * @see {@link make} for constructing the SQL-backed service directly + * @see {@link EventJournal.layerMemory} for an in-memory `EventJournal` layer + * @see {@link EventJournal.layerIndexedDb} for an IndexedDB-backed `EventJournal` layer + * + * @stability unstable + * @category layers + * @since 4.0.0 + */ +export const layer = (options?: { + readonly entryTable?: string + readonly remotesTable?: string + readonly remoteCursorsTable?: string +}): Layer.Layer => + Layer.effect(EventJournal.EventJournal)(make(options)) + +const ArrayBuffer = Schema.instanceOf(globalThis.ArrayBuffer, { + expected: "ArrayBuffer" +}) + +const SqlUint8Array = Schema.Union([Schema.Uint8Array, ArrayBuffer]).pipe( + Schema.decodeTo( + Schema.Uint8Array, + SchemaTransformation.transform({ + decode: (value) => value instanceof globalThis.ArrayBuffer ? new globalThis.Uint8Array(value) : value, + encode: (value) => value + }) + ) +) + +const SqlEntryId = SqlUint8Array.pipe(Schema.decodeTo(EventJournal.EntryId)) + +const SqlNatural = Schema.Union([ + Schema.Natural, + Schema.FiniteFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), + Schema.BigInt.pipe( + Schema.decodeTo( + Schema.Natural, + SchemaTransformation.transform({ + decode: Number, + encode: BigInt + }) + ) + ) +]) + +const EntryRow = Schema.Struct({ + id: SqlEntryId, + event: Schema.String, + primary_key: Schema.String, + payload: SqlUint8Array, + timestamp: SqlNatural +}) + +const EntryRowArray = Schema.Array(EntryRow) +const decodeUploadBounds = Schema.decodeUnknownEffect(Schema.Tuple([ + Schema.Struct({ checkpoint: SqlNatural, latest: SqlNatural }) +])) +const EntryIdRowArray = Schema.Array(Schema.Struct({ id: SqlEntryId })) + +type EntryRow = Schema.Schema.Type + +const toEntry = (row: EntryRow): EventJournal.Entry => + new EventJournal.Entry({ + id: row.id, + event: row.event, + primaryKey: row.primary_key, + payload: row.payload + }, { disableChecks: true }) + +const toEntryRow = (entry: EventJournal.Entry): EntryRow => ({ + id: entry.id, + event: entry.event, + primary_key: entry.primaryKey, + payload: entry.payload, + timestamp: EventJournal.entryIdMillis(entry.id) +}) + +const CursorRowArray = Schema.Array(Schema.Struct({ next_sequence: SqlNatural })) + +const withTracerDisabled = Effect.withTracerEnabled(false) + +// Preserve callback errors without wrapping them as journal SQL errors. +class CallbackFailure { + readonly error: E + constructor(error: E) { + this.error = error + } +} diff --git a/packages/effect/src/eventlog/SqlEventLogServerEncrypted.ts b/packages/effect/src/eventlog/SqlEventLogServerEncrypted.ts new file mode 100644 index 000000000..6efa588d1 --- /dev/null +++ b/packages/effect/src/eventlog/SqlEventLogServerEncrypted.ts @@ -0,0 +1,476 @@ +/** + * Stores encrypted event-log server state in SQL. + * + * This module provides the durable `Storage` implementation used by + * `EventLogServerEncrypted` when entries should be stored without exposing + * plaintext event data to the database. It persists the server remote id, + * session authentication bindings, and encrypted batch tables, assigns stable + * sequence numbers, and streams changes. Clients remain responsible for + * encrypting writes and decrypting reads. + * + * @stability unstable + * @since 4.0.0 + */ +import * as Arr from "../Array.ts" +import type { NonEmptyArray } from "../Array.ts" +import * as Effect from "../Effect.ts" +import * as Layer from "../Layer.ts" +import * as PubSub from "../PubSub.ts" +import * as RcMap from "../RcMap.ts" +import * as Schema from "../Schema.ts" +import type * as Scope from "../Scope.ts" +import * as Semaphore from "../Semaphore.ts" +import * as SqlClient from "../sql/SqlClient.ts" +import type * as SqlError from "../sql/SqlError.ts" +import type * as Statement from "../sql/Statement.ts" +import * as Stream from "../Stream.ts" +import { type EntryId, makeRemoteIdUnsafe, type RemoteId } from "./EventJournal.ts" +import * as EventLogEncryption from "./EventLogEncryption.ts" +import type { WriteAck } from "./EventLogMessage.ts" +import * as EventLogServerEncrypted from "./EventLogServerEncrypted.ts" + +/** + * Creates encrypted event-log server `Storage` backed by SQL. + * + * **Details** + * + * It persists the server remote id, session authentication bindings, and encrypted + * batches in dialect-specific tables, creating per-identity/store batch and entry + * id tables as needed. + * + * **Gotchas** + * + * Storage does not retry sequence conflicts between competing instances. + * `EventLog.runRemote` retries failed writes; direct `EventLogRemote.write` + * callers must retry failures other than Forbidden themselves. Live notifications + * are local to each storage instance; clients on other instances must reconnect + * to read those writes. + * + * @stability unstable + * @category constructors + * @since 4.0.0 + */ +export const makeStorage = (options?: { + readonly entryTablePrefix?: string + readonly remoteIdTable?: string +}): Effect.Effect< + EventLogServerEncrypted.Storage["Service"], + SqlError.SqlError, + SqlClient.SqlClient | EventLogEncryption.EventLogEncryption | Scope.Scope +> => + Effect.gen(function*() { + const encryptions = yield* EventLogEncryption.EventLogEncryption + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + + const tablePrefix = options?.entryTablePrefix ?? "effect_events" + const remoteIdTable = options?.remoteIdTable ?? "effect_remote_id" + const sessionAuthBindingsTable = `${tablePrefix}_session_auth_bindings` + + const remoteIdTableSql = sql(remoteIdTable) + const sessionAuthBindingsTableSql = sql(sessionAuthBindingsTable) + + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} ( + remote_id BYTEA PRIMARY KEY + )`, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} ( + remote_id BINARY(16) PRIMARY KEY + )`, + mssql: () => + sql` + IF OBJECT_ID(${remoteIdTable}, N'U') IS NULL + CREATE TABLE ${remoteIdTableSql} ( + remote_id VARBINARY(16) PRIMARY KEY + )`, + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${remoteIdTableSql} ( + remote_id BLOB PRIMARY KEY + )` + }) + + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} ( + public_key TEXT PRIMARY KEY, + signing_public_key BYTEA NOT NULL + )`, + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} ( + public_key VARCHAR(191) PRIMARY KEY, + signing_public_key BINARY(32) NOT NULL + )`, + mssql: () => + sql` + IF OBJECT_ID(${sessionAuthBindingsTable}, N'U') IS NULL + CREATE TABLE ${sessionAuthBindingsTableSql} ( + public_key NVARCHAR(191) PRIMARY KEY, + signing_public_key VARBINARY(32) NOT NULL + )`, + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${sessionAuthBindingsTableSql} ( + public_key TEXT PRIMARY KEY, + signing_public_key BLOB NOT NULL + )` + }) + + const remoteId = yield* sql<{ remote_id: Uint8Array }>`SELECT remote_id FROM ${remoteIdTableSql}`.pipe( + Effect.flatMap((results) => { + if (results.length > 0) { + return Effect.succeed(results[0].remote_id as RemoteId) + } + const created = makeRemoteIdUnsafe() + return Effect.as( + sql`INSERT INTO ${remoteIdTableSql} (remote_id) VALUES (${created})`, + created + ) + }) + ) + + const resources = yield* RcMap.make({ + lookup: Effect.fnUntraced(function*(scopeKey: string) { + const scopeHash = (yield* encryptions.sha256String(new TextEncoder().encode(scopeKey))).slice(0, 16) + const table = `${tablePrefix}_${scopeHash}` + const tableSql = sql(table) + const idsTableSql = sql(`${table}_ids`) + const indexSql = sql(`${table}_last_sequence`) + + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE TABLE IF NOT EXISTS ${tableSql} ( + sequence INTEGER PRIMARY KEY, + last_sequence INTEGER NOT NULL, + iv BYTEA NOT NULL, + entry_ids BYTEA NOT NULL, + encrypted_batch BYTEA NOT NULL + )`.pipe( + Effect.andThen(sql`CREATE INDEX IF NOT EXISTS ${indexSql} ON ${tableSql} (last_sequence)`), + Effect.andThen(sql` + CREATE TABLE IF NOT EXISTS ${idsTableSql} ( + entry_id BYTEA PRIMARY KEY, + sequence INTEGER NOT NULL + )`) + ), + mysql: () => + sql` + CREATE TABLE IF NOT EXISTS ${tableSql} ( + sequence INT PRIMARY KEY, + last_sequence INT NOT NULL, + iv BINARY(12) NOT NULL, + entry_ids LONGBLOB NOT NULL, + encrypted_batch LONGBLOB NOT NULL, + INDEX (last_sequence) + )`.pipe( + Effect.andThen(sql` + CREATE TABLE IF NOT EXISTS ${idsTableSql} ( + entry_id BINARY(16) PRIMARY KEY, + sequence INT NOT NULL + )`) + ), + mssql: () => + sql` + IF OBJECT_ID(${table}, N'U') IS NULL + CREATE TABLE ${tableSql} ( + sequence INT PRIMARY KEY, + last_sequence INT NOT NULL, + iv VARBINARY(12) NOT NULL, + entry_ids VARBINARY(MAX) NOT NULL, + encrypted_batch VARBINARY(MAX) NOT NULL, + INDEX ${indexSql} NONCLUSTERED (last_sequence) + )`.pipe( + Effect.andThen(sql` + IF OBJECT_ID(${`${table}_ids`}, N'U') IS NULL + CREATE TABLE ${idsTableSql} ( + entry_id VARBINARY(16) PRIMARY KEY, + sequence INT NOT NULL + )`) + ), + orElse: () => + sql` + CREATE TABLE IF NOT EXISTS ${tableSql} ( + sequence INTEGER PRIMARY KEY, + last_sequence INTEGER NOT NULL, + iv BLOB NOT NULL, + entry_ids BLOB NOT NULL, + encrypted_batch BLOB NOT NULL + )`.pipe( + Effect.andThen(sql`CREATE INDEX IF NOT EXISTS ${indexSql} ON ${tableSql} (last_sequence)`), + Effect.andThen(sql` + CREATE TABLE IF NOT EXISTS ${idsTableSql} ( + entry_id BLOB PRIMARY KEY, + sequence INTEGER NOT NULL + )`) + ) + }) + + const pubsub = yield* Effect.acquireRelease( + PubSub.unbounded(), + PubSub.shutdown + ) + // Serializes writes within this storage instance. + const semaphore = yield* Semaphore.make(1) + return { pubsub, semaphore, tableSql, idsTableSql } as const + }, withTracerDisabled), + idleTimeToLive: "5 minutes" + }) + + // Returns how many of the ids were not stored yet. Stored ids keep their + // first sequence. + const insertNewIds = (idsTableSql: Statement.Identifier, rows: ReadonlyArray) => + sql.onDialectOrElse({ + mysql: () => + sql`INSERT IGNORE INTO ${idsTableSql} ${sql.insert(rows)}`.raw.pipe( + Effect.map((result) => (result as { affectedRows: number }).affectedRows) + ), + mssql: () => + sql` + MERGE ${idsTableSql} WITH (HOLDLOCK) AS target + USING (VALUES ${ + sql.join(",", false)(rows.map((row) => sql`(${row.entry_id}, ${row.sequence})`)) + }) AS source (entry_id, sequence) + ON target.entry_id = source.entry_id + WHEN NOT MATCHED THEN + INSERT (entry_id, sequence) VALUES (source.entry_id, source.sequence) + OUTPUT inserted.entry_id; + `.pipe(Effect.map((inserted) => inserted.length)), + orElse: () => + sql` + INSERT INTO ${idsTableSql} ${sql.insert(rows)} + ON CONFLICT DO NOTHING RETURNING entry_id + `.pipe(Effect.map((inserted) => inserted.length)) + }) + + const getSessionAuthBinding = (publicKey: string) => + sql` + SELECT public_key, signing_public_key + FROM ${sessionAuthBindingsTableSql} + WHERE public_key = ${publicKey} + `.pipe( + Effect.flatMap(decodeSessionAuthBindings), + Effect.map((rows) => { + const row = rows[0] + return row === undefined ? undefined : row.signing_public_key as Uint8Array + }), + Effect.orDie + ) + + return EventLogServerEncrypted.Storage.of({ + getId: Effect.succeed(remoteId), + // Auto-increment sequences start at 1. + firstSequence: 1, + getOrCreateSessionAuthBinding: Effect.fnUntraced( + function*(publicKey, signingPublicKey) { + const existing = yield* getSessionAuthBinding(publicKey) + if (existing !== undefined) { + return existing + } + return yield* sql` + INSERT INTO ${sessionAuthBindingsTableSql} (public_key, signing_public_key) + VALUES (${publicKey}, ${signingPublicKey}) + `.pipe( + Effect.as(signingPublicKey) + ) + }, + sql.withTransaction, + withTracerDisabled, + Effect.orDie + ), + write: Effect.fnUntraced( + function*(publicKey, storeId, batches) { + if (batches.length === 0) return [] + const scopeKey = makeEncryptedScopeKey(publicKey, storeId) + const { idsTableSql, pubsub, semaphore, tableSql } = yield* RcMap.get(resources, scopeKey) + + const writeBatches = Effect.gen(function*() { + const stored: Array = [] + // Read before inserting ids, which record the sequence they are stored at. + const [last] = yield* sql<{ last_sequence: number | string | null }>` + SELECT MAX(last_sequence) AS last_sequence FROM ${tableSql} + ` + let nextSequence = Number(last?.last_sequence ?? 0) + 1 + for (const batch of batches) { + // Inserting the ids decides whether the batch is new. SQL Server's + // MERGE also requires unique source ids. + const ids = new Map() + for (let i = 0; i < batch.entryIds.length; i++) { + const entry_id = batch.entryIds[i] + const key = entry_id.join(",") + if (!ids.has(key)) ids.set(key, { entry_id, sequence: nextSequence + i }) + } + let inserted = 0 + for (const chunk of Arr.chunksOf(ids.values(), 200)) { + inserted += yield* insertNewIds(idsTableSql, chunk) + } + if (inserted === 0) continue + const row: BatchRow = { + sequence: nextSequence, + last_sequence: nextSequence + batch.entryIds.length - 1, + iv: batch.iv, + entry_ids: concatEntryIds(batch.entryIds), + encrypted_batch: batch.encryptedBatch + } + yield* sql`INSERT INTO ${tableSql} ${sql.insert(row)}` + stored.push({ sequence: nextSequence, ...batch }) + nextSequence += batch.entryIds.length + } + return stored + }) + + return yield* semaphore.withPermits(1)(Effect.gen(function*() { + const stored = yield* sql.withTransaction(writeBatches) + yield* PubSub.publishAll(pubsub, stored) + return stored + })) + }, + Effect.orDie, + Effect.scoped, + withTracerDisabled + ), + entrySequences: Effect.fnUntraced( + function*(publicKey, storeId, entryIds) { + const { idsTableSql } = yield* RcMap.get(resources, makeEncryptedScopeKey(publicKey, storeId)) + const acks: Array = [] + for (const chunk of Arr.chunksOf(entryIds, 200)) { + const rows = yield* sql` + SELECT entry_id, sequence FROM ${idsTableSql} WHERE ${sql.in("entry_id", chunk)} + `.pipe(Effect.flatMap(decodeIdRows)) + for (const row of rows) acks.push({ entryId: row.entry_id as EntryId, sequence: row.sequence }) + } + return acks + }, + Effect.orDie, + Effect.scoped, + withTracerDisabled + ), + changes: Effect.fnUntraced( + function*(publicKey, storeId, startSequence) { + const scopeKey = makeEncryptedScopeKey(publicKey, storeId) + const { pubsub, tableSql } = yield* RcMap.get(resources, scopeKey) + const subscription = yield* PubSub.subscribe(pubsub) + const initial = yield* sql` + SELECT sequence, iv, entry_ids, encrypted_batch FROM ${tableSql} + WHERE last_sequence >= ${startSequence} + ORDER BY sequence ASC + `.pipe( + Effect.flatMap(decodeBatches) + ) + return Stream.fromArray(initial).pipe(Stream.concat(Stream.fromSubscription(subscription))) + }, + Effect.orDie, + withTracerDisabled, + Stream.unwrap + ) + }) + }).pipe(withTracerDisabled) + +type IdRow = { + readonly entry_id: Uint8Array + readonly sequence: number +} + +const decodeIdRows = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ + entry_id: Schema.Uint8Array, + sequence: Schema.Number +}))) + +type BatchRow = { + readonly sequence: number + readonly last_sequence: number + readonly iv: Uint8Array + readonly entry_ids: Uint8Array + readonly encrypted_batch: Uint8Array +} + +const concatEntryIds = (entryIds: ReadonlyArray): Uint8Array => { + const data = new Uint8Array(entryIds.length * 16) + for (let i = 0; i < entryIds.length; i++) data.set(entryIds[i], i * 16) + return data +} + +const EncryptedRemoteBatchSql = Schema.Struct({ + sequence: Schema.Natural, + iv: Schema.Uint8Array, + entry_ids: Schema.Uint8Array, + encrypted_batch: Schema.Uint8Array +}) + +type EncryptedRemoteBatchSql = Schema.Schema.Type + +const SessionAuthBindingSql = Schema.Struct({ + public_key: Schema.String, + signing_public_key: Schema.Uint8Array +}) + +type SessionAuthBindingSql = Schema.Schema.Type + +const decodeBatchRows = Schema.decodeUnknownEffect(Schema.Array(EncryptedRemoteBatchSql)) +const decodeSessionAuthBindingRows = Schema.decodeUnknownEffect(Schema.Array(SessionAuthBindingSql)) + +const toEncryptedRemoteBatch = (row: EncryptedRemoteBatchSql): EventLogEncryption.EncryptedRemoteBatch => { + const entryIds: Array = [] + for (let offset = 0; offset < row.entry_ids.byteLength; offset += 16) { + entryIds.push(row.entry_ids.subarray(offset, offset + 16) as EntryId) + } + return { + sequence: row.sequence, + entryIds: entryIds as NonEmptyArray, + iv: row.iv as Uint8Array, + encryptedBatch: row.encrypted_batch as Uint8Array + } +} + +const decodeBatches = ( + rows: unknown +): Effect.Effect, Schema.SchemaError> => + decodeBatchRows(rows).pipe(Effect.map((batches) => batches.map(toEncryptedRemoteBatch))) + +const decodeSessionAuthBindings = ( + rows: unknown +): Effect.Effect, Schema.SchemaError> => decodeSessionAuthBindingRows(rows) + +/** + * Provides encrypted server `Storage` using the SQL-backed implementation. + * + * @stability unstable + * @category layers + * @since 4.0.0 + */ +export const layerStorage = (options?: { + readonly entryTablePrefix?: string + readonly remoteIdTable?: string +}): Layer.Layer< + EventLogServerEncrypted.Storage, + SqlError.SqlError, + SqlClient.SqlClient | EventLogEncryption.EventLogEncryption +> => Layer.effect(EventLogServerEncrypted.Storage)(makeStorage(options)) + +/** + * Provides SQL-backed encrypted server `Storage` and supplies the default Web + * Crypto `EventLogEncryption` layer. + * + * @stability unstable + * @category layers + * @since 4.0.0 + */ +export const layerStorageSubtle = (options?: { + readonly entryTablePrefix?: string + readonly remoteIdTable?: string +}): Layer.Layer => + layerStorage(options).pipe( + Layer.provide(EventLogEncryption.layerSubtle) + ) + +const makeEncryptedScopeKey = ( + publicKey: string, + storeId: string +): string => `${publicKey}/${storeId}` + +const withTracerDisabled = Effect.withTracerEnabled(false) diff --git a/repos/effect/packages/effect/src/eventlog/SqlEventLogServerUnencrypted.ts b/packages/effect/src/eventlog/SqlEventLogServerUnencrypted.ts similarity index 74% rename from repos/effect/packages/effect/src/eventlog/SqlEventLogServerUnencrypted.ts rename to packages/effect/src/eventlog/SqlEventLogServerUnencrypted.ts index 35a59f9d7..10047a53e 100644 --- a/repos/effect/packages/effect/src/eventlog/SqlEventLogServerUnencrypted.ts +++ b/packages/effect/src/eventlog/SqlEventLogServerUnencrypted.ts @@ -11,7 +11,6 @@ * @since 4.0.0 */ import * as Arr from "../Array.ts" -import * as Context from "../Context.ts" import * as Effect from "../Effect.ts" import * as Layer from "../Layer.ts" import * as PubSub from "../PubSub.ts" @@ -26,8 +25,9 @@ import * as Stream from "../Stream.ts" import { Entry, EntryId, makeRemoteIdUnsafe, RemoteEntry, type RemoteId } from "./EventJournal.ts" import * as EventLogServerUnencrypted from "./EventLogServerUnencrypted.ts" -let storageIdCounter = 0 const withTracerDisabled = Effect.withTracerEnabled(false) +// Keeps each history lookup within dialect parameter limits. +const lookupBatchSize = 200 type StorageOptions = { readonly entryTablePrefix?: string @@ -42,7 +42,13 @@ type StorageOptions = { * * The implementation creates tables for the server remote id, store sequences, * entries, and session authentication bindings, then persists and streams - * plaintext remote entries. + * plaintext remote entries. Writes reach `changes` subscribers once the + * outermost SQL transaction commits, including caller-owned transactions, and are + * dropped if it rolls back. + * + * **Gotchas** + * + * Concurrent caller-owned transactions can publish writes out of sequence order. * * @stability unstable * @category constructors @@ -144,6 +150,40 @@ export const makeStorage = Effect.fnUntraced(function*(options?: StorageOptions) )` }) + // Serves the batch lookup in `entriesAfter`. Postgres indexes hashes to stay + // under its B-tree row limit, MySQL indexes prefixes, and MSSQL has no + // bounded index yet. + const eventKeyIndex = `${entriesTable}_event_key_idx` + yield* sql.onDialectOrElse({ + pg: () => + sql` + CREATE INDEX IF NOT EXISTS ${sql(eventKeyIndex)} + ON ${entriesTableSql} (store_id, hashtextextended(event, 0), hashtextextended(primary_key, 0), entry_id) + `, + mysql: () => { + const indexExists = sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = ${entriesTable} + AND index_name = ${eventKeyIndex} + `.pipe( + Effect.map((rows) => Number(rows[0].count) > 0) + ) + return sql` + CREATE INDEX ${sql(eventKeyIndex)} + ON ${entriesTableSql} (store_id, event(191), primary_key(191), entry_id) + `.pipe( + Effect.catch((error) => Effect.flatMap(indexExists, (exists) => exists ? Effect.void : Effect.fail(error))) + ) + }, + mssql: () => Effect.void, + orElse: () => + sql` + CREATE INDEX IF NOT EXISTS ${sql(eventKeyIndex)} + ON ${entriesTableSql} (store_id, event, primary_key, entry_id) + ` + }) + yield* sql.onDialectOrElse({ pg: () => sql` @@ -237,49 +277,22 @@ export const makeStorage = Effect.fnUntraced(function*(options?: StorageOptions) }) const transactionSemaphore = yield* Semaphore.make(1) - type PendingWrites = Array]> - const pendingWrites = Context.Service( - `effect/eventlog/SqlEventLogServerUnencrypted/PendingWrites/${storageIdCounter++}` - ) - const publish = Effect.fnUntraced(function*([storeId, entries]: PendingWrites[number]) { - const pubsub = yield* RcMap.get(pubsubs, storeId) - return yield* PubSub.publishAll(pubsub, entries) - }, Effect.scoped) - // Merge nested buffers on success; publish only after the outer commit. - // Hold the outer permit through publication to prevent later commits from overtaking it. - const withTransaction = Effect.fnUntraced(function*( - effect: Effect.Effect - ): Effect.fn.Return> { - const parent = yield* Effect.serviceOption(pendingWrites) - // Allow body interruption, but mask commit through publication. - const transaction = Effect.uninterruptibleMask(Effect.fnUntraced(function*(restore) { - const writes: PendingWrites = [] - const result = yield* sql.withTransaction( - Effect.provideService(restore(effect), pendingWrites, writes) - ) - if (parent._tag === "Some") { - parent.value.push(...writes) - } else { - const stores = new Map>() - for (const [storeId, entries] of writes) { - const existing = stores.get(storeId) - if (existing === undefined) { - stores.set(storeId, [...entries]) - } else { - existing.push(...entries) - } - } - // Sibling buffers may merge out of sequence order. - for (const entries of stores.values()) { - entries.sort((a, b) => a.remoteSequence - b.remoteSequence) - } - yield* Effect.forEach(stores, publish, { discard: true }) - } - return result - })) - // Wait interruptibly; nested transactions reuse the outer permit. - return yield* parent._tag === "Some" ? transaction : transactionSemaphore.withPermit(transaction) - }) + const publish = (storeId: string, entries: ReadonlyArray) => + RcMap.get(pubsubs, storeId).pipe( + Effect.flatMap((pubsub) => PubSub.publishAll(pubsub, entries)), + Effect.asVoid, + Effect.scoped + ) + // Hold the permit through commit and publication to preserve commit order + // for storage-owned transactions. + const withTransaction = (effect: Effect.Effect): Effect.Effect => + Effect.flatMap( + Effect.serviceOption(sql.transactionService), + (transaction) => + transaction._tag === "Some" + ? sql.withTransaction(effect) + : transactionSemaphore.withPermit(sql.withTransaction(effect)) + ) const ensureStore = (storeId: string) => sql.onDialectOrElse({ @@ -362,6 +375,77 @@ export const makeStorage = Effect.fnUntraced(function*(options?: StorageOptions) Effect.flatMap(decodeRemoteEntries) ) + // Postgres matches on the indexed hashes, then on the values to rule out + // collisions. + const matchesKey = sql.onDialectOrElse({ + pg: () => (event: string, primaryKeys: ReadonlyArray) => + sql` + hashtextextended(event, 0) = hashtextextended(${event}, 0) AND event = ${event} + AND hashtextextended(primary_key, 0) IN (${ + sql.csv(primaryKeys.map((primaryKey) => sql`hashtextextended(${primaryKey}, 0)`)) + }) + AND ${sql.in("primary_key", primaryKeys)} + `, + orElse: () => (event: string, primaryKeys: ReadonlyArray) => + sql`event = ${event} AND ${sql.in("primary_key", primaryKeys)}` + }) + + // One SELECT per index lookup, joined with UNION ALL: MySQL scans the whole + // store for the equivalent OR. + const selectChunkHistory = (storeId: string, oldest: Entry, entries: ReadonlyArray) => { + const primaryKeysByEvent = new Map>() + for (const entry of entries) { + const primaryKeys = primaryKeysByEvent.get(entry.event) + if (primaryKeys === undefined) { + primaryKeysByEvent.set(entry.event, new Set([entry.primaryKey])) + } else { + primaryKeys.add(entry.primaryKey) + } + } + let statement = sql` + SELECT sequence, entry_id, event, primary_key, payload + FROM ${entriesTableSql} + WHERE store_id = ${storeId} AND ${sql.in("entry_id", entries.map((entry) => entry.id))} + ` + for (const [event, primaryKeys] of primaryKeysByEvent) { + statement = sql` + ${statement} + UNION ALL + SELECT sequence, entry_id, event, primary_key, payload + FROM ${entriesTableSql} + WHERE store_id = ${storeId} + AND ${matchesKey(event, Array.from(primaryKeys))} + AND entry_id >= ${oldest.id} + ` + } + return Effect.flatMap(statement, decodeRemoteEntries) + } + + // Chunks and the UNION ALL branches can return the same row more than once. + const selectBatchHistory = (storeId: string, oldest: Entry, batch: Arr.NonEmptyReadonlyArray) => + Effect.forEach( + Arr.chunksOf(batch, lookupBatchSize), + (chunk) => selectChunkHistory(storeId, oldest, chunk) + ).pipe( + Effect.map((chunks) => { + const bySequence = new Map() + for (const remoteEntry of chunks.flat()) { + bySequence.set(remoteEntry.remoteSequence, remoteEntry) + } + return Array.from(bySequence.values()).sort((a, b) => a.remoteSequence - b.remoteSequence) + }) + ) + + const selectEntriesFrom = (storeId: string, entry: Entry) => + sql` + SELECT sequence, entry_id, event, primary_key, payload + FROM ${entriesTableSql} + WHERE store_id = ${storeId} AND entry_id >= ${entry.id} + ORDER BY sequence ASC + `.pipe( + Effect.flatMap(decodeRemoteEntries) + ) + const getSessionAuthBinding = (publicKey: string) => sql` SELECT public_key, signing_public_key @@ -378,6 +462,7 @@ export const makeStorage = Effect.fnUntraced(function*(options?: StorageOptions) return EventLogServerUnencrypted.Storage.of({ getId: Effect.succeed(remoteId), + firstSequence: 1, getOrCreateSessionAuthBinding: Effect.fnUntraced( function*(publicKey, signingPublicKey) { const existing = yield* getSessionAuthBinding(publicKey) @@ -395,15 +480,8 @@ export const makeStorage = Effect.fnUntraced(function*(options?: StorageOptions) Effect.orDie, withTracerDisabled ), - entriesAfter: (storeId, entry) => - sql` - SELECT sequence, entry_id, event, primary_key, payload - FROM ${entriesTableSql} - WHERE store_id = ${storeId} AND entry_id >= ${entry.id} - ORDER BY sequence ASC - `.pipe( - Effect.flatMap(decodeRemoteEntries), - Effect.map(Arr.map((r) => r.entry)), + entriesAfter: (storeId, entry, batch) => + (batch === undefined ? selectEntriesFrom(storeId, entry) : selectBatchHistory(storeId, entry, batch)).pipe( Effect.orDie, withTracerDisabled ), @@ -454,8 +532,7 @@ export const makeStorage = Effect.fnUntraced(function*(options?: StorageOptions) const nextSequence = currentNextSequence + entries.length yield* setNextSequence(storeId, nextSequence) - const writes = yield* pendingWrites - writes.push([storeId, committed]) + yield* sql.afterCommit(publish(storeId, committed)) return committed }, @@ -467,11 +544,13 @@ export const makeStorage = Effect.fnUntraced(function*(options?: StorageOptions) function*({ storeId, startSequence, compactors }) { const pubsub = yield* RcMap.get(pubsubs, storeId) const subscription = yield* PubSub.subscribe(pubsub) - const backlog = yield* EventLogServerUnencrypted.compactBacklog({ - compactors, - remoteEntries: yield* selectEntriesAfter(storeId, startSequence) - }) - let watermark = backlog.length > 0 ? backlog[backlog.length - 1]!.remoteSequence : startSequence - 1 + const remoteEntries = yield* selectEntriesAfter(storeId, startSequence) + const backlog = yield* EventLogServerUnencrypted.compactBacklog({ compactors, remoteEntries }) + // Compaction can drop trailing entries, so skip live entries up to the + // last uncompacted one. + const watermark = remoteEntries.length > 0 + ? remoteEntries[remoteEntries.length - 1]!.remoteSequence + : startSequence - 1 return Stream.fromArray(backlog).pipe( Stream.concat( diff --git a/repos/effect/packages/effect/src/eventlog/index.ts b/packages/effect/src/eventlog/index.ts similarity index 94% rename from repos/effect/packages/effect/src/eventlog/index.ts rename to packages/effect/src/eventlog/index.ts index ce4597284..bf4a1de7b 100644 --- a/repos/effect/packages/effect/src/eventlog/index.ts +++ b/packages/effect/src/eventlog/index.ts @@ -71,6 +71,12 @@ export * as EventLogServerUnencrypted from "./EventLogServerUnencrypted.ts" */ export * as EventLogSessionAuth from "./EventLogSessionAuth.ts" +/** + * @stability unstable + * @since 4.0.6 + */ +export * as EventRow from "./EventRow.ts" + /** * @stability unstable * @since 4.0.0 diff --git a/packages/effect/src/eventlog/internal/conflicts.ts b/packages/effect/src/eventlog/internal/conflicts.ts new file mode 100644 index 000000000..fd6baed6f --- /dev/null +++ b/packages/effect/src/eventlog/internal/conflicts.ts @@ -0,0 +1,123 @@ +import type * as Arr from "../../Array.ts" +import * as Order from "../../Order.ts" +import type { Entry } from "../EventJournal.ts" + +/** Orders entry ids by their raw UUID bytes. */ +export const entryIdOrder = Order.make((a, b) => { + for (let i = 0; i < 16; i++) { + if (a[i] !== b[i]) { + return a[i] < b[i] ? -1 : 1 + } + } + return 0 +}) + +/** Orders entries by id. */ +const entryOrder = Order.make((a, b) => entryIdOrder(a.id, b.id)) + +/** + * The source entry a compaction output stands for, and the output's position + * among the outputs written for that source. + */ +interface Anchor { + readonly entry: Entry + readonly index: number +} + +// Set during client replay only, so it never leaves the process. +const anchors = new WeakMap() + +/** + * Records that `entry` was written by a compactor for `source`, as its + * `index`th output for that source. Untagged entries are their own anchor. + */ +export const setAnchor = (entry: Entry, source: Entry, index: number): void => { + anchors.set(entry, { entry: source, index }) +} + +const anchorEntry = (entry: Entry): Entry => anchors.get(entry)?.entry ?? entry +const anchorIndex = (entry: Entry): number => anchors.get(entry)?.index ?? 0 + +// Sorts by anchor id, then by write order among one anchor's outputs. +const compareAnchors = (a: Entry, b: Entry): number => + entryOrder(anchorEntry(a), anchorEntry(b)) || anchorIndex(a) - anchorIndex(b) + +/** + * Pairs a batch of incoming entries with the stored entries that conflict with + * them: same event and primary key, and an id strictly greater than the + * incoming entry's anchor. + * + * The batch is sorted by anchor id, then by write order, so it is applied in + * the same order on every replica and a compaction output takes its source's + * place and precedence. Add the stored entries that may conflict, then call + * `toBatch` to sort and filter them. + */ +export class ConflictIndex { + /** The incoming entries in anchor order. */ + readonly batch: ReadonlyArray + /** The anchor of the first incoming entry. Only stored entries with a greater id can conflict. */ + readonly oldest: Entry + /** Creation time of `oldest`, for coarse filters. */ + readonly oldestMillis: number + private readonly stored = new Map>() + + constructor(batch: Arr.NonEmptyReadonlyArray) { + this.batch = batch.slice().sort(compareAnchors) + this.oldest = anchorEntry(this.batch[0]) + this.oldestMillis = this.oldest.createdAtMillis + for (const entry of this.batch) { + let byKey = this.stored.get(entry.event) + if (byKey === undefined) { + byKey = new Map() + this.stored.set(entry.event, byKey) + } + if (!byKey.has(entry.primaryKey)) { + byKey.set(entry.primaryKey, { + event: entry.event, + primaryKey: entry.primaryKey, + oldest: anchorEntry(entry), + entries: [] + }) + } + } + } + + get events(): Array { + return Array.from(this.stored.keys()) + } + + /** The `(event, primaryKey)` groups in the batch. */ + *groups(): Iterable { + for (const byKey of this.stored.values()) { + yield* byKey.values() + } + } + + add(entry: Entry): void { + this.stored.get(entry.event)?.get(entry.primaryKey)?.entries.push(entry) + } + + toBatch(): Array<{ readonly entry: Entry; readonly conflicts: ReadonlyArray }> { + for (const group of this.groups()) { + group.entries.sort(entryOrder) + } + return this.batch.map((entry) => { + const anchor = anchorEntry(entry) + return { + entry, + conflicts: this.stored.get(entry.event)!.get(entry.primaryKey)!.entries.filter((stored) => + entryOrder(stored, anchor) > 0 + ) + } + }) + } +} + +/** An event and primary key in the batch, with the stored entries added for it. */ +export interface ConflictGroup { + readonly event: string + readonly primaryKey: string + /** The lowest anchor among the group's incoming entries. Only stored entries with a greater id can conflict. */ + readonly oldest: Entry + readonly entries: Array +} diff --git a/repos/effect/packages/effect/src/eventlog/internal/identityRootSecretDerivation.ts b/packages/effect/src/eventlog/internal/identityRootSecretDerivation.ts similarity index 100% rename from repos/effect/packages/effect/src/eventlog/internal/identityRootSecretDerivation.ts rename to packages/effect/src/eventlog/internal/identityRootSecretDerivation.ts diff --git a/repos/effect/packages/effect/src/graphql/GraphQL.ts b/packages/effect/src/graphql/GraphQL.ts similarity index 100% rename from repos/effect/packages/effect/src/graphql/GraphQL.ts rename to packages/effect/src/graphql/GraphQL.ts diff --git a/repos/effect/packages/effect/src/graphql/GraphQLClient.ts b/packages/effect/src/graphql/GraphQLClient.ts similarity index 100% rename from repos/effect/packages/effect/src/graphql/GraphQLClient.ts rename to packages/effect/src/graphql/GraphQLClient.ts diff --git a/repos/effect/packages/effect/src/graphql/GraphQLClientError.ts b/packages/effect/src/graphql/GraphQLClientError.ts similarity index 97% rename from repos/effect/packages/effect/src/graphql/GraphQLClientError.ts rename to packages/effect/src/graphql/GraphQLClientError.ts index 8935736a7..9976cefb0 100644 --- a/repos/effect/packages/effect/src/graphql/GraphQLClientError.ts +++ b/packages/effect/src/graphql/GraphQLClientError.ts @@ -5,11 +5,12 @@ * @stability experimental * @since 4.0.0 */ -import * as Duration from "../Duration.ts" -import type * as Headers from "../http/Headers.ts" +import type * as Duration from "../Duration.ts" +import * as Headers from "../http/Headers.ts" import type * as HttpClientError from "../http/HttpClientError.ts" import { HeadersSchema } from "../internal/graphqlHeaders.ts" import { hasProperty } from "../Predicate.ts" +import * as Result from "../Result.ts" import * as Schema from "../Schema.ts" const TypeId = "~effect/graphql/GraphQLClientError" @@ -226,16 +227,8 @@ const isRetryableStatus = (status: number): boolean => status === 429 || (status */ const parseRetryAfter = (value: string | undefined, now: number): Duration.Duration | undefined => { if (value === undefined) return undefined - const trimmed = value.trim() - if (/^\d+$/.test(trimmed)) { - return Duration.seconds(Number(trimmed)) - } - // Every HTTP date names a day or month; `Date.parse` alone also accepts - // bare numbers such as `1.5` or `-1`. - if (!/[a-z]/i.test(trimmed)) return undefined - const date = Date.parse(trimmed) - if (Number.isNaN(date)) return undefined - return Duration.millis(Math.max(0, date - now)) + const retryAfter = Headers.parseRetryAfter(value) + return Result.isSuccess(retryAfter) ? Headers.retryAfterDelay(retryAfter.success, now) : undefined } /** diff --git a/repos/effect/packages/effect/src/graphql/GraphQLGroup.ts b/packages/effect/src/graphql/GraphQLGroup.ts similarity index 100% rename from repos/effect/packages/effect/src/graphql/GraphQLGroup.ts rename to packages/effect/src/graphql/GraphQLGroup.ts diff --git a/repos/effect/packages/effect/src/graphql/GraphQLMiddleware.ts b/packages/effect/src/graphql/GraphQLMiddleware.ts similarity index 100% rename from repos/effect/packages/effect/src/graphql/GraphQLMiddleware.ts rename to packages/effect/src/graphql/GraphQLMiddleware.ts diff --git a/repos/effect/packages/effect/src/graphql/GraphQLProtocol.ts b/packages/effect/src/graphql/GraphQLProtocol.ts similarity index 99% rename from repos/effect/packages/effect/src/graphql/GraphQLProtocol.ts rename to packages/effect/src/graphql/GraphQLProtocol.ts index 2985d62db..6f4d446be 100644 --- a/repos/effect/packages/effect/src/graphql/GraphQLProtocol.ts +++ b/packages/effect/src/graphql/GraphQLProtocol.ts @@ -511,9 +511,10 @@ export const makeHttp: { * * **Gotchas** * - * `GraphQLResolver` resolves each lookup's URL once, before middleware run and - * before the batched request exists, then sends each batch to the URL it was - * grouped by. A single call resolves it in the transport, after middleware. + * `GraphQLResolver` resolves each lookup's URL once, in the calling fiber, + * before middleware run and before the batched request exists, then sends + * each batch to the URL it was grouped by. A single call resolves it in the + * transport, after middleware. * So the URL must depend only on the context and the operation, and * middleware must not change `kind` or `operationName` or provide services * the URL reads. diff --git a/repos/effect/packages/effect/src/graphql/GraphQLResolver.ts b/packages/effect/src/graphql/GraphQLResolver.ts similarity index 85% rename from repos/effect/packages/effect/src/graphql/GraphQLResolver.ts rename to packages/effect/src/graphql/GraphQLResolver.ts index 07699bc95..863e4080e 100644 --- a/repos/effect/packages/effect/src/graphql/GraphQLResolver.ts +++ b/packages/effect/src/graphql/GraphQLResolver.ts @@ -9,7 +9,8 @@ * in separate batches. Compose `RequestResolver.setDelay` and * `RequestResolver.batchN` with `.pipe` to control batching. * - * Lookups with different protocol URLs are batched separately. See + * Lookups with different protocol URLs are batched separately, and + * `RequestResolver.batchN` limits each URL's batches. See * `GraphQLProtocol.layerHttp` for middleware and caching constraints. * * @stability experimental @@ -27,6 +28,7 @@ import * as internalGraphQL from "../internal/graphql.ts" import * as MutableHashMap from "../MutableHashMap.ts" import * as Request from "../Request.ts" import * as RequestResolver from "../RequestResolver.ts" +import * as Tracer from "../Tracer.ts" import type * as Types from "../Types.ts" import type * as GraphQL from "./GraphQL.ts" import type { OperationMethod, PartialResult } from "./GraphQLClient.ts" @@ -65,10 +67,13 @@ type BatchKey = [GraphQL.Services] extends [never] ? interface KeyRequest extends Request.Request { readonly key: K + /** The protocol URL {@link request} resolved for this lookup, if it could. */ + readonly url: string | undefined } -// Requests are structural, so two requests with equal keys are one request. -const makeRequest = (key: K): KeyRequest => Request.of>()({ key }) +// Requests are structural, so two requests with equal keys and URLs are one request. +const makeRequest = (key: K, url: string | undefined): KeyRequest => + Request.of>()({ key, url }) // ----------------------------------------------------------------------------- // Running @@ -129,8 +134,13 @@ export const request: { (key: K, resolver: RequestResolver.RequestResolver>): Effect.Effect } = dual( 2, - (key: K, resolver: RequestResolver.RequestResolver>): Effect.Effect => - Effect.request(makeRequest(key), resolver) + (key: K, resolver: RequestResolver.RequestResolver>): Effect.Effect => { + // Resolved in the caller's fiber, so the URL is part of the pending batch's key. + const resolveUrl = urlResolvers.get(resolver.batchKey) + return resolveUrl === undefined + ? queue(makeRequest(key, undefined), resolver) + : Effect.flatMap(resolveUrl, (url) => queue(makeRequest(key, url), resolver)) + } ) // ----------------------------------------------------------------------------- @@ -609,41 +619,90 @@ interface Slice { const constNoSuchElement = Exit.fail(new Cause.NoSuchElementError()) /** - * Resolves each entry's URL in its own context, then splits the entries by URL - * and `batchKey`. Each batch runs in its first entry's context, carrying the - * URL it was grouped by, so the transport doesn't resolve it again. + * URL resolvers by resolver `batchKey`, which `RequestResolver.batchN` and + * `setDelay` keep, so {@link request} can resolve a lookup's URL before it is + * queued. + */ +const urlResolvers = new WeakMap>() + +/** + * Groups pending entries by URL and `batchKey`, so collection limits apply to + * each group. A URL that `request` couldn't resolve, for example after + * `RequestResolver.grouped` replaced the key, is resolved here in the entry's + * context, and the batch is split again. Each batch runs in its first entry's + * context, under any span opened around the batch, carrying the URL it was + * grouped by, so the transport doesn't resolve it again. */ -const makeResolver = ( +const makeResolver = >( method: OperationMethod, batchKey: ((context: Context.Context) => unknown) | undefined, runAll: (entries: Arr.NonEmptyArray>) => Effect.Effect, any> ): RequestResolver.RequestResolver => { const resolve = internalGraphQL.rawExecutors.get(method)?.url - const operation = { kind: method.operation.kind, operationName: method.operation.name } + const resolveUrl = resolve?.({ kind: method.operation.kind, operationName: method.operation.name }) + // Arrays are compared structurally, so equal keys share a batch. + const keyOf = (entry: Request.Entry, url: string | undefined) => [url, batchKey?.(entry.context)] as const const urlOf = (entry: Request.Entry) => - resolve === undefined ? Effect.succeed(undefined) : Effect.provideContext(resolve(operation), entry.context) - return RequestResolver.make((entries) => - Effect.flatMap(Effect.forEach(entries, urlOf, { concurrency: "unbounded" }), (urls) => { - // Arrays are compared structurally, so equal keys share a batch. - const batches = MutableHashMap.empty< - readonly [string | undefined, unknown], - Arr.NonEmptyArray> - >() - entries.forEach((entry, i) => { - const key = [urls[i], batchKey?.(entry.context)] as const - const batch = MutableHashMap.get(batches, key) - if (batch._tag === "Some") batch.value.push(entry) - else MutableHashMap.set(batches, key, [entry]) + entry.request.url !== undefined || resolveUrl === undefined + ? Effect.succeed(entry.request.url) + : runIn(resolveUrl, entry.context) + const resolver = RequestResolver.makeGrouped({ + key: (entry) => keyOf(entry, entry.request.url), + resolver: (entries) => + Effect.contextWith((current: Context.Context) => { + const span = batchSpan(current) + return Effect.flatMap(Effect.forEach(entries, urlOf, { concurrency: "unbounded" }), (urls) => { + const batches = MutableHashMap.empty< + readonly [string | undefined, unknown], + Arr.NonEmptyArray> + >() + entries.forEach((entry, i) => { + const key = keyOf(entry, urls[i]) + const batch = MutableHashMap.get(batches, key) + if (batch._tag === "Some") batch.value.push(entry) + else MutableHashMap.set(batches, key, [entry]) + }) + return Effect.forEach(batches, ([[url], entries]) => { + let context: Context.Context = entries[0].context + if (url !== undefined) context = Context.add(context, internalGraphQL.ResolvedUrl, { resolve, url }) + if (span !== undefined) context = Context.add(context, Tracer.ParentSpan, span) + return runIn(runAll(entries), context) + }, { concurrency: "unbounded", discard: true }) + }) }) - return Effect.forEach(batches, ([[url], entries]) => { - const context: Context.Context = entries[0].context - return Effect.provideContext( - runAll(entries), - url === undefined ? context : Context.add(context, internalGraphQL.ResolvedUrl, { resolve, url }) - ) - }, { concurrency: "unbounded", discard: true }) - }) + }) + if (resolveUrl !== undefined) urlResolvers.set(resolver.batchKey, resolveUrl) + return resolver +} + +// Replaces, rather than merges into, the batch fiber's context, which may +// belong to a lookup with other services or `Context.Reference` values. +const runIn = (effect: Effect.Effect, context: Context.Context): Effect.Effect => + Effect.updateContext(effect, (_: Context.Context) => context) + +// The caller's span when it queued a lookup. A batch fiber inherits it from the +// caller that started or filled the batch, even if that caller was interrupted. +const QueuedSpan = Context.Reference("effect/graphql/GraphQLResolver/QueuedSpan", { + defaultValue: () => undefined +}) + +const queue = ( + request: KeyRequest, + resolver: RequestResolver.RequestResolver> +): Effect.Effect => + Effect.contextWith((context: Context.Context) => + Effect.provideService( + Effect.request(request, resolver), + QueuedSpan, + Context.getOrUndefined(context, Tracer.ParentSpan) + ) ) + +// A span opened around the batch, such as `RequestResolver.withSpan`'s, rather +// than the one the batch fiber inherited from a caller. +const batchSpan = (context: Context.Context) => { + const span = Context.getOrUndefined(context, Tracer.ParentSpan) + return span === Context.get(context, QueuedSpan) ? undefined : span } /** diff --git a/repos/effect/packages/effect/src/graphql/index.ts b/packages/effect/src/graphql/index.ts similarity index 100% rename from repos/effect/packages/effect/src/graphql/index.ts rename to packages/effect/src/graphql/index.ts diff --git a/repos/effect/packages/effect/src/http-api/HttpApi.ts b/packages/effect/src/http-api/HttpApi.ts similarity index 99% rename from repos/effect/packages/effect/src/http-api/HttpApi.ts rename to packages/effect/src/http-api/HttpApi.ts index 6f9ab62b8..ec8bf4604 100644 --- a/repos/effect/packages/effect/src/http-api/HttpApi.ts +++ b/packages/effect/src/http-api/HttpApi.ts @@ -322,7 +322,7 @@ const extractResponseContent = ( function add(schema: Schema.Top) { const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema - if (HttpApiSchema.isStreamSchema(body)) return + if (HttpApiSchema.isStreamSchema(body) || HttpApiSchema.isUpgrade(body)) return const status = getStatus(schema) const schemas = map.get(status) if (schemas === undefined) { diff --git a/repos/effect/packages/effect/src/http-api/HttpApiBuilder.ts b/packages/effect/src/http-api/HttpApiBuilder.ts similarity index 87% rename from repos/effect/packages/effect/src/http-api/HttpApiBuilder.ts rename to packages/effect/src/http-api/HttpApiBuilder.ts index 21a033919..24a8145aa 100644 --- a/repos/effect/packages/effect/src/http-api/HttpApiBuilder.ts +++ b/packages/effect/src/http-api/HttpApiBuilder.ts @@ -10,10 +10,12 @@ * @since 4.0.0 */ import type { NonEmptyReadonlyArray } from "../Array.ts" +import * as ByteSize from "../ByteSize.ts" import type * as Cause from "../Cause.ts" import * as Context from "../Context.ts" import * as Effect from "../Effect.ts" import * as Base64 from "../encoding/Base64.ts" +import * as Ndjson from "../encoding/Ndjson.ts" import * as Sse from "../encoding/Sse.ts" import * as Fiber from "../Fiber.ts" import type { FileSystem } from "../FileSystem.ts" @@ -26,6 +28,7 @@ import type { HttpPlatform } from "../http/HttpPlatform.ts" import * as HttpRouter from "../http/HttpRouter.ts" import * as Request from "../http/HttpServerRequest.ts" import { HttpServerRequest } from "../http/HttpServerRequest.ts" +import * as Respondable from "../http/HttpServerRespondable.ts" import * as Response from "../http/HttpServerResponse.ts" import type { HttpServerResponse } from "../http/HttpServerResponse.ts" import * as Multipart from "../http/Multipart.ts" @@ -47,7 +50,7 @@ import type { NoInfer } from "../Types.ts" import * as UndefinedOr from "../UndefinedOr.ts" import * as HttpApi from "./HttpApi.ts" import * as HttpApiEndpoint from "./HttpApiEndpoint.ts" -import { HttpApiSchemaError } from "./HttpApiError.ts" +import { HttpApiPayloadTooLarge, HttpApiSchemaError } from "./HttpApiError.ts" import type * as HttpApiGroup from "./HttpApiGroup.ts" import * as HttpApiMiddleware from "./HttpApiMiddleware.ts" import * as HttpApiSchema from "./HttpApiSchema.ts" @@ -739,6 +742,11 @@ type PayloadDecoder = readonly nullOnEmpty: boolean readonly decode: (input: unknown) => Effect.Effect } + | { + readonly _tag: "Stream" + readonly maxBytes: number | undefined + readonly decode: (request: HttpServerRequest) => Stream.Stream + } function buildPayloadDecoders( payloadMap: HttpApiEndpoint.PayloadMap, @@ -746,6 +754,15 @@ function buildPayloadDecoders( ): Map { const result = new Map() payloadMap.forEach(({ encoding, schemas }, contentType) => { + if (encoding._tag === "Stream") { + const schema = schemas[0] as HttpApiSchema.StreamUint8Array | HttpApiSchema.StreamNdjson + result.set(contentType, { + _tag: "Stream", + maxBytes: schema.maxBytes === undefined ? undefined : ByteSize.toNumberUnsafe(schema.maxBytes), + decode: makePayloadStreamDecoder(schema, options) + }) + return + } const schema = Schema.Union(schemas) const decode = Schema.decodeUnknownEffect( encoding._tag === "FormUrlEncoded" ? Schema.toCodecArrayFromSingle(schema) : schema, @@ -777,6 +794,17 @@ function decodePayload( if (!existing) { return Response.text(`Unsupported content-type: ${contentType}`, { status: 415 }) } + if (existing._tag === "Stream") { + if (existing.maxBytes !== undefined && Number(httpRequest.headers["content-length"]) > existing.maxBytes) { + return payloadTooLargeResponse + } + // The stream can be pulled after the handler returns, inside a streaming + // response, so it carries the element schema's services with it. + return Effect.map( + Effect.context(), + (context) => Stream.provideContext(existing.decode(httpRequest), context) + ) + } const { _tag, decode } = existing switch (_tag) { case "Multipart": { @@ -824,6 +852,69 @@ function decodePayload( } } +const payloadTooLargeResponse = Response.empty({ status: 413, headers: { connection: "close" } }) + +// The handler pulls the body itself, so nothing is read ahead of it. Body read +// failures stay defects, as they are for buffered payloads. +function makePayloadStreamDecoder( + schema: HttpApiSchema.StreamUint8Array | HttpApiSchema.StreamNdjson, + options: SchemaAST.ParseOptions | undefined +): (request: HttpServerRequest) => Stream.Stream { + const maxBytes = schema.maxBytes + const readBody = (request: HttpServerRequest) => { + const body = Stream.orDie(request.stream) + return maxBytes === undefined ? body : limitBytes(body, maxBytes) + } + if (HttpApiSchema.isStreamUint8Array(schema)) { + return readBody + } + const decodeElement = Schema.decodeUnknownEffect(Schema.toCodecJson(schema.schema), options) + const maxElementBytes = schema.maxElementBytes + const elementTooLarge = new HttpApiPayloadTooLarge({ limit: "maxElementBytes", size: maxElementBytes }) + const maxLineBytes = ByteSize.toNumberUnsafe(maxElementBytes) + return (request) => + readBody(request).pipe( + Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true, maxLineBytes })), + Stream.mapEffect((json) => decodeElement(json)), + Stream.mapError((error) => { + if (HttpApiPayloadTooLarge.is(error)) return error + if (!Ndjson.isNdjsonError(error)) return new HttpApiSchemaError({ kind: "Payload", cause: error }) + if (error.reason === "LineTooLarge") return elementTooLarge + return new HttpApiSchemaError({ kind: "Payload", cause: ndjsonSchemaError(error) }) + }) + ) +} + +// `JSON.stringify` found no JSON representation for an encoded element +const isNdjsonPackError = (error: unknown): error is Ndjson.NdjsonError => + Ndjson.isNdjsonError(error) && error.kind === "Pack" + +const ndjsonSchemaError = (error: Ndjson.NdjsonError): Schema.SchemaError => + new Schema.SchemaError( + new SchemaIssue.InvalidValue({ + message: error.reason === "InvalidUtf8" + ? "Expected valid UTF-8" + : error.unterminated + ? "Expected a valid JSON line, got an unterminated final line" + : "Expected a valid JSON line" + }) + ) + +function limitBytes( + self: Stream.Stream, + maxBytes: ByteSize.ByteSize +): Stream.Stream { + const limit = ByteSize.toNumberUnsafe(maxBytes) + const error = new HttpApiPayloadTooLarge({ limit: "maxBytes", size: maxBytes }) + return Stream.suspend(() => { + let total = 0 + return Stream.mapArrayEffect(self, (chunks) => { + for (const chunk of chunks) total += chunk.length + return total > limit ? Effect.fail(error) : Effect.succeed(chunks) + }) + }) +} + function handlerToHttpEffect( api: Pick, group: HttpApiGroup.Top, @@ -847,6 +938,7 @@ function handlerToHttpEffect( ) const encodeStream = makeStreamEncoder(endpoint, options.success) const encodeWithHeaders = makeWithHeadersEncoder(endpoint, options.success, options.headers) + const upgrade = getUpgradeSuccessSchema(endpoint) const shouldParsePayload = endpoint.payload.size > 0 && !isRaw const payloadBy = shouldParsePayload ? buildPayloadDecoders(endpoint.payload, options.payload) : undefined @@ -884,10 +976,20 @@ function handlerToHttpEffect( request.payload = yield* HttpApiSchemaError.wrap("Payload", result) } } + if (upgrade !== undefined) { + if (!isUpgradeRequest(httpRequest, upgrade.protocol)) { + return upgradeRequired(upgrade.protocol) + } + request.socket = yield* Effect.orDie(httpRequest.upgrade) + } let response = yield* handler(request) if (Response.isHttpServerResponse(response)) { return response } + if (upgrade !== undefined) { + // The server replaces this with the 101 once the connection is upgraded. + return Response.empty() + } let responseHeaders: unknown | undefined if (encodeWithHeaders !== undefined && HttpApiSchema.isWithHeadersValue(response)) { responseHeaders = response.headers @@ -908,8 +1010,11 @@ function handlerToHttpEffect( ).pipe( Effect.withErrorReporting, Effect.catch((error) => { - if (HttpApiSchemaError.is(error)) return Effect.die(error) - return Effect.orDie(encodeError(error)) + if (HttpApiSchemaError.is(error) || HttpApiPayloadTooLarge.is(error)) return Effect.die(error) + return Effect.catch(encodeError(error), (schemaError) => + Respondable.isRespondable(error) + ? Respondable.toResponse(error) + : Effect.die(schemaError)) }), Effect.provideContext(context) ) @@ -1071,25 +1176,7 @@ function makeStreamEncoder( const status = HttpApiSchema.getStatusSuccessSchema(successSchema.schema) const contentType = streamSchema.contentType - if (HttpApiSchema.isStreamUint8Array(streamSchema)) { - return (response, context) => { - if (!Stream.isStream(response)) { - return hasBuffered ? undefined : new Schema.SchemaError( - new SchemaIssue.InvalidValue({ message: "Expected a streaming response" }) - ) - } - - return Effect.succeed(Response.stream( - Stream.provideContext( - response as Stream.Stream, - context as Context.Context - ), - { status, contentType } - )) - } - } - - const sseEncoder = makeSseEncoder(streamSchema, options) + const encodeBody = makeStreamBodyEncoder(streamSchema, options) return (response, context) => { if (!Stream.isStream(response)) { @@ -1100,7 +1187,7 @@ function makeStreamEncoder( return Effect.succeed(Response.stream( Stream.provideContext( - encodeSseStream(response, sseEncoder), + encodeBody(response), context as Context.Context ), { status, contentType } @@ -1108,6 +1195,47 @@ function makeStreamEncoder( } } +function makeStreamBodyEncoder( + streamSchema: HttpApiSchema.StreamSchema, + options: SchemaAST.ParseOptions | undefined +): (stream: Stream.Stream) => Stream.Stream { + if (HttpApiSchema.isStreamUint8Array(streamSchema)) { + return (stream) => stream as Stream.Stream + } + if (HttpApiSchema.isStreamNdjson(streamSchema)) { + const encodeElements = Schema.encodeUnknownEffect( + Schema.NonEmptyArray(Schema.toCodecJson(streamSchema.schema)), + options + ) + // There is no error schema, so a bad element aborts the response + return (stream) => + stream.pipe( + Stream.mapArrayEffect((elements) => Effect.orDie(encodeElements(elements))), + Stream.pipeThroughChannel(Ndjson.encode()), + Stream.catchIf(isNdjsonPackError, Stream.die) + ) + } + const sseEncoder = makeSseEncoder(streamSchema, options) + return (stream) => encodeSseStream(stream, sseEncoder) +} + +function getUpgradeSuccessSchema(endpoint: HttpApiEndpoint.Top): HttpApiSchema.Upgrade | undefined { + for (const schema of endpoint.success) { + if (HttpApiSchema.isUpgrade(schema)) return schema + } +} + +// RFC 9110 §7.8: `Upgrade` only applies with an `upgrade` connection option. +const isUpgradeRequest = (request: HttpServerRequest, protocol: string): boolean => + request.headers.upgrade?.toLowerCase() === protocol.toLowerCase() && + /(?:^|,)\s*upgrade\s*(?:,|$)/i.test(request.headers.connection ?? "") + +const upgradeRequired = (protocol: string) => + Response.empty({ + status: 426, + headers: { connection: "Upgrade", upgrade: protocol } + }) + function getStreamSuccessSchema(endpoint: HttpApiEndpoint.Top) { for (const schema of endpoint.success) { const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema diff --git a/repos/effect/packages/effect/src/http-api/HttpApiClient.ts b/packages/effect/src/http-api/HttpApiClient.ts similarity index 86% rename from repos/effect/packages/effect/src/http-api/HttpApiClient.ts rename to packages/effect/src/http-api/HttpApiClient.ts index e5e2287a3..47721e683 100644 --- a/repos/effect/packages/effect/src/http-api/HttpApiClient.ts +++ b/packages/effect/src/http-api/HttpApiClient.ts @@ -12,11 +12,14 @@ * @since 4.0.0 */ import * as Arr from "../Array.ts" +import * as ByteSize from "../ByteSize.ts" import * as Cause from "../Cause.ts" import type * as Context from "../Context.ts" import * as Effect from "../Effect.ts" +import * as Ndjson from "../encoding/Ndjson.ts" import * as Sse from "../encoding/Sse.ts" import { identity } from "../Function.ts" +import type * as Headers from "../http/Headers.ts" import * as HttpBody from "../http/HttpBody.ts" import * as HttpClient from "../http/HttpClient.ts" import * as HttpClientError from "../http/HttpClientError.ts" @@ -31,6 +34,7 @@ import * as Schema from "../Schema.ts" import * as SchemaAST from "../SchemaAST.ts" import * as SchemaIssue from "../SchemaIssue.ts" import * as SchemaTransformation from "../SchemaTransformation.ts" +import type * as Socket from "../socket/Socket.ts" import * as Stream from "../Stream.ts" import type { Simplify } from "../Types.ts" import * as UndefinedOr from "../UndefinedOr.ts" @@ -96,9 +100,31 @@ export type SuccessType = S extends HttpApiSchema.WithHeaders< never > : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream + : S extends HttpApiSchema.StreamNdjson ? + Stream.Stream<_S["Type"], HttpClientError.HttpClientError | Schema.SchemaError, never> + : S extends HttpApiSchema.Upgrade ? Upgraded : S extends Schema.Constraint ? S["Type"] : never +/** + * The value a client method returns for an endpoint declared with + * `HttpApiSchema.Upgrade`: the headers of the `101 Switching Protocols` + * response and the upgraded connection. + * + * **Details** + * + * See `HttpClientResponse.upgrade` for how the connection behaves and when it + * closes. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface Upgraded { + readonly headers: Headers.Headers + readonly socket: Socket.Socket +} + type SuccessDecodingServices = S extends HttpApiSchema.StreamSse< infer _Events, infer _Error, @@ -107,6 +133,7 @@ type SuccessDecodingServices = S extends HttpApiSchema.StreamSse< | _Events["DecodingServices"] | _Error["DecodingServices"] : S extends HttpApiSchema.StreamUint8Array ? never + : S extends HttpApiSchema.StreamNdjson ? _S["DecodingServices"] : S extends Schema.Constraint ? S["DecodingServices"] : never @@ -161,7 +188,8 @@ export declare namespace Client { Endpoint extends HttpApiEndpoint.ConstraintRequest, E, R, - Mode extends ResponseMode + Mode extends ResponseMode, + PayloadR > = Effect.Effect< Response, Mode>, | HttpApiMiddleware.Error @@ -170,6 +198,7 @@ export declare namespace Client { | HttpClientError.HttpClientError | ([Mode] extends ["response-only"] ? never : Endpoint["~Error"]["Type"] | Schema.SchemaError), | R + | PayloadR | Endpoint["~Params"]["EncodingServices"] | Endpoint["~Query"]["EncodingServices"] | Endpoint["~Payload"]["EncodingServices"] @@ -186,6 +215,11 @@ export declare namespace Client { * error, and service channels reflect the endpoint schemas, middleware, and * selected response mode. * + * **Details** + * + * The services of a streaming `payload` are inferred and added to the + * returned effect. + * * @stability unstable * @category models * @since 4.0.0 @@ -194,17 +228,18 @@ export declare namespace Client { Endpoint extends HttpApiEndpoint.ConstraintRequest, E, R - > = ( + > = ( request: Simplify< HttpApiEndpoint.ClientRequest< Endpoint["~Params"], Endpoint["~Query"], Endpoint["~Payload"], Endpoint["~Headers"], - Mode + Mode, + PayloadR > > - ) => MethodReturn + ) => MethodReturn /** * Extracts client methods for endpoints in top-level groups so they can be exposed @@ -412,13 +447,30 @@ export const makeClient = + Effect.map(response.upgrade, (socket): Upgraded => ({ headers: response.headers, socket })) + } const encodeParams = UndefinedOr.map( endpoint.params, (schema) => Schema.encodeUnknownEffect(schema, parseOptions.params) ) - const payloadSchemas = HttpApiEndpoint.getPayloadSchemas(endpoint) + const payloadSchemas: Array = [] + let streamPayload: StreamPayloadSchema | undefined + for (const schema of HttpApiEndpoint.getPayloadSchemas(endpoint)) { + if (HttpApiSchema.isStreamUint8Array(schema) || HttpApiSchema.isStreamNdjson(schema)) { + streamPayload = schema + } else { + payloadSchemas.push(schema) + } + } + const streamPayloadEncoder = UndefinedOr.map(streamPayload, (schema) => ({ + contentType: schema.contentType, + encode: makeStreamPayloadEncoder(schema, parseOptions.payload) + })) const encodePayload = Arr.isArrayNonEmpty(payloadSchemas) ? Schema.encodeUnknownEffect( HttpMethod.hasBody(endpoint.method) @@ -459,7 +511,19 @@ export const makeClient = () + httpRequest = HttpClientRequest.setBody( + httpRequest, + HttpBody.stream( + Stream.provideContext( + streamPayloadEncoder.encode(request.payload), + context as Context.Context + ), + streamPayloadEncoder.contentType + ) + ) + } else if (encodePayload !== undefined) { if (HttpMethod.hasBody(endpoint.method)) { if (request.payload instanceof FormData) { httpRequest = HttpClientRequest.bodyFormData(httpRequest, request.payload) @@ -471,6 +535,10 @@ export const makeClient = httpRequest = HttpClientRequest.appendUrlParams(httpRequest, urlParams) } + } else if (streamPayloadEncoder !== undefined) { + return yield* new Schema.SchemaError( + new SchemaIssue.InvalidValue({ message: "Expected a Stream payload" }, request.payload) + ) } // headers @@ -486,6 +554,10 @@ export const makeClient = (api: Api, options?: { // ---------------------------------------------------------------------------- -const paramsRegExp = /(\/?):(\w+)(\?)?/g - const compilePath = (path: string, schema: Schema.Top | undefined) => { if (schema === undefined || !path.includes(":")) { return (_: any) => path } const paramNames = HttpApiPath.getParamNames(schema) - return (params: Record) => { - return path.replace(paramsRegExp, (match, slash: string, key: string, optional: string | undefined) => { - if (paramNames !== undefined && !paramNames.has(key)) { - return match - } + return (params: Record) => + HttpApiPath.replaceParams(path, paramNames, (key, optional) => { const value = params[key] - if (value === undefined) { - if (optional !== undefined) { - return "" - } - throw new Error(`Missing path parameter: ${key}`) - } - return `${slash}${encodeURIComponent(value)}` + if (value !== undefined) return encodeURIComponent(value) + if (optional) return undefined + throw new Error(`Missing path parameter: ${key}`) }) - } } function schemasToResponse( @@ -932,6 +994,12 @@ const isWithHeadersStreamSuccess = ( schema: StreamSuccessSchema ): schema is HttpApiSchema.WithHeaders => HttpApiSchema.isWithHeaders(schema) +function getUpgradeSuccessSchema(endpoint: HttpApiEndpoint.Top): HttpApiSchema.Upgrade | undefined { + for (const schema of endpoint.success) { + if (HttpApiSchema.isUpgrade(schema)) return schema + } +} + function getStreamSuccessSchemas(endpoint: HttpApiEndpoint.Top): Array { const schemas: Array = [] for (const schema of endpoint.success) { @@ -950,18 +1018,11 @@ function streamToResponse( ) { const isWithHeaders = isWithHeadersStreamSuccess(successSchema) const streamSchema = isWithHeaders ? successSchema.schema : successSchema - const sse = HttpApiSchema.isStreamUint8Array(streamSchema) - ? undefined - : { - declaration: streamSchema, - decoder: makeSseDecoder(streamSchema, options) - } + const decodeBody = makeStreamBodyDecoder(streamSchema, options) const toStream: ResponseDecoder = (response, sseOptions) => Effect.map(Effect.context(), (context) => Stream.provideContext( - sse === undefined ? - response.stream : - decodeSseStream(response.stream, sse.declaration, sse.decoder(sseOptions)), + decodeBody(response, sseOptions), context as Context.Context )) if (!isWithHeaders) return toStream @@ -974,6 +1035,55 @@ function streamToResponse( ) } +function makeStreamBodyDecoder( + streamSchema: HttpApiSchema.StreamSchema, + options: SchemaAST.ParseOptions | undefined +): ( + response: HttpClientResponse.HttpClientResponse, + sseOptions: Sse.DecodeOptions | undefined +) => Stream.Stream { + if (HttpApiSchema.isStreamUint8Array(streamSchema)) { + return (response) => response.stream + } + if (HttpApiSchema.isStreamNdjson(streamSchema)) { + const decodeElement = Schema.decodeUnknownEffect(Schema.toCodecJson(streamSchema.schema), options) + const maxElementBytes = ByteSize.toNumberUnsafe(streamSchema.maxElementBytes) + return (response) => + response.stream.pipe( + Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true, maxLineBytes: maxElementBytes })), + Stream.mapError((error) => { + if (!Ndjson.isNdjsonError(error)) return error + if (error.reason !== "LineTooLarge") return ndjsonSchemaError(error) + return new HttpClientError.HttpClientError({ + reason: new HttpClientError.DecodeError({ + request: response.request, + response, + description: `NDJSON line exceeds maxElementBytes of ${maxElementBytes} bytes` + }) + }) + }), + Stream.mapEffect((json) => decodeElement(json)) + ) + } + const decoder = makeSseDecoder(streamSchema, options) + return (response, sseOptions) => decodeSseStream(response.stream, streamSchema, decoder(sseOptions)) +} + +// `JSON.stringify` found no JSON representation for an encoded element +const isNdjsonPackError = (error: unknown): error is Ndjson.NdjsonError => + Ndjson.isNdjsonError(error) && error.kind === "Pack" + +const ndjsonSchemaError = (error: Ndjson.NdjsonError): Schema.SchemaError => + new Schema.SchemaError( + new SchemaIssue.InvalidValue({ + message: error.reason === "InvalidUtf8" + ? "Expected valid UTF-8" + : error.unterminated + ? "Expected a valid JSON line, got an unterminated final line" + : "Expected a valid JSON line" + }) + ) + function makeSseDecoder( declaration: HttpApiSchema.StreamSse, parseOptions: SchemaAST.ParseOptions | undefined @@ -1138,6 +1248,24 @@ const statusOrElse = (response: HttpClientResponse.HttpClientResponse) => const $HttpBody = Schema.declare(HttpBody.isHttpBody) +type StreamPayloadSchema = HttpApiSchema.StreamUint8Array | HttpApiSchema.StreamNdjson + +function makeStreamPayloadEncoder( + schema: StreamPayloadSchema, + options: SchemaAST.ParseOptions | undefined +): (stream: Stream.Stream) => Stream.Stream { + if (HttpApiSchema.isStreamUint8Array(schema)) { + return (stream) => stream as Stream.Stream + } + const encodeElements = Schema.encodeUnknownEffect(Schema.NonEmptyArray(Schema.toCodecJson(schema.schema)), options) + return (stream) => + stream.pipe( + Stream.mapArrayEffect((elements) => encodeElements(elements)), + Stream.pipeThroughChannel(Ndjson.encode()), + Stream.catchIf(isNdjsonPackError, Stream.die) + ) +} + function getEncodePayloadSchema( schemas: readonly [Schema.Constraint, ...Array], method: HttpMethod.HttpMethod diff --git a/repos/effect/packages/effect/src/http-api/HttpApiEndpoint.ts b/packages/effect/src/http-api/HttpApiEndpoint.ts similarity index 87% rename from repos/effect/packages/effect/src/http-api/HttpApiEndpoint.ts rename to packages/effect/src/http-api/HttpApiEndpoint.ts index 714926546..db3a91a88 100644 --- a/repos/effect/packages/effect/src/http-api/HttpApiEndpoint.ts +++ b/packages/effect/src/http-api/HttpApiEndpoint.ts @@ -18,7 +18,7 @@ import * as Context from "../Context.ts" import type { Effect } from "../Effect.ts" import type * as Sse from "../encoding/Sse.ts" import { identity, memoize } from "../Function.ts" -import type { HttpMethod } from "../http/HttpMethod.ts" +import { hasBody, type HttpMethod } from "../http/HttpMethod.ts" import * as HttpRouter from "../http/HttpRouter.ts" import type { HttpServerRequest } from "../http/HttpServerRequest.ts" import type { HttpServerResponse } from "../http/HttpServerResponse.ts" @@ -27,9 +27,11 @@ import { type Pipeable, pipeArguments } from "../Pipeable.ts" import * as Predicate from "../Predicate.ts" import * as Schema from "../Schema.ts" import * as AST from "../SchemaAST.ts" +import type * as Socket from "../socket/Socket.ts" import type * as Stream from "../Stream.ts" import type { Simplify } from "../Struct.ts" import type * as Types from "../Types.ts" +import type * as HttpApiError from "./HttpApiError.ts" import type * as HttpApiGroup from "./HttpApiGroup.ts" import type * as HttpApiMiddleware from "./HttpApiMiddleware.ts" import * as HttpApiSchema from "./HttpApiSchema.ts" @@ -57,6 +59,8 @@ type SuccessType = S extends HttpApiSchema.WithHeaders< infer _Value > ? Stream.Stream<_Value, _Error["Type"], never> : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream + : S extends HttpApiSchema.StreamNdjson ? Stream.Stream<_S["Type"], unknown, never> + : S extends HttpApiSchema.Upgrade ? void : S extends Schema.Constraint ? S["Type"] : never @@ -66,6 +70,7 @@ type SuccessEncodingServices = S extends HttpApiSchema.StreamSse< infer _Value > ? _Events["EncodingServices"] | _Error["EncodingServices"] : S extends HttpApiSchema.StreamUint8Array ? never + : S extends HttpApiSchema.StreamNdjson ? _S["EncodingServices"] : S extends Schema.Constraint ? S["EncodingServices"] : never @@ -75,6 +80,7 @@ type SuccessDecodingServices = S extends HttpApiSchema.StreamSse< infer _Value > ? _Events["DecodingServices"] | _Error["DecodingServices"] : S extends HttpApiSchema.StreamUint8Array ? never + : S extends HttpApiSchema.StreamNdjson ? _S["DecodingServices"] : S extends Schema.Constraint ? S["DecodingServices"] : never @@ -82,13 +88,18 @@ type UnwrapReadonlyArray = S extends ReadonlyArray ? A : S type ExtractBufferedSuccess = Exclude< Extract, Schema.Top>, - HttpApiSchema.StreamSchema | HttpApiSchema.WithHeaders + HttpApiSchema.StreamSchema | HttpApiSchema.Upgrade | HttpApiSchema.WithHeaders > type ExtractStreamSuccess = UnwrapReadonlyArray extends infer Success ? Success extends HttpApiSchema.StreamSchema ? Success : never : never +type ExtractUpgradeSuccess = Extract, HttpApiSchema.Upgrade> + +type UpgradeSocket = [Extract] extends [never] ? {} + : { readonly socket: Socket.Socket } + type ExtractWithHeadersSuccess = UnwrapReadonlyArray extends infer Success ? Success extends HttpApiSchema.WithHeaders ? HttpApiSchema.WithHeaders< _Inner extends HttpApiSchema.StreamSchema ? _Inner : Schema.toCodecJson<_Inner>, @@ -98,8 +109,12 @@ type ExtractWithHeadersSuccess = UnwrapReadonlyArra : never type ToSuccessCodec = [ExtractBufferedSuccess] extends [never] ? - ExtractStreamSuccess | ExtractWithHeadersSuccess - : Schema.toCodecJson> | ExtractStreamSuccess | ExtractWithHeadersSuccess + ExtractStreamSuccess | ExtractUpgradeSuccess | ExtractWithHeadersSuccess + : + | Schema.toCodecJson> + | ExtractStreamSuccess + | ExtractUpgradeSuccess + | ExtractWithHeadersSuccess type ToJsonCodec = [S] extends [never] ? never : [S] extends [Schema.Constraint] ? Schema.toCodecJson @@ -110,13 +125,33 @@ type ToStringTreeCodec = [S] extends [never] ? never : [S] extends [Schema.Constraint] ? Schema.toCodecStringTree : never -type RequestFromParts = +type ToPayloadCodec = [Exclude] extends [never] ? + Extract + : ToJsonCodec> | Extract + +// Stream payloads are keyed on the schema, because a stream schema's `Type` +// describes the response direction. +type RequestPayload = Payload extends HttpApiSchema.StreamUint8Array ? + Stream.Stream + : Payload extends HttpApiSchema.StreamNdjson ? + Stream.Stream<_S["Type"], HttpApiError.HttpApiSchemaError | HttpApiError.HttpApiPayloadTooLarge> + : Payload extends Schema.Top ? BufferedRequestPayload + : never + +type BufferedRequestPayload = PayloadType extends Brand ? + Stream.Stream + : Simplify + +type PayloadError = Payload extends HttpApiSchema.StreamUint8Array ? HttpApiError.HttpApiPayloadTooLarge + : Payload extends HttpApiSchema.StreamNdjson ? + HttpApiError.HttpApiSchemaError | HttpApiError.HttpApiPayloadTooLarge + : never + +type RequestFromParts = + & UpgradeSocket & ([ParamsType] extends [never] ? {} : { readonly params: Simplify }) & ([QueryType] extends [never] ? {} : { readonly query: Simplify }) - & ([PayloadType] extends [never] ? {} - : PayloadType extends Brand ? - { readonly payload: Stream.Stream } - : { readonly payload: Simplify }) + & ([Payload] extends [never] ? {} : { readonly payload: RequestPayload }) & ([HeadersType] extends [never] ? {} : { readonly headers: Simplify }) & { readonly request: HttpServerRequest @@ -124,7 +159,8 @@ type RequestFromParts readonly group: HttpApiGroup.Top } -type RequestRawFromParts = +type RequestRawFromParts = + & UpgradeSocket & ([ParamsType] extends [never] ? {} : { readonly params: Simplify }) & ([QueryType] extends [never] ? {} : { readonly query: Simplify }) & ([HeadersType] extends [never] ? {} : { readonly headers: Simplify }) @@ -138,14 +174,26 @@ type RequestRawFromParts = * Maps normalized media types to a payload encoding strategy and one or more * schemas. Each schema retains its declared content type in its encoding annotation. * + * **Details** + * + * A streaming payload (`HttpApiSchema.StreamUint8Array` or + * `HttpApiSchema.StreamNdjson`) has a `Stream` encoding and is the only schema + * for its media type. + * * @stability unstable * @category models * @since 4.0.0 */ -export type PayloadMap = ReadonlyMap] -}> +export type PayloadMap = ReadonlyMap< + string, + { + readonly encoding: HttpApiSchema.PayloadEncoding + readonly schemas: readonly [Schema.Top, ...Array] + } | { + readonly encoding: { readonly _tag: "Stream"; readonly contentType: string } + readonly schemas: readonly [HttpApiSchema.StreamUint8Array | HttpApiSchema.StreamNdjson] + } +> /** * Represents an API endpoint. An API endpoint is mapped to a single route on @@ -178,8 +226,15 @@ export interface HttpApiEndpoint< readonly "~Error": Error readonly "~Middleware": Middleware readonly "~MiddlewareServices": MiddlewareServices - readonly "~Request": RequestFromParts - readonly "~RequestRaw": RequestRawFromParts + readonly "~Request": RequestFromParts< + this, + Params["Type"], + Query["Type"], + Payload, + Headers["Type"], + Success + > + readonly "~RequestRaw": RequestRawFromParts readonly identifier: Identifier readonly path: Path @@ -518,7 +573,7 @@ export type RequestRaw = Endpoint extends ConstraintRequest ? Endpoint * Builds the request object accepted by a generated client method, including the * params, query, headers, and payload required by the endpoint, plus optional * response mode and SSE decoding options. Multipart payloads are supplied as - * `FormData`. + * `FormData`, and streaming payloads as a `Stream` that requires `PayloadR`. * * @stability unstable * @category utility types @@ -529,7 +584,8 @@ export type ClientRequest< Query extends Schema.Constraint, Payload extends Schema.Constraint, Headers extends Schema.Constraint, - ResponseMode extends ClientResponseMode + ResponseMode extends ClientResponseMode, + PayloadR = never > = ( & ([Params["Type"]] extends [never] ? {} : { readonly params: Params["Type"] }) & ([Query["Type"]] extends [never] ? {} : { readonly query: Query["Type"] }) @@ -538,8 +594,8 @@ export type ClientRequest< : Payload["Type"] extends infer P ? P extends Brand | Brand ? { readonly payload: FormData } - : { readonly payload: Payload["Type"] } - : { readonly payload: Payload["Type"] }) + : { readonly payload: ClientPayload } + : { readonly payload: ClientPayload }) ) extends infer Req ? keyof Req extends never ? (void | { readonly responseMode?: ResponseMode readonly sseOptions?: Sse.DecodeOptions | undefined @@ -550,6 +606,11 @@ export type ClientRequest< } : void +type ClientPayload = Payload extends HttpApiSchema.StreamUint8Array + ? Stream.Stream + : Payload extends HttpApiSchema.StreamNdjson ? Stream.Stream + : Payload["Type"] + /** * Controls what a generated client method returns: the decoded success value, * the decoded value paired with the raw response, or only the raw response. @@ -628,7 +689,11 @@ export type ErrorServicesDecode = Endpoint extends ConstraintRequest ? */ export type Handler = ( request: Simplify -) => Effect | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R> +) => Effect< + SuccessType | HttpServerResponse, + Endpoint["~Error"]["Type"] | PayloadError> | E, + R +> /** * The raw server handler for an endpoint, receiving a request shape without a @@ -981,7 +1046,9 @@ export type QueryConstraint = * encode to `string | ReadonlyArray | undefined` and OpenAPI can expand * it into `in: query` parameters * - for body methods, payload may be any `Schema.Top` (or content-type keyed - * schemas) and OpenAPI uses `requestBody` instead of `parameters` + * schemas) and OpenAPI uses `requestBody` instead of `parameters`. This + * includes one `HttpApiSchema.StreamUint8Array` or `HttpApiSchema.StreamNdjson`, + * but not `HttpApiSchema.StreamSse`, which is response-only * * @stability unstable * @category utility types @@ -991,7 +1058,10 @@ export type PayloadConstraint = Method extends HttpMe string, Schema.Encoder | undefined, unknown> > : - Schema.Top | ReadonlyArray + PayloadSchema | ReadonlyArray + +// `StreamSse` is response-only, so it is the one stream schema excluded here. +type PayloadSchema = Schema.Top & { readonly sseMode?: never } /** * Payload constraint used when automatic codecs are enabled: no-body methods @@ -1004,7 +1074,7 @@ export type PayloadConstraint = Method extends HttpMe */ export type PayloadConstraintCodecs = Method extends HttpMethod.NoBody ? Record : - Schema.Top | ReadonlyArray + PayloadSchema | ReadonlyArray /** * Constraint for success response schemas, allowing either a single schema or a @@ -1031,7 +1101,7 @@ type ErrorSchema = S extends HttpApiSchema.WithHeaders = [ Extract< ErrorSchema ? S[number] : S>, - HttpApiSchema.StreamSchema + HttpApiSchema.StreamSchema | HttpApiSchema.Upgrade > ] extends [never] ? S : never @@ -1073,7 +1143,7 @@ export const make = (method: Method): { Path, ToStringTreeCodec, ToStringTreeCodec, - Method extends HttpMethod.WithBody ? ToJsonCodec> + Method extends HttpMethod.WithBody ? ToPayloadCodec> : ToStringTreeCodec>, ToStringTreeCodec, ToSuccessCodec, @@ -1156,7 +1226,7 @@ export const make = (method: Method): { query: ensureStruct(options?.query, transformStringTree), headers: ensureStruct(options?.headers, transformStringTree), payload: getPayload(options?.payload, method, disableCodecs), - success: getSuccessResponse(options?.success, method, disableCodecs), + success: getSuccessResponse(options?.success, method, options?.payload !== undefined, disableCodecs), error: getErrorResponse(options?.error, disableCodecs), annotations: Context.empty(), middlewares: new Set(), @@ -1185,21 +1255,46 @@ function getPayload( ): PayloadMap { const result: Map< string, - { encoding: HttpApiSchema.PayloadEncoding; schemas: [Schema.Top, ...Array] } + { + encoding: HttpApiSchema.PayloadEncoding | { readonly _tag: "Stream"; readonly contentType: string } + schemas: [Schema.Top, ...Array] + } > = new Map() - if (payload === undefined) return result + if (payload === undefined) return result as PayloadMap const schemas: Array = Array.isArray(payload) ? payload : Schema.isSchema(payload) ? [payload] : [(Schema.Struct(payload as any)).pipe(HttpApiSchema.asFormUrlEncoded())] const transform = disableCodecs ? identity : transformPayload + let hasStream = false for (const schema of schemas) { + if (HttpApiSchema.isStreamSchema(schema)) { + if (HttpApiSchema.isStreamSse(schema)) { + throw new Error("StreamSse is response-only and cannot be used as a payload") + } + if (!hasBody(method)) { + throw new Error(`${method} endpoints cannot declare a streaming payload`) + } + if (hasStream) { + throw new Error("Multiple streaming payloads are not supported") + } + hasStream = true + const contentType = MediaType.normalize(schema.contentType) + if (result.has(contentType)) { + throw new Error(`Cannot combine buffered and streaming payloads for content-type: ${schema.contentType}`) + } + result.set(contentType, { encoding: { _tag: "Stream", contentType: schema.contentType }, schemas: [schema] }) + continue + } const encoding = HttpApiSchema.getPayloadEncoding(schema.ast, method) const contentType = MediaType.normalize(encoding.contentType) const existing = result.get(contentType) if (existing) { + if (existing.encoding._tag === "Stream") { + throw new Error(`Cannot combine buffered and streaming payloads for content-type: ${encoding.contentType}`) + } if (existing.encoding._tag !== encoding._tag) { throw new Error(`Multiple payload encodings for content-type: ${encoding.contentType}`) } @@ -1211,7 +1306,7 @@ function getPayload( result.set(contentType, { encoding, schemas: [transform(schema, method)] }) } } - return result + return result as PayloadMap } const reservedStreamFailureEvent = "effect/http-api/stream/failure" @@ -1219,16 +1314,17 @@ const reservedStreamFailureEvent = "effect/http-api/stream/failure" function getSuccessResponse( success: Schema.Top | ReadonlyArray | undefined, method: HttpMethod, + hasPayload: boolean, disableCodecs: boolean ): Set { if (success === undefined) return new Set() const schemas = Arr.ensure(success) - validateSuccessResponse(schemas, method) + validateSuccessResponse(schemas, method, hasPayload) return new Set(disableCodecs ? schemas : schemas.map(transformResponseSchema)) } const transformResponseSchema = memoize((schema: Schema.Top): Schema.Top => { - if (HttpApiSchema.isStreamSchema(schema)) return schema + if (HttpApiSchema.isStreamSchema(schema) || HttpApiSchema.isUpgrade(schema)) return schema if (HttpApiSchema.isWithHeaders(schema)) { const inner = HttpApiSchema.isStreamSchema(schema.schema) ? schema.schema @@ -1249,12 +1345,27 @@ function getErrorResponse( if (HttpApiSchema.isStreamSchema(body)) { throw new Error("Streaming schemas are not supported in error responses") } + if (HttpApiSchema.isUpgrade(body)) { + throw new Error("Upgrade schemas are not supported in error responses") + } } validateResponseExclusivity(schemas, HttpApiSchema.getStatusErrorSchema) return new Set(disableCodecs ? schemas : schemas.map(transformResponseSchema)) } -function validateSuccessResponse(schemas: ReadonlyArray, method: HttpMethod) { +function validateSuccessResponse(schemas: ReadonlyArray, method: HttpMethod, hasPayload: boolean) { + if (schemas.some(HttpApiSchema.isUpgrade)) { + if (schemas.length > 1) { + throw new Error("Cannot combine an upgrade with other success responses") + } + if (method === "HEAD") { + throw new Error("HEAD endpoints cannot declare an upgrade") + } + if (hasPayload) { + throw new Error("Upgrade endpoints cannot declare a payload") + } + return + } let hasStream = false const statuses = new Map { + readonly [HttpApiPayloadTooLargeTypeId]: HttpApiPayloadTooLargeTypeId = HttpApiPayloadTooLargeTypeId + readonly [ErrorReporter.ignore] = true + + static is(u: unknown): u is HttpApiPayloadTooLarge { + return hasProperty(u, HttpApiPayloadTooLargeTypeId) + } + + readonly name = "HttpApiPayloadTooLarge" + readonly message = `Payload exceeds ${this.limit} of ${this.size} bytes`; + + [HttpServerRespondable.symbol]() { + return Effect.succeed(payloadTooLargeResponse) + } +} diff --git a/repos/effect/packages/effect/src/http-api/HttpApiGroup.ts b/packages/effect/src/http-api/HttpApiGroup.ts similarity index 100% rename from repos/effect/packages/effect/src/http-api/HttpApiGroup.ts rename to packages/effect/src/http-api/HttpApiGroup.ts diff --git a/repos/effect/packages/effect/src/http-api/HttpApiMiddleware.ts b/packages/effect/src/http-api/HttpApiMiddleware.ts similarity index 100% rename from repos/effect/packages/effect/src/http-api/HttpApiMiddleware.ts rename to packages/effect/src/http-api/HttpApiMiddleware.ts diff --git a/repos/effect/packages/effect/src/http-api/HttpApiScalar.ts b/packages/effect/src/http-api/HttpApiScalar.ts similarity index 96% rename from repos/effect/packages/effect/src/http-api/HttpApiScalar.ts rename to packages/effect/src/http-api/HttpApiScalar.ts index eb585fa80..a8666cd4b 100644 --- a/repos/effect/packages/effect/src/http-api/HttpApiScalar.ts +++ b/packages/effect/src/http-api/HttpApiScalar.ts @@ -17,7 +17,6 @@ import type * as Layer from "../Layer.ts" import type * as HttpApi from "./HttpApi.ts" import type * as HttpApiGroup from "./HttpApiGroup.ts" import * as Html from "./internal/html.ts" -import * as internal from "./internal/httpApiScalar.ts" import * as OpenApi from "./OpenApi.ts" /** @@ -212,7 +211,8 @@ const makeHandler = ( * **Details** * * The route serves the OpenAPI specification generated from the API at the - * configured path, defaulting to `/docs`. + * configured path, defaulting to `/docs`. The bundled script is loaded when + * the layer is built, not when this module is imported. * * @stability unstable * @category layers @@ -226,11 +226,12 @@ export const layer = } | undefined ): Layer.Layer => HttpRouter.use(Effect.fnUntraced(function*(router) { + const { javascript } = yield* Effect.promise(() => import("./internal/httpApiScalar.ts")) const handler = makeHandler({ api, source: { _tag: "Inline", - source: internal.javascript + source: javascript }, scalar: options?.scalar }) diff --git a/repos/effect/packages/effect/src/http-api/HttpApiSchema.ts b/packages/effect/src/http-api/HttpApiSchema.ts similarity index 76% rename from repos/effect/packages/effect/src/http-api/HttpApiSchema.ts rename to packages/effect/src/http-api/HttpApiSchema.ts index 617a81609..129c8ee71 100644 --- a/repos/effect/packages/effect/src/http-api/HttpApiSchema.ts +++ b/packages/effect/src/http-api/HttpApiSchema.ts @@ -10,6 +10,7 @@ * @stability unstable * @since 4.0.0 */ +import * as ByteSize from "../ByteSize.ts" import * as Sse from "../encoding/Sse.ts" import { constVoid, type LazyArg } from "../Function.ts" import { hasBody, type HttpMethod } from "../http/HttpMethod.ts" @@ -19,6 +20,7 @@ import * as Predicate from "../Predicate.ts" import * as Schema from "../Schema.ts" import * as SchemaAST from "../SchemaAST.ts" import * as SchemaTransformation from "../SchemaTransformation.ts" +import * as Socket from "../socket/Socket.ts" import * as Stream from "../Stream.ts" declare module "../Schema.ts" { @@ -120,8 +122,12 @@ export type StatusLiteral = HttpStatus.Literal * * **Details** * - * This is equivalent to calling `.annotate({ httpApiStatus: code })` on the - * schema. You can pass either a numeric status code (for example, `201`) or a + * The status is stored with `.annotateKey({ httpApiStatus: code })`, so a named + * schema reused with different statuses still emits a single OpenAPI component. + * If the schema has checks or already has an `httpApiStatus` annotation, it is + * stored with `.annotate({ httpApiStatus: code })` instead. A later + * `.annotate({ httpApiStatus })` takes precedence, and a later check drops the + * status. You can pass either a numeric status code (for example, `201`) or a * common literal name (for example, `"Created"`). * * @stability unstable @@ -142,7 +148,10 @@ export function status(code: StatusLiteral): { */ export function status(code: number | StatusLiteral) { const statusCode = typeof code === "string" ? HttpStatus.fromLiteral(code) : code - return (self: S): S["Rebuild"] => self.annotate({ httpApiStatus: statusCode }) + return (self: S): S["Rebuild"] => + self.ast.checks !== undefined || hasStatusAnnotation(self.ast) + ? self.annotate({ httpApiStatus: statusCode }) + : self.annotateKey({ httpApiStatus: statusCode }) } /** @@ -251,7 +260,7 @@ export function asNoContent(options: { } } -type StreamMode = "sse" | "uint8array" +type StreamMode = "sse" | "uint8array" | "ndjson" /** * Mode describing whether an SSE stream emits full events or raw data values. @@ -282,6 +291,9 @@ export type StreamSseMode = "events" | "data" * application event. Endpoint construction rejects event schemas that declare * the reserved name statically. * + * `StreamSse` is response-only. The `payload` slot's type excludes it, and + * endpoint construction rejects it. + * * @stability unstable * @category models * @since 4.0.0 @@ -339,13 +351,14 @@ export interface SseEventFromData extends {} /** - * Schema for a streaming `Uint8Array` success response. + * Schema for a streaming `Uint8Array` body. * * **Details** * - * This declaration stores the response content type for later endpoint, - * server, client, and OpenAPI integrations. It is intentionally separate from - * the buffered `asUint8Array` response encoding. + * It works as a success response or as a request payload. It is intentionally + * separate from the buffered `asUint8Array` encoding. As a payload, the handler + * receives the body as it is read, and `maxBytes` caps the bytes read. It has + * no default and does not fall back to `MaxBodySize`. * * @stability unstable * @category models @@ -366,22 +379,91 @@ export interface StreamUint8Array extends readonly _tag: "StreamUint8Array" readonly mode: "uint8array" readonly contentType: string + readonly maxBytes: ByteSize.ByteSize | undefined +} + +/** + * Schema for a stream of newline-delimited JSON elements. + * + * **Details** + * + * Each element is encoded as one line of JSON followed by `\n`, including the + * last. When decoding, `\n`, `\r\n` and a bare `\r` end a line, empty lines are + * skipped, a leading BOM is stripped and invalid UTF-8 fails. The first + * malformed line or element that fails to decode ends the stream, after every + * element before it has been delivered. A missing final newline is accepted if + * the last line parses. + * + * `maxElementBytes` caps the UTF-8 bytes of one line, without its terminator. + * `maxBytes` caps the whole body and has no default. + * + * As a request payload, the handler receives the elements as the body is + * read. Malformed lines and elements that fail to decode are an + * `HttpApiSchemaError`, and limit breaches are an `HttpApiPayloadTooLarge`. + * + * **Gotchas** + * + * There is no `error` schema. A failure mid-stream aborts the connection, so + * the reader sees a body read error, never a clean end. Typed failures + * mid-stream are a {@link StreamSse} feature. A body cut cleanly at a line + * boundary can't be told apart from a complete one. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface StreamNdjson extends + Schema.BottomLazy< + SchemaAST.Declaration, + StreamNdjson + > +{ + readonly "Type": Stream.Stream + readonly "Encoded": Stream.Stream + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "Rebuild": StreamNdjson + readonly "~type.make.in": Stream.Stream + readonly "~type.make": Stream.Stream + readonly "Iso": Stream.Stream + readonly [StreamSchemaTypeId]: typeof StreamSchemaTypeId + readonly _tag: "StreamNdjson" + readonly mode: "ndjson" + readonly contentType: string + readonly schema: S + readonly maxBytes: ByteSize.ByteSize | undefined + readonly maxElementBytes: ByteSize.ByteSize } /** - * Schema for a streaming HTTP API success response. + * Schema for a streaming HTTP API body. + * + * **Details** + * + * {@link StreamUint8Array} and {@link StreamNdjson} describe the body in either + * direction, with the endpoint slot deciding which. {@link StreamSse} is for + * success responses only. * * @stability unstable * @category models * @since 4.0.0 */ -export type StreamSchema = StreamSse | StreamUint8Array +export type StreamSchema = + | StreamSse + | StreamUint8Array + | StreamNdjson const streamSchema = Schema.declare(Stream.isStream) +const defaultMaxElementBytes = ByteSize.mebibytes(1) + /** * Creates a Server-Sent Events streaming success response schema. * + * **Details** + * + * `StreamSse` is response-only. Endpoint construction rejects it in `payload`. + * * @stability unstable * @category constructors * @since 4.0.0 @@ -422,7 +504,12 @@ export const StreamSse: { } /** - * Creates a streaming `Uint8Array` success response schema. + * Creates a streaming `Uint8Array` body schema. + * + * **Details** + * + * `contentType` defaults to `application/octet-stream`. `maxBytes` only applies + * to request payloads and has no default. * * @stability unstable * @category constructors @@ -430,12 +517,64 @@ export const StreamSse: { */ export const StreamUint8Array = (options?: { readonly contentType?: string | undefined + readonly maxBytes?: ByteSize.Input | undefined }): StreamUint8Array => Schema.make(streamSchema.ast, { [StreamSchemaTypeId]: StreamSchemaTypeId, _tag: "StreamUint8Array", mode: "uint8array", - contentType: options?.contentType ?? defaultStreamContentType("uint8array") + contentType: options?.contentType ?? defaultStreamContentType("uint8array"), + maxBytes: options?.maxBytes === undefined ? undefined : ByteSize.fromInputUnsafe(options.maxBytes) + }) + +/** + * Creates a schema for a stream of newline-delimited JSON elements. + * + * **Details** + * + * `contentType` defaults to `application/x-ndjson` and `maxElementBytes` to 1 + * MiB. `maxBytes` has no default. + * + * **Example** (Declaring an NDJSON request payload) + * + * ```ts import.meta.vitest + * import { Schema } from "effect" + * import { HttpApiEndpoint, HttpApiSchema } from "effect/http-api" + * + * const Event = Schema.Struct({ id: Schema.Number }) + * const payload = HttpApiSchema.StreamNdjson({ schema: Event, maxBytes: "100 MiB" }) + * + * // The handler receives a Stream of Events + * const importEvents = HttpApiEndpoint.post("importEvents", "/events", { + * payload, + * success: Schema.Number + * }) + * + * importEvents.path // => "/events" + * payload.contentType // => "application/x-ndjson" + * payload.maxElementBytes // => 1048576n + * ``` + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const StreamNdjson = (options: { + readonly schema: S + readonly contentType?: string | undefined + readonly maxBytes?: ByteSize.Input | undefined + readonly maxElementBytes?: ByteSize.Input | undefined +}): StreamNdjson => + Schema.make>(streamSchema.ast, { + [StreamSchemaTypeId]: StreamSchemaTypeId, + _tag: "StreamNdjson", + mode: "ndjson", + contentType: options.contentType ?? defaultStreamContentType("ndjson"), + schema: options.schema, + maxBytes: options.maxBytes === undefined ? undefined : ByteSize.fromInputUnsafe(options.maxBytes), + maxElementBytes: options.maxElementBytes === undefined + ? defaultMaxElementBytes + : ByteSize.fromInputUnsafe(options.maxElementBytes) }) /** @@ -456,12 +595,109 @@ export const isStreamSse = (u: unknown): u is StreamSse isStreamSchema(u) && u._tag === "StreamUint8Array" +/** + * @internal + */ +export const isStreamNdjson = (u: unknown): u is StreamNdjson => + isStreamSchema(u) && u._tag === "StreamNdjson" + +const UpgradeTypeId = "~effect/http-api/HttpApiSchema/Upgrade" + +/** + * Schema for a success response that switches the connection to another + * protocol with an HTTP/1.1 upgrade. + * + * **Details** + * + * Created with {@link Upgrade}. The response is `101 Switching Protocols`, and + * the connection then carries raw bytes in both directions as a `Socket`. + * + * @stability unstable + * @category models + * @since 4.0.6 + */ +export interface Upgrade extends + Schema.Bottom< + Socket.Socket, + Socket.Socket, + never, + never, + SchemaAST.Declaration, + Upgrade + > +{ + readonly "Rebuild": Upgrade + readonly [UpgradeTypeId]: typeof UpgradeTypeId + readonly protocol: string +} + +const upgradeSchema = Schema.declare(Socket.isSocket).pipe(status(101)) + +/** + * Declares an endpoint success that upgrades the connection to `protocol`, + * such as Docker's `tcp` for container attach. + * + * **When to use** + * + * Use as the only success schema of an endpoint that hands over its + * connection as a raw byte stream after `101 Switching Protocols`. For + * WebSocket, upgrade the request with `HttpServerRequest.upgrade` and + * connect with `Socket.makeWebSocket` instead. + * + * **Details** + * + * The handler receives the connection as `socket`. The `101` response is + * sent when the handler acquires the socket's reader, so a handler can still + * fail with a declared error before then. Requests that do not ask for + * `protocol` get `426 Upgrade Required`. + * + * The client sends `Connection: Upgrade` and `Upgrade: protocol`, and returns + * the response headers with the connection. Other responses are decoded as + * the endpoint's errors. + * + * The endpoint cannot declare a payload or other success schemas. + * + * **Example** (Declaring an attach endpoint) + * + * ```ts + * import { Schema } from "effect" + * import { HttpApiEndpoint, HttpApiSchema } from "effect/http-api" + * + * const attach = HttpApiEndpoint.post("attach", "/containers/:id/attach", { + * params: { id: Schema.String }, + * success: HttpApiSchema.Upgrade({ protocol: "tcp" }) + * }) + * ``` + * + * @stability unstable + * @category constructors + * @since 4.0.6 + */ +export const Upgrade = (options: { + readonly protocol: string +}): Upgrade => { + if (options.protocol.toLowerCase() === "websocket") { + throw new Error("Upgrade does not support WebSocket") + } + return Schema.make(upgradeSchema.ast, { + [UpgradeTypeId]: UpgradeTypeId, + protocol: options.protocol + }) +} + +/** + * @internal + */ +export const isUpgrade = (u: unknown): u is Upgrade => Schema.isSchema(u) && Predicate.hasProperty(u, UpgradeTypeId) + function defaultStreamContentType(mode: StreamMode): string { switch (mode) { case "sse": return "text/event-stream" case "uint8array": return "application/octet-stream" + case "ndjson": + return "application/x-ndjson" } } @@ -627,6 +863,9 @@ export function WithHeaders( if (isWithHeaders(schema)) { throw new Error("WithHeaders schemas cannot be nested") } + if (isUpgrade(schema)) { + throw new Error("WithHeaders cannot wrap an upgrade") + } return Schema.make>(withHeadersValueSchema.ast, { [WithHeadersTypeId]: WithHeadersTypeId, schema, @@ -791,6 +1030,9 @@ export function encodeToWithHeaders< }) { return (self: S): encodeToWithHeaders => { const body = options.body + if (isUpgrade(body)) { + throw new Error("encodeToWithHeaders cannot wrap an upgrade") + } const headers = Schema.Struct(options.headers) const status = resolveHttpApiStatus(body.ast) const encoding = resolveHttpApiEncoding(body.ast) @@ -1031,7 +1273,19 @@ const resolveHttpApiEncoding = SchemaAST.resolveAt("~httpApiEncoding") */ export const getWithHeadersAnnotation = SchemaAST.resolveAt("~httpApiWithHeaders") -const resolveHttpApiStatus = SchemaAST.resolveAt("httpApiStatus") +const resolveHttpApiStatusAnnotation = SchemaAST.resolveAt("httpApiStatus") + +function hasStatusAnnotation(ast: SchemaAST.AST): boolean { + const annotations = SchemaAST.resolve(ast) + return annotations !== undefined && "httpApiStatus" in annotations +} + +// An `httpApiStatus` annotation, even `undefined`, overrides the key context set by +// `status`, which a later check drops, matching annotations +function resolveHttpApiStatus(ast: SchemaAST.AST): number | undefined { + if (hasStatusAnnotation(ast)) return resolveHttpApiStatusAnnotation(ast) + return ast.checks === undefined ? ast.context?.annotations?.httpApiStatus : undefined +} const defaultJsonEncoding: Encoding = { _tag: "Json", diff --git a/repos/effect/packages/effect/src/http-api/HttpApiSecurity.ts b/packages/effect/src/http-api/HttpApiSecurity.ts similarity index 100% rename from repos/effect/packages/effect/src/http-api/HttpApiSecurity.ts rename to packages/effect/src/http-api/HttpApiSecurity.ts diff --git a/repos/effect/packages/effect/src/http-api/HttpApiSwagger.ts b/packages/effect/src/http-api/HttpApiSwagger.ts similarity index 85% rename from repos/effect/packages/effect/src/http-api/HttpApiSwagger.ts rename to packages/effect/src/http-api/HttpApiSwagger.ts index d25913a8c..6a694e4b9 100644 --- a/repos/effect/packages/effect/src/http-api/HttpApiSwagger.ts +++ b/packages/effect/src/http-api/HttpApiSwagger.ts @@ -17,11 +17,12 @@ import type * as Layer from "../Layer.ts" import type * as HttpApi from "./HttpApi.ts" import type * as HttpApiGroup from "./HttpApiGroup.ts" import * as Html from "./internal/html.ts" -import * as internal from "./internal/httpApiSwagger.ts" import * as OpenApi from "./OpenApi.ts" const makeHandler = (options: { readonly api: HttpApi.HttpApi + readonly javascript: string + readonly css: string }) => { const makeResponse = Function.memoize((api: HttpApi.HttpApi) => { const spec = OpenApi.fromApi(api) @@ -31,7 +32,7 @@ const makeHandler = ( ${Html.escape(spec.info.title)} Documentation - +
@@ -39,7 +40,7 @@ const makeHandler = ( ${Html.escapeJson(spec)} - ``` - -## 0.3.3 - -### Patch Changes - -- df2e948: Fixes meta.required wrong inheritance and adds at least storybook story, waiting for unit tests - -## 0.3.2 - -### Patch Changes - -- e7c3aff: deep merge default values which comes from persistency wrt tanstack def values - -## 0.3.1 - -### Patch Changes - -- c8e5220: update OmegaForm persistency configuration to use policies array and add query params option - -## 0.3.0 - -### Minor Changes - -- 6f33b83: Enhance OmegaForm functionality with persistency support and improved input handling. Added clear and reset methods to the form API, updated internal input state management, and refined form stories for better demonstration of features. - -## 0.2.8 - -### Patch Changes - -- 7fcd006: feat: add showErrorOn props in OmegaForm Wrapper components, to manage when to show the errors - -## 0.2.7 - -### Patch Changes - -- f7909fc: Explicitly pass down model-value - -## 0.2.6 - -### Patch Changes - -- 34abab7: Changes spread order in OmegaFormVuetify - -## 0.2.5 - -### Patch Changes - -- f6043f4: manually pass clear function - -## 0.2.4 - -### Patch Changes - -- b9efc9f: Moves from custom ui to standard vuetify ui for clearable selects -- 587a06a: Fixes model value reference inside vuetify components - -## 0.2.3 - -### Patch Changes - -- Updated dependencies [d3b05cb] -- Updated dependencies [4627c7b] -- Updated dependencies [1e1f32b] - - effect-app@2.39.1 - - @effect-app/vue@2.40.1 - -## 0.2.2 - -### Patch Changes - -- Updated dependencies [bdc9d5c] - - effect-app@2.39.0 - - @effect-app/vue@2.40.0 - -## 0.2.1 - -### Patch Changes - -- d01c3e9: Manages focus in OmegaInput - -## 0.2.0 - -### Minor Changes - -- 7ac3d9d: Adds checkbox default to OmegaForm - -## 0.1.3 - -### Patch Changes - -- 21b0696: Fix: OmegaInput Attributes spreading - -## 0.1.2 - -### Patch Changes - -- Updated dependencies [8b7fdbe] - - effect-app@2.38.5 - - @effect-app/vue@2.39.9 - -## 0.1.1 - -### Patch Changes - -- b7cfa49: Fixes default styling for reset in select and autocomplete - -## 0.1.0 - -### Minor Changes - -- 98d5344: Enhance OmegaForm components with new input types and improved structure - - Added support to CSS inside JS bundle both for modules than for custom elements - - OmegaFormInput now fully support children as input, so vuetify it's only a default for environments that globally import that library - - Updated OmegaInput.vue to support autocomplete and autocompletemultiple types. - - Refactored OmegaInternalInput.vue to streamline input handling and improve slot usage. - - Modified OmegaErrors.vue to utilize slots for better flexibility in error display. - - Added new types to OmegaFormStuff.ts for enhanced type safety. - - Adjusted Vite configuration to support custom elements in Vue components. - -## 0.0.9 - -### Patch Changes - -- Updated dependencies - - effect-app@2.38.4 - - @effect-app/vue@2.39.8 - -## 0.0.8 - -### Patch Changes - -- Updated dependencies - - effect-app@2.38.3 - - @effect-app/vue@2.39.7 - -## 0.0.7 - -### Patch Changes - -- Updated dependencies [c0ed28a] -- Updated dependencies [0fcab38] - - effect-app@2.38.2 - - @effect-app/vue@2.39.6 - -## 0.0.6 - -### Patch Changes - -- Updated dependencies [cd15a8e] - - effect-app@2.38.1 - - @effect-app/vue@2.39.5 - -## 0.0.5 - -### Patch Changes - -- Updated dependencies [81479bf] - - effect-app@2.38.0 - - @effect-app/vue@2.39.4 - -## 0.0.4 - -### Patch Changes - -- 6e7dc86: add support for textarea and pass attrs to input. - -## 0.0.3 - -### Patch Changes - -- Updated dependencies [7bedb13] - - effect-app@2.37.1 - - @effect-app/vue@2.39.3 - -## 0.0.2 - -### Patch Changes - -- ce0bfca: fix style - -## 0.0.1 - -### Patch Changes - -- c3a730c: initial vue-components diff --git a/packages/vue-components/README.md b/packages/vue-components/README.md deleted file mode 100644 index b7cea449f..000000000 --- a/packages/vue-components/README.md +++ /dev/null @@ -1,233 +0,0 @@ -[![Netlify Status](https://api.netlify.com/api/v1/badges/b1b84831-789e-4629-a9e3-55a36e136653/deploy-status)](https://app.netlify.com/sites/sharp-babbage-154f0a/deploys) - -# Vue Component Library Starter - -> Create your own [Vue 3](https://v3.vuejs.org/) component library with TypeScript, [Vite](https://vitejs.dev) and [VitePress](https://vitepress.vuejs.org/). - -Sooner or later, you will find that creating a component library is much better than having all components inside your app project. A component library force to you remove app specific logic from your components, making it easier to test and reuse them in other apps. - -Once the components are in a library, documentation becomes critical. This starter project includes a documentation app powered by VitePress. It not only documents the usage of the component, but also provides a testing bed during the development of components. See the generated documentation app [here](https://sharp-babbage-154f0a.netlify.com/). - -## Setup - -> When running `docs:dev` for the first time, you may encounter error like `vitepress data not properly injected in app` in your browser. Restart the server and reload the browser. Please refer to [issue #30](https://github.com/wuruoyun/vue-component-lib-starter/issues/30) for more details. - -```bash -# install dependencies -npm install - -# start the doc app with hot reload, great for testing components -npm run docs:dev - -# build the library, available under dist -npm run build - -# build the doc app, available under docs/.vitepress/dist -npm run docs:build - -# preview the doc app locally from docs/.vitepress/dist -npm run docs:serve -``` - -You may use [Netlify](https://www.netlify.com/) to auto build and deploy the doc app like this project does. - -## Develop and test locally - -The best way to develop and test your component is by creating demos in `docs/components/demo` folder, as shown by the example components. - -If you want to test the library in your Vue3 app locally: - -- In the root folder of this library, run `npm link`. This will create a symbolic link to the library. -- In the root folder of your client app, run `npm link my-lib`. This will add the symbolic link to the `node_modules` folder in your client app. -- You can now import `my-lib` in your client app. - -There is no need to add `my-lib` to your client app's dependency in this case. - -If you made changes to the library, you will need to rebuild the library. Your Vue3 app shall hot reload when the building of library is completed. - -## How it works - -### Components - -The library is a [Vue plugin](https://v3.vuejs.org/guide/plugins.html). The `install` function in [index.ts](src/index.ts) registers all components under [components](src/components) to Vue globably. - -The components are also exported by [index.ts](src/index.ts) so that the client app can import them individually and register them locally, instead of using the library as a plugin. This may be a better option if the client app only use a small set of components in your library. - -As there are already many UI component libraries for Vue 3, you may just want to build on top of one of them and create components for your specific needs. The Component B in this starter shows the example of using [PrimeVue](https://www.primefaces.org/primevue/) as the fundation library. However, this means the client app shall also use the same fundation component library as your library does. - -The doc app itself is a client app of the libary, therefore PrimeVue is imported in [docs/.vitepress/theme/index.js](docs/.vitepress/theme/index.js). The configuration in [docs/.vitepress/config.js](docs/.vitepress/config.js) below forces VitePress to resolve these modules with no duplication, avoiding error at runtime, as PrimeVue also has Vue in its dependency. - -```js -module.exports = { - vite: { - resolve: { - dedupe: ["vue", /primevue\/.+/] - } - } -} -``` - -> In [vite.config.ts](vite.config.ts), format 'umd' is not present in `build.lib.formats` option. This is because the PrimeVue components used by this library are externalized, and therefore requiring corresponding options in `rollupOptions.output.globals`. To avoid adding global varaibles for PrimeVue components, 'umd' is removed for simplicity. - -### Utilities and constants - -The library includes example utilities and constants. They are also exported in [index.ts](src/index.ts). The client app may use them as below: - -```js - -``` - -### Styling - -Individual components may have styles defined in its `.vue` file. They will be processed, combined and minified into `dist/style.css`, which is included in the `exports` list in [package.json](package.json). - -If you have library level styles shared by all components in the library, you may add them to [src/assets/main.scss](src/assets/main.scss). This file is imported in [index.ts](src/index.ts), therefore the processed styles are also included into `dist/style.css`. To avoid conflicting with other global styles, consider pre-fixing the class names or wrapping them into a namespace class. - -If you have your own special set of SVG icons, you may create a font file (`.woff` format) using tools like [Icomoon](https://icomoon.io/) or [Fontello](https://fontello.com/). This starter includes an example font file [src/assets/fonts/myfont.woff](src/assets/fonts/myfont.woff) and references it in [src/assets/main.scss](src/assets/main.scss), with utility icon CSS classes. An icon from the font file is used in Component A. Vite will include the font file into the build, see [https://vitejs.dev/guide/assets.html](https://vitejs.dev/guide/assets.html). - -The client app shall import `style.css`, usually in the entry file: - -```js -import "my-lib/dist/style.css" -``` - -### Third-party dependencies - -Third-party libraries used by you library may bloat up the size of your library, if you simply add them to the `dependencies` in [package.json](package.json). - -The following are some strategies to reduce the size of your library: - -#### Externalization - -If you expect the client app of your library may also need the same dependency, you may externalize the dependency. For example, to exclude PrimeVue from your library build artifact, in [vite.config.ts](vite.config.ts), you may have - -```js -module.exports = defineConfig({ - rollupOptions: { - external: ['vue', /primevue\/.+/] - } - } -}) -``` - -The dependency to be externalized may be declared as peer dependency in your library. - -#### Cherry picking - -If you don't expect the client app of your library also needing the same dependency, you may embed cherry-picked functions. For example, to embed the `fill` function of popular library [lodash](https://lodash.com), import the `fill` function like the following: - -```js -import fill from "lodash/fill" -``` - -Even with tree-shaking, the codes being brought into your library may still be large, as the function may have its own dependencies. - -Note that `import { fill } from 'lodash'` or `import _ from 'lodash'` will not work and will embed the whole `lodash` library. - -Finally, if your client app also use `lodash` and you don't want `lodash` to be in both the client app and your libraries, even after cherry-picking, you may consider cherry-picking in component library and re-export them as utils for client to consume, so that the client does not need to depend on `lodash`, therefore avoiding duplication. - -### Type generation - -In [tsconfig.json](tsconfig.json), the following options instructs `tsc` to emit declaration (`.d.ts` files) only, as `vite build` handles the `.js` file generation. The generated `.d.ts` files are sent to `dist/types` folder. - -```json -"compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "declarationDir": "./dist/types" -} -``` - -In [package.json](package.json), the line below locates the generated types for library client. - -```json -"types": "./dist/types/index.d.ts", -``` - -> In [vite.config.ts](vite.config.ts), `build.emptyOutDir` is set to `false` and `rimraf` is used instead to remove the `dist` folder before the build. This is to avoid the `dist/types` folder generated by `tsc` being deleted when running `vite build`. - -### Configuration - -#### TypeScript - -In [tsconfig.json](tsconfig.js), set the following as recommended by Vite (since esbuild is used). However, enableing this option leads to https://github.com/vitejs/vite/issues/5814. The workaround is to also enable `compilerOptions.skipLibCheck`. - -```json -"compilerOptions": { - "isolatedModules": true -} -``` - -In [tsconfig.json](tsconfig.js), set the following to address [Issue #32](https://github.com/wuruoyun/vue-component-lib-starter/issues/32). The solution is from https://github.com/johnsoncodehk/volar/discussions/592. - -```json -"compilerOptions": { - "types": [ - "vite/client" - ] -} -``` - -## v4 Breaking Changes (from v3) - -### 1. Vuetify 4 CSS Reset Removed - -Vuetify 4 removed the aggressive global CSS reset that v3 included (universal `margin: 0; padding: 0`, list/heading/input resets, etc.). If your app relied on these defaults, opt in to the supplemental reset: - -```ts -import "@effect-app/vue-components/reset.css" -``` - -### 2. Nested union `_tag` handling - -`S.TaggedStruct` produces a bare `Literal` AST node, while legacy `S.Struct({ _tag: S.Literal("X") })` produces `Union([Literal("X")])` after `AST.toType`. Both patterns now correctly produce `"select"` metadata thanks to the `unwrapSingleLiteralUnion` helper. A `console.warn` is emitted for the legacy pattern to encourage migration to `TaggedStruct`. - -### 3. `UndefinedOr` defaults include the key with explicit `undefined` - -- **v3**: `defaultsValueFromSchema` skipped keys where the recursive call returned `undefined`, so `UndefinedOr` fields were omitted from the result object entirely. -- **v4**: The key is present in the result with an explicit `undefined` value. - -### 4. `S.optionalKey(X).pipe(S.withDecodingDefault(...))` support - -- **v3**: Not supported. The v3 equivalent `S.optionalWith` encoded defaults inside a `PropertySignatureTransformation`, opaque to AST inspection. -- **v4**: `defaultsValueFromSchema` detects `PropertySignatureTransformation` and extracts defaults, enabling `withDecodingDefault` as a new pattern for declaring field defaults directly on the schema. - -### On Submit event - -The :on-submit event could be tricky in `` component. -This is a prop that is basically a map of Tanstack Form `onSubmit` option and accept a function that return a Promise. If you want to use it as an event, you have to manage the state of loading yourself with `@submit` with a function returning `void` - -E.g. - -to use it as a Promise based Tanstack way: - -```html - -``` - -to use it as an event: - -```html - -``` - -this will give you a type error instead, because `:is-loading` is only accepted in event mode - -```html - -``` diff --git a/packages/vue-components/__tests__/FixedNuxtErrorBoundary.test.ts b/packages/vue-components/__tests__/FixedNuxtErrorBoundary.test.ts deleted file mode 100644 index 2e88b1f55..000000000 --- a/packages/vue-components/__tests__/FixedNuxtErrorBoundary.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { mount } from "@vue/test-utils" -import { CauseException } from "effect-app/client" -import * as Cause from "effect/Cause" -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" -import { defineComponent, h, nextTick, onErrorCaptured, ref } from "vue" -import { createMemoryHistory, createRouter, type Router } from "vue-router" -import FixedNuxtErrorBoundary from "../src/components/FixedNuxtErrorBoundary.vue" - -/** - * Behavior contract of the shared error boundary (extracted from the two - * app-local copies — this pins the "no behavior change" claim of the dedup): - * - a supported error (e.g. thrown in setup) renders the error slot and - * emits "error"; it is NOT reported via captureException - * - an unsupported error (e.g. native event handler) is reported via - * captureException (+ toast when debug) and does NOT take over the page - * - an interrupts-only Effect CauseException is ignored entirely - * - a route change clears the error state - * - scheduleAfterReady defers handling (backend hydration path) - * - enabled=false registers no handler, so errors propagate to the parent - */ - -const noopWarn = () => {} - -// Throws in setup only while `active` is true — a supported error ("setup -// function"), and togglable so the route-change test can stop re-throwing. -const makeThrowingChild = (active: { value: boolean }, err: unknown) => - defineComponent({ - name: "ThrowingChild", - setup() { - if (active.value) throw err - return () => h("div", { id: "child-ok" }, "ok") - } - }) - -// Throws from a native click handler — an unsupported error info. -const ClickThrowChild = defineComponent({ - name: "ClickThrowChild", - setup() { - return () => - h("button", { - id: "boom", - onClick: () => { - throw new Error("click boom") - } - }, "boom") - } -}) - -const makeRouter = () => - createRouter({ - history: createMemoryHistory(), - routes: [ - { path: "/", component: { render: () => null } }, - { path: "/other", component: { render: () => null } } - ] - }) - -describe("FixedNuxtErrorBoundary", () => { - let captureException: ReturnType - let toastError: ReturnType - let router: Router - let warnSpy: ReturnType - - beforeEach(() => { - captureException = vi.fn() - toastError = vi.fn() - router = makeRouter() - warnSpy = vi.spyOn(console, "warn").mockImplementation(noopWarn) - }) - - afterEach(() => { - warnSpy.mockRestore() - }) - - // Mount through a host component that renders the boundary with its slots, - // mirroring real usage (Suspender wraps it). Mounting the boundary directly - // as the test root breaks error propagation into its own onErrorCaptured. - const mountBoundary = ( - child: ReturnType, - props: Record = {} - ) => { - const emitted: Error[] = [] - const Host = defineComponent({ - name: "Host", - setup() { - return () => - h(FixedNuxtErrorBoundary, { - captureException, - toastError, - debug: true, - onError: (e: Error) => emitted.push(e), - ...props - }, { - default: () => h(child), - error: (params: { error: Error; clearError: () => void }) => - h("div", { id: "error-slot" }, params.error.message) - }) - } - }) - const wrapper = mount(Host, { global: { plugins: [router] } }) - return { wrapper, emitted } - } - - it("renders the error slot and emits on a supported (setup) error, without reporting it", async () => { - const active = ref(true) - const { emitted, wrapper } = mountBoundary(makeThrowingChild(active, new Error("setup boom"))) - await nextTick() - - expect(wrapper.find("#error-slot").exists()).toBe(true) - expect(wrapper.find("#error-slot").text()).toBe("setup boom") - expect(emitted).toHaveLength(1) - expect(captureException).not.toHaveBeenCalled() - expect(warnSpy).toHaveBeenCalled() - }) - - it("reports an unsupported (native handler) error via captureException + toast, without taking over the page", async () => { - const { emitted, wrapper } = mountBoundary(ClickThrowChild) - await wrapper.find("#boom").trigger("click") - await nextTick() - - expect(captureException).toHaveBeenCalledTimes(1) - expect(captureException.mock.calls[0][0]).toBeInstanceOf(Error) - expect(toastError).toHaveBeenCalledWith("An unexpected error has occurred: Error: click boom") - expect(wrapper.find("#error-slot").exists()).toBe(false) - expect(emitted).toHaveLength(0) - }) - - it("does not toast an unsupported error when debug is off", async () => { - const { wrapper } = mountBoundary(ClickThrowChild, { debug: false }) - await wrapper.find("#boom").trigger("click") - - expect(captureException).toHaveBeenCalledTimes(1) - expect(toastError).not.toHaveBeenCalled() - }) - - it("ignores an interrupts-only Effect CauseException", async () => { - const interrupted = new CauseException(Cause.interrupt(), "Interrupted") - const { emitted, wrapper } = mountBoundary(makeThrowingChild(ref(true), interrupted)) - await nextTick() - - expect(wrapper.find("#error-slot").exists()).toBe(false) - expect(emitted).toHaveLength(0) - expect(captureException).not.toHaveBeenCalled() - }) - - it("clears the error on route change", async () => { - const active = ref(true) - const { wrapper } = mountBoundary(makeThrowingChild(active, new Error("boom"))) - await nextTick() - expect(wrapper.find("#error-slot").exists()).toBe(true) - - active.value = false - await router.push("/other") - await nextTick() - - expect(wrapper.find("#error-slot").exists()).toBe(false) - expect(wrapper.find("#child-ok").exists()).toBe(true) - }) - - it("defers handling through scheduleAfterReady when provided", async () => { - const queue: (() => void)[] = [] - const { wrapper } = mountBoundary(makeThrowingChild(ref(true), new Error("deferred boom")), { - scheduleAfterReady: (fn: () => void) => queue.push(fn) - }) - await nextTick() - - expect(wrapper.find("#error-slot").exists()).toBe(false) - expect(queue).toHaveLength(1) - - queue.forEach((fn) => fn()) - await nextTick() - expect(wrapper.find("#error-slot").text()).toBe("deferred boom") - }) - - it("calls onHandled after emitting, before rendering the error slot", async () => { - const onHandled = vi.fn() - const { wrapper } = mountBoundary(makeThrowingChild(ref(true), new Error("hooked boom")), { onHandled }) - await nextTick() - - expect(onHandled).toHaveBeenCalledTimes(1) - expect(onHandled.mock.calls[0][0]).toBeInstanceOf(Error) - expect(onHandled.mock.calls[0][2]).toBe("setup function") - expect(wrapper.find("#error-slot").exists()).toBe(true) - }) - - it("registers no handler when enabled is false, letting the error propagate", async () => { - const parentCaught = vi.fn() - const Host = defineComponent({ - name: "Host", - setup() { - onErrorCaptured((err) => { - parentCaught(err) - return false - }) - return () => - h(FixedNuxtErrorBoundary, { - captureException, - toastError, - debug: true, - enabled: false - }, { - default: () => h(makeThrowingChild(ref(true), new Error("escaped boom"))), - error: () => h("div", { id: "error-slot" }) - }) - } - }) - const wrapper = mount(Host, { global: { plugins: [router] } }) - await nextTick() - - expect(parentCaught).toHaveBeenCalledTimes(1) - expect(wrapper.find("#error-slot").exists()).toBe(false) - }) -}) diff --git a/packages/vue-components/__tests__/IntegerValidation.test.ts b/packages/vue-components/__tests__/IntegerValidation.test.ts deleted file mode 100644 index 8ed3d9935..000000000 --- a/packages/vue-components/__tests__/IntegerValidation.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema, type NumberFieldMeta, toLocalizedStandardSchemaV1 } from "../src/components/OmegaForm" - -const germanTranslations: Record = { - "validation.integer.expected": "Es wird eine ganze Zahl erwartet, tatsächlich: {actualValue}", - "validation.number.expected": "Es wird eine Zahl erwartet, tatsächlich: {actualValue}", - "validation.empty": "Das Feld darf nicht leer sein" -} - -const germanTrans = (id: string, values?: Record) => { - let text = germanTranslations[id] || id - if (values) { - for (const [k, v] of Object.entries(values)) text = text.replace(`{${k}}`, String(v)) - } - return text -} - -describe("Integer validation with German translations (form-level localized schema)", () => { - it("should generate number metadata with int refinement for S.Int fields", () => { - const TestSchema = S.Struct({ value: S.Int }) - const { meta } = generateMetaFromSchema(TestSchema) - expect(meta.value?.type).toBe("number") - expect((meta.value as NumberFieldMeta).refinement).toBe("int") - }) - - it("shows German error for decimal value at form-level validation", async () => { - const standardSchema = toLocalizedStandardSchemaV1(S.Struct({ value: S.Int }) as any, germanTrans) - const result = await standardSchema["~standard"].validate({ value: 59.5 }) - expect(result.issues).toBeDefined() - const issue = result.issues?.find((i: any) => i.path?.[0] === "value") - expect(issue?.message).toContain("ganze Zahl") - }) - - it("shows German empty message for undefined required int at form-level validation", async () => { - const standardSchema = toLocalizedStandardSchemaV1(S.Struct({ value: S.Int }) as any, germanTrans) - const result = await standardSchema["~standard"].validate({ value: undefined }) - expect(result.issues).toBeDefined() - const issue = result.issues?.find((i: any) => i.path?.[0] === "value") - expect(issue?.message).toBe("Das Feld darf nicht leer sein") - }) - - it("accepts valid integer at form-level validation", async () => { - const standardSchema = toLocalizedStandardSchemaV1(S.Struct({ value: S.Int }) as any, germanTrans) - const result = await standardSchema["~standard"].validate({ value: 59 }) - expect(result.issues).toBeUndefined() - expect("value" in result && (result as any).value).toEqual({ value: 59 }) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm.test.ts b/packages/vue-components/__tests__/OmegaForm.test.ts deleted file mode 100644 index cc5925957..000000000 --- a/packages/vue-components/__tests__/OmegaForm.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { NonEmptyString255 } from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema, type MetaRecord } from "../src/components/OmegaForm" -import OmegaForm from "./OmegaFormTest.vue" -import OmegaIntlProvider from "./OmegaIntlProvider.vue" - -class kkk extends S.Class("kkk")({ - lll: S.String.pipe(S.check(S.isMinLength(10)), S.check(S.isMaxLength(21))), - mmm: S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 }))) -}) {} - -class bbb extends S.Class("bbb")({ - ccc: S.String.pipe(S.check(S.isMinLength(1)), S.check(S.isMaxLength(2))), - ddd: S.NullOr(S.String.pipe(S.check(S.isMaxLength(3)))), - jjj: S.UndefinedOr(S.String.pipe(S.check(S.isMaxLength(4)))), - nnn: S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 }))), - kkk, - ooo: S.String.pipe(S.check(S.isMinLength(1)), S.check(S.isMaxLength(23))) -}) {} - -describe("OmegaForm", () => { - describe("addRecursiveAnnotations", () => { - it("should properly extract metadata from complex schema", () => { - const testSchema = S.Struct({ - aaa: NonEmptyString255, - bbb: S.NullOr(bbb), - eee: S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 }))), - fff: S.Union([S.Literal("left"), S.Literal("right"), S.Literal("both")]), - zzz: S.NullOr(S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 })))).pipe( - S.annotate({ - message: () => "foobar" - }) - ) - }) - - const expectedMeta = { - aaa: { - maxLength: 255, - minLength: 1, - nullableOrUndefined: false, - required: true, - type: "string" - }, - "bbb.ccc": { - maxLength: 2, - minLength: 1, - nullableOrUndefined: false, - required: true, - type: "string" - }, - "bbb.ddd": { - maxLength: 3, - nullableOrUndefined: "null", - required: false, - type: "string" - }, - "bbb.jjj": { - maxLength: 4, - nullableOrUndefined: "undefined", - required: false, - type: "string" - }, - "bbb.kkk.lll": { - maxLength: 21, - minLength: 10, - nullableOrUndefined: false, - required: true, - type: "string" - }, - "bbb.kkk.mmm": { - maximum: 20, - minimum: 10, - nullableOrUndefined: false, - required: true, - type: "number" - }, - "bbb.nnn": { - maximum: 20, - minimum: 10, - nullableOrUndefined: false, - required: true, - type: "number" - }, - "bbb.ooo": { - maxLength: 23, - minLength: 1, - nullableOrUndefined: false, - required: true, - type: "string" - }, - eee: { - maximum: 20, - minimum: 10, - nullableOrUndefined: false, - required: true, - type: "number" - }, - fff: { - members: ["left", "right", "both"], - - nullableOrUndefined: false, - required: true, - type: "select" - }, - zzz: { - maximum: 20, - minimum: 10, - nullableOrUndefined: "null", - required: false, - type: "number" - } - } - - const result = generateMetaFromSchema(testSchema) - - expect(result).toHaveProperty("schema") - expect(result).toHaveProperty("meta") - - expect(result.schema).toBe(testSchema) - - expect(result.meta).toEqual(expectedMeta) - }) - }) - - describe("generateMetaFromSchema", () => { - it("should generate meta from schema", () => { - const testSchema = S.Struct({ - aaa: S.String, - bbb: S.NullOr( - S.Struct({ - ccc: S.String.pipe(S.check(S.isMinLength(1)), S.check(S.isMaxLength(2))), - ddd: S.NullOr(S.String.pipe(S.check(S.isMaxLength(3)))), - jjj: S.UndefinedOr(S.String.pipe(S.check(S.isMaxLength(4)))), - nnn: S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 }))), - kkk: S.Struct({ - lll: S.String.pipe(S.check(S.isMinLength(10)), S.check(S.isMaxLength(21))), - mmm: S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 }))) - }), - ooo: S.String.pipe(S.check(S.isMinLength(1)), S.check(S.isMaxLength(23))) - }) - ), - eee: S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 }))), - fff: S.Union([S.Literal("left"), S.Literal("right"), S.Literal("both")]), - zzz: S.NullOr(S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 })))) - }) - - const result = generateMetaFromSchema(testSchema) - - // Type check: ensure the meta record has the correct keys - type TestType = typeof testSchema extends S.Codec ? T : never - const meta: MetaRecord = result.meta - - // Value check - expect(meta["aaa"]).toBeDefined() - expect(meta["bbb.ccc"]).toBeDefined() - expect(meta["bbb.kkk.lll"]).toBeDefined() - expect(meta["eee"]).toBeDefined() - expect(meta["fff"]).toBeDefined() - expect(result.schema).toBe(testSchema) - }) - }) -}) - -// Create a wrapper component that includes the OmegaIntlProvider -const OmegaFormWithProvider = { - components: { OmegaForm, OmegaIntlProvider }, - template: ` - - - - ` -} - -describe("OmegaForm UI", () => { - it("renders", () => { - const wrapper = mount(OmegaFormWithProvider) - expect(wrapper.exists()).toBe(true) - }) - - it("should display the sum of first and second inputs", async () => { - const wrapper = mount(OmegaFormWithProvider) - - // Find the input elements - const firstInput = wrapper.find("input[id=\"first\"]") - const secondInput = wrapper.find("input[id=\"second\"]") - - console.log(firstInput) - - // Set values - await firstInput.setValue(10) - await secondInput.setValue(20) - - // Trigger change events - await firstInput.trigger("change") - await secondInput.trigger("change") - - // Get the element showing the sum - const sumElement = wrapper.find("[data-testid=\"valuez\"]") - - // Assert the sum is correct (10 + 20 = 30) - expect(sumElement.text()).toBe("30") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/CustomFilterFieldSchema.test.ts b/packages/vue-components/__tests__/OmegaForm/CustomFilterFieldSchema.test.ts deleted file mode 100644 index c893d391e..000000000 --- a/packages/vue-components/__tests__/OmegaForm/CustomFilterFieldSchema.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { toLocalizedStandardSchemaV1 } from "../../src/components/OmegaForm" - -const identityTrans = (id: string, values?: Record) => { - if (!values) return id - return Object.entries(values).reduce( - (acc, [k, v]) => acc.replaceAll(`{${k}}`, String(v)), - id - ) -} - -const maxLineLengthCheck = (max: number) => - S.makeFilter((value: string) => { - const tooLong = value.split("\n").find((line) => line.length > max) - return tooLong !== undefined ? `Line "${tooLong}" exceeds ${max} chars` : undefined - }) - -describe("OmegaForm form-level schema custom checks", () => { - it("includes custom filter checks in the form-level localized schema", async () => { - const schema = S.Struct({ - height: S.NonEmptyString100.pipe( - S.check(S.isMinLength(10)), - S.check(maxLineLengthCheck(20)) - ) - }) - - const standardSchema = toLocalizedStandardSchemaV1(schema as any, identityTrans) - const invalid = "1234567890\n123456789012345678901" - const result = await standardSchema["~standard"].validate({ height: invalid }) - - expect(result.issues).toBeDefined() - const issue = result.issues?.find((i: any) => i.path?.[0] === "height") - expect(issue).toBeDefined() - expect(issue?.message).toContain("exceeds 20 chars") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/DateValidation.test.ts b/packages/vue-components/__tests__/OmegaForm/DateValidation.test.ts deleted file mode 100644 index 9d794a84d..000000000 --- a/packages/vue-components/__tests__/OmegaForm/DateValidation.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it, vi } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -describe("Date field validation", () => { - it("accepts a raw YYYY-MM-DD string and decodes it to Date on submit", async () => { - let submittedValue: { date: Date } | null = null - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - date: S.DateFromString - }), - { - onSubmit: async ({ value }) => { - submittedValue = value - } - } - ) - - return { form } - } - }) - - await wrapper.find("[data-testid='date-input']").setValue("2024-06-01") - await wrapper.find("[data-testid='submit']").trigger("click") - - await vi.waitFor(() => { - expect(submittedValue).not.toBeNull() - }) - - expect(submittedValue?.date).toBeInstanceOf(Date) - expect(submittedValue?.date.toISOString()).toContain("2024-06-01") - expect(wrapper.find("[data-testid='errors']").text()).toBe("[]") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/DecimalSeparatorInput.test.ts b/packages/vue-components/__tests__/OmegaForm/DecimalSeparatorInput.test.ts deleted file mode 100644 index 9b2d7045c..000000000 --- a/packages/vue-components/__tests__/OmegaForm/DecimalSeparatorInput.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { describe, expect, it } from "vitest" -import { handleDecimalSeparatorBeforeinput } from "../../src/components/OmegaForm/decimalSeparatorInput" - -// Simulates typing into a VNumberInput: the handler is attached in capture -// phase on the component root, the native input is the event target. -const setup = ( - value: string, - separator: string, - cursor?: { start: number; end: number } -) => { - const root = document.createElement("div") - const input = document.createElement("input") - root.appendChild(input) - document.body.appendChild(root) - input.value = value - const start = cursor?.start ?? value.length - const end = cursor?.end ?? value.length - input.setSelectionRange(start, end) - - let vuetifySawBeforeinput = false - let inputEventFired = false - input.addEventListener("beforeinput", () => { - vuetifySawBeforeinput = true - }) - input.addEventListener("input", () => { - inputEventFired = true - }) - root.addEventListener( - "beforeinput", - (e) => handleDecimalSeparatorBeforeinput(e, separator), - { capture: true } - ) - - const type = (data: string) => { - const e = new InputEvent("beforeinput", { - data, - inputType: "insertText", - bubbles: true, - cancelable: true - }) - input.dispatchEvent(e) - return e - } - - return { - input, - type, - sawVuetifyHandler: () => vuetifySawBeforeinput, - sawInputEvent: () => inputEventFired - } -} - -describe("handleDecimalSeparatorBeforeinput", () => { - it("translates '.' into ',' when the active separator is ','", () => { - const t = setup("1", ",") - const e = t.type(".") - expect(e.defaultPrevented).toBe(true) - expect(t.sawVuetifyHandler()).toBe(false) - expect(t.input.value).toBe("1,") - expect(t.sawInputEvent()).toBe(true) - }) - - it("translates ',' into '.' when the active separator is '.'", () => { - const t = setup("1", ".") - t.type(",") - expect(t.input.value).toBe("1.") - }) - - it("translates both '.' and ',' when the active separator is a custom one", () => { - const dot = setup("1", "٫") - dot.type(".") - expect(dot.input.value).toBe("1٫") - - const comma = setup("2", "٫") - comma.type(",") - expect(comma.input.value).toBe("2٫") - }) - - it("leaves the event alone when the typed char is the active separator", () => { - const t = setup("1", ",") - const e = t.type(",") - expect(e.defaultPrevented).toBe(false) - expect(t.sawVuetifyHandler()).toBe(true) - expect(t.input.value).toBe("1") - }) - - it("leaves plain digits alone", () => { - const t = setup("1", ",") - const e = t.type("5") - expect(e.defaultPrevented).toBe(false) - expect(t.sawVuetifyHandler()).toBe(true) - }) - - it("swallows the wrong separator when the value already has one", () => { - const t = setup("1,5", ",") - const e = t.type(".") - expect(e.defaultPrevented).toBe(true) - expect(t.input.value).toBe("1,5") - expect(t.sawInputEvent()).toBe(false) - }) - - it("inserts at the cursor position and replaces the selection", () => { - const t = setup("15", ",", { start: 1, end: 1 }) - t.type(".") - expect(t.input.value).toBe("1,5") - expect(t.input.selectionStart).toBe(2) - expect(t.input.selectionEnd).toBe(2) - }) - - it("normalizes the wrong separator inside pasted data", () => { - const t = setup("", ",") - t.type("1.5") - expect(t.input.value).toBe("1,5") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/DecodeTransformFallback.test.ts b/packages/vue-components/__tests__/OmegaForm/DecodeTransformFallback.test.ts deleted file mode 100644 index 3abd2812c..000000000 --- a/packages/vue-components/__tests__/OmegaForm/DecodeTransformFallback.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import * as SchemaGetter from "effect/SchemaGetter" -import { describe, expect, it } from "vitest" -import { createUseFormWithCustomInput, generateMetaFromSchema, type NumberFieldMeta, useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -const CustomNumberInput = { - template: ` -
- - {{ inputProps.errorMessages.join(', ') }} -
- `, - props: ["inputProps", "field"], - inheritAttrs: false -} - -describe("OmegaForm decodeTo fallback transform", () => { - const inputSchema = S.Struct({ amount: S.PositiveInt }) - - const transformedSchema = S.Struct({ amount: S.NonNegativeInt }).pipe( - S.decodeTo(inputSchema, { - decode: SchemaGetter.transform((input: { amount: number }) => input.amount === 0 ? { amount: 666 } : input), - encode: SchemaGetter.passthrough({ strict: false }) - }) - ) - - const createWrapper = (defaultValues: { amount: number }) => { - let submittedValue: any = null - - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(transformedSchema, { - defaultValues, - onSubmit: async ({ value }) => { - submittedValue = value - } - }) - return { form, getSubmittedValue: () => submittedValue } - } - }) - - return wrapper - } - - it("should accept 0 at the input boundary and pass 666 to onSubmit via decode", async () => { - const wrapper = createWrapper({ amount: 0 }) - await wrapper.vm.$nextTick() - - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - await wrapper.vm.$nextTick() - await new Promise((resolve) => setTimeout(resolve, 100)) - - const vm = wrapper.vm as any - expect(vm.getSubmittedValue()).toEqual({ amount: 666 }) - }) - - it("should pass through a positive amount unchanged", async () => { - const wrapper = createWrapper({ amount: 5 }) - await wrapper.vm.$nextTick() - - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - await wrapper.vm.$nextTick() - await new Promise((resolve) => setTimeout(resolve, 100)) - - const vm = wrapper.vm as any - expect(vm.getSubmittedValue()).toEqual({ amount: 5 }) - }) - - it("meta extraction should reflect the outer (source) schema, not the inner decoded one", () => { - const { meta } = generateMetaFromSchema(transformedSchema) - const amount = meta.amount as NumberFieldMeta | undefined - - expect(amount).toBeDefined() - expect(amount!.type).toBe("number") - expect(amount!.refinement).toBe("int") - // NonNegativeInt ⇒ minimum: 0 (not exclusiveMinimum: 0 from PositiveInt) - expect(amount!.minimum).toBe(0) - expect(amount!.exclusiveMinimum).toBeUndefined() - }) - - it("should allow 0 typed into form.Input and submit with 666 via decode", async () => { - const useForm = createUseFormWithCustomInput(CustomNumberInput) - let submittedValue: any = null - - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useForm(transformedSchema, { - defaultValues: { amount: 1 }, - onSubmit: async ({ value }) => { - submittedValue = value - } - }) - return { form, getSubmittedValue: () => submittedValue } - } - }, { - global: { stubs: { CustomNumberInput } } - }) - - await wrapper.vm.$nextTick() - - const input = wrapper.find("[data-testid=\"custom-input-field\"]") - await input.setValue("0") - await wrapper.vm.$nextTick() - - expect(wrapper.find("[data-testid=\"custom-input-error\"]").exists()).toBe(false) - - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - await wrapper.vm.$nextTick() - await new Promise((resolve) => setTimeout(resolve, 100)) - - const vm = wrapper.vm as any - expect(vm.getSubmittedValue()).toEqual({ amount: 666 }) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/DefaultValuesPriority.test.ts b/packages/vue-components/__tests__/OmegaForm/DefaultValuesPriority.test.ts deleted file mode 100644 index cce2d6ff4..000000000 --- a/packages/vue-components/__tests__/OmegaForm/DefaultValuesPriority.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -// __tests__/OmegaForm/DefaultValuesPriority.test.ts -import { describe, expect, it } from "vitest" -import { deepMerge } from "../../src/components/OmegaForm" - -describe("deepMerge", () => { - it("treats arrays as values, not as merged structures", () => { - expect(deepMerge({ xs: [1, 2, 3] }, { xs: [9] })).toEqual({ xs: [9] }) - }) - - it("recursively merges objects", () => { - expect(deepMerge({ a: { b: 1, c: 2 } }, { a: { c: 9, d: 3 } })) - .toEqual({ a: { b: 1, c: 9, d: 3 } }) - }) - - it("source wins for primitives", () => { - expect(deepMerge({ x: 1 }, { x: 2 })).toEqual({ x: 2 }) - }) -}) - -// NOTE: defaultValuesPriority resolution lives inside useOmegaForm's -// `defaultValues` computed and isn't exported. A behavioral test would mount -// a form. The deepMerge unit tests above pin the merge mechanic; the priority -// resolution itself is exercised indirectly by the Meta and Defaults stories. diff --git a/packages/vue-components/__tests__/OmegaForm/Defaults.test.ts b/packages/vue-components/__tests__/OmegaForm/Defaults.test.ts deleted file mode 100644 index 320d85dcd..000000000 --- a/packages/vue-components/__tests__/OmegaForm/Defaults.test.ts +++ /dev/null @@ -1,551 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as Effect from "effect-app/Effect" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -describe("OmegaForm Defaults", () => { - // Setup the same schemas as in Defaults.vue story - const struct = { - a: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))), - b: S.NonEmptyString, - c: S.NonEmptyArray(S.String).pipe( - S.withConstructorDefault(Effect.sync(() => [S.NonEmptyString("C"), S.NonEmptyString("Non Empty Array")])) - ), - d: S - .NonEmptyArray(S.Struct({ - e: S.NonEmptyString - })) - .pipe( - S.withConstructorDefault(Effect.sync(() => [{ e: S.NonEmptyString("default") }])) - ), - f: S.Union([ - S.Struct({ - _tag: S.Literal("taggo1").pipe(S.withConstructorDefault(Effect.succeed("taggo1"))), - g: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))), - i: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))) - }), - S.Struct({ - _tag: S.Literal("taggo2").pipe(S.withConstructorDefault(Effect.succeed("taggo2"))), - h: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))), - i: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))) - }) - ]), - j: S.Finite.pipe(S.withConstructorDefault(Effect.succeed(0))), - k: S.Boolean.pipe(S.withConstructorDefault(Effect.succeed(true))), - l: S.NullOr( - S.Union([ - S.Struct({ - a: S.NonEmptyString255, - common: S.NonEmptyString255, - _tag: S.Literal("A") - }), - S.Struct({ - b: S.NonEmptyString255, - common: S.NonEmptyString255, - _tag: S.Literal("B") - }) - ]) - ), - m: S.Struct({ - n: S.NullOr(S.Struct({ q: S.String })), - o: S.UndefinedOr(S.Struct({ q: S.String })) - }), - p: S.NullOr(S.Struct({ z: S.String })), - q: S.UndefinedOr(S.Struct({ z: S.String })), - r: S - .NullOr(S.Struct({ - p: S.NullOr(S.Struct({ z: S.String })), - r: S.UndefinedOr(S.Struct({ z: S.String })) - })) - .withConstructorDefault, - s: S.NullOr(S.Struct({ z: S.String })).withConstructorDefault, - t: S.FiniteFromString.pipe(S.withConstructorDefault(Effect.succeed(1000))), - u: S.NullOr(S.NonEmptyString), - v: S.UndefinedOr(S.NonEmptyString) - } - - class ClassSchema extends S.Class("ClassSchema")(struct) {} - const schema = S.Struct(struct) - - const Union = S.Union([ - S.Struct({ - _tag: S.Literal("tag1").pipe(S.withConstructorDefault(Effect.succeed("tag1"))), - a: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))), - b: schema - }), - S.Struct({ - _tag: S.Literal("tag2"), - a: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))), - b: S.NonEmptyString.pipe(S.withConstructorDefault(Effect.succeed(S.NonEmptyString("default")))), - c: schema - }) - ]) - - it("should have correct default values for form zero (ClassSchema)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(ClassSchema) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "default", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - }) - }) - - it("should have correct default values for form one (schema)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "default", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - }) - }) - - it("should have correct default values for form two (Union)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(Union) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "_tag": "tag1", - "a": "default", - "b": "default", - "c": { - "a": "default", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - } - }) - }) - - it("should not initialize _tag for a root union without explicit defaults", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(S.Union([ - S.Struct({ - _tag: S.Literal("one"), - a: S.Struct({ z: S.NonEmptyString100 }) - }), - S.Struct({ - _tag: S.Literal("two"), - a: S.Struct({ y: S.NonNegativeInt }) - }) - ])) - - return { form } - } - }) - - await wrapper.vm.$nextTick() - - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - expect(JSON.parse(valuesText)).not.toHaveProperty("_tag") - }) - - it("should have correct default values for form three (with defaultValues merged)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - defaultValues: { - a: "aaaaah" - } - }) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "aaaaah", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - }) - }) - - it("should have correct default values for form four (defaultFromSchema: only)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - defaultValues: { - a: "aaaaah" - } - }, { - defaultValuesPriority: ["schema"] - }) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "default", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - }) - }) - - it("should have correct default values for form five (defaultFromSchema: nope)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - defaultValues: { - a: "aaaaah" - } - }, { - defaultValuesPriority: ["tanstack"] - }) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "aaaaah" - }) - }) - - it("should have correct default values for form six (defaultFromSchema: merge)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - defaultValues: { - a: "aaaaah" - } - }, { - defaultValuesPriority: ["tanstack", "schema"] - }) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "aaaaah", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - }) - }) - - it("should have correct default values for form seven (Union with different structure)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(S.Union([ - S.Struct({ - _tag: S.Literal("tag1").pipe(S.withConstructorDefault(Effect.succeed("tag1"))), - a: S.NonEmptyString, - s: S.NullOr(S.Finite).withConstructorDefault - }), - S.Struct({ - _tag: S.Literal("tag2"), - b: S.NonEmptyString, - t: S.Finite - }) - ])) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "_tag": "tag1", - "a": "", - "s": null, - "b": "" - }) - }) - - it("should have correct default values for form eight (ClassSchema with filter)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(ClassSchema - .pipe( - S.check(S.makeFilter((form) => { - if (form.a !== form.b) { - return { - path: ["a"], - message: "Email and confirmation must match!" - } - } - })) - )) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "default", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - }) - }) - - it("should have correct default values for form nine (ClassSchema with filter inline)", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(ClassSchema.pipe(S.check(S.makeFilter((form) => { - if (form.a !== form.b) { - return { - path: ["a"], - message: "Email and confirmation must match!" - } - } - })))) - return { form } - } - }) - - await wrapper.vm.$nextTick() - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - const values = JSON.parse(valuesText) - - expect(values).toEqual({ - "a": "default", - "b": "", - "c": ["C", "Non Empty Array"], - "d": [{ "e": "default" }], - "f": { "_tag": "taggo1", "g": "default", "i": "default", "h": "default" }, - "j": 0, - "k": true, - "l": null, - "m": { "n": null }, - "p": null, - "r": null, - "s": null, - "t": 1000, - "u": null - }) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/Defaults.values.test.ts b/packages/vue-components/__tests__/OmegaForm/Defaults.values.test.ts deleted file mode 100644 index dec4f4981..000000000 --- a/packages/vue-components/__tests__/OmegaForm/Defaults.values.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -// __tests__/OmegaForm/Defaults.values.test.ts -import * as Effect from "effect-app/Effect" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defaultsValueFromSchema } from "../../src/components/OmegaForm" -import { fillNestedDefaults } from "../../src/components/OmegaForm/meta/defaults" - -describe("defaultsValueFromSchema", () => { - it("extracts withConstructorDefault values", () => { - const schema = S.Struct({ - name: S.String.pipe(S.withConstructorDefault(Effect.succeed("Bob"))) - }) - expect(defaultsValueFromSchema(schema)).toEqual({ name: "Bob" }) - }) - - it("extracts withDecodingDefault on optionalKey", () => { - const schema = S.Struct({ - flag: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("on"))) - }) - expect(defaultsValueFromSchema(schema)).toEqual({ flag: "on" }) - }) - - it("returns null for NullOr fields without explicit default", () => { - const schema = S.Struct({ - x: S.NullOr(S.String) - }) - expect(defaultsValueFromSchema(schema)).toEqual({ x: null }) - }) - - it("returns undefined for UndefinedOr fields without explicit default", () => { - const schema = S.Struct({ - x: S.UndefinedOr(S.Finite) - }) - expect(defaultsValueFromSchema(schema)).toEqual({ x: undefined }) - }) - - it("returns empty string for plain S.String at the leaf", () => { - const schema = S.Struct({ x: S.String }) - expect(defaultsValueFromSchema(schema)).toEqual({ x: "" }) - }) - - it("returns false for plain S.Boolean at the leaf", () => { - const schema = S.Struct({ x: S.Boolean }) - expect(defaultsValueFromSchema(schema)).toEqual({ x: false }) - }) - - it("preserves values passed via the record argument", () => { - const schema = S.Struct({ x: S.String, y: S.String }) - expect(defaultsValueFromSchema(schema, { x: "preset" })).toEqual({ x: "preset", y: "" }) - }) - - it("merges fields across union members, picking explicit defaults", () => { - const schema = S.Union([ - S.TaggedStruct("A", { v: S.String.pipe(S.withConstructorDefault(Effect.succeed("a-default"))) }), - S.TaggedStruct("B", { v: S.String, extra: S.String }) - ]) - expect(defaultsValueFromSchema(schema)).toEqual({ _tag: "A", v: "a-default", extra: "" }) - }) -}) - -describe("fillNestedDefaults", () => { - it("fills missing nullable children for a materialized nullable struct", () => { - const schema = S.Struct({ - override: S.NullOr(S.Struct({ - min: S.NullOr(S.NonNegativeNumber), - max: S.NullOr(S.NonNegativeNumber) - })) - }) - - expect(fillNestedDefaults(schema.ast, { override: { min: 100 } })).toEqual({ - override: { - min: 100, - max: null - } - }) - }) - - it("does not add fields from another tagged union branch", () => { - const schema = S.Union([ - S.TaggedStruct("A", { a: S.NullOr(S.String), common: S.String }), - S.TaggedStruct("B", { b: S.Number, nullableB: S.NullOr(S.Number) }) - ]) - - expect(fillNestedDefaults(schema.ast, { _tag: "B", b: 1 })).toEqual({ - _tag: "B", - b: 1 - }) - }) - - it("fills missing nullable children for materialized struct elements of an array", () => { - const schema = S.Struct({ - items: S.Array(S.NullOr(S.Struct({ - a: S.NullOr(S.String), - b: S.NullOr(S.String) - }))) - }) - - expect(fillNestedDefaults(schema.ast, { items: [{ a: "x" }, null] })).toEqual({ - items: [{ a: "x", b: null }, null] - }) - }) - - it("returns the same reference when there is nothing to fill", () => { - const schema = S.Struct({ - items: S.Array(S.NullOr(S.Struct({ - a: S.NullOr(S.String), - b: S.NullOr(S.String) - }))) - }) - - const value = { items: [{ a: "x", b: null }, null] } - expect(fillNestedDefaults(schema.ast, value)).toBe(value) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/DialogBlockingI18n.test.ts b/packages/vue-components/__tests__/OmegaForm/DialogBlockingI18n.test.ts deleted file mode 100644 index 234848dc5..000000000 --- a/packages/vue-components/__tests__/OmegaForm/DialogBlockingI18n.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { mount } from "@vue/test-utils" -import { describe, expect, it, vi } from "vitest" -import { defineComponent, h, ref } from "vue" -import { provideBus, usePreventClose } from "../../src/components/OmegaForm/blockDialog" -import { useIntlKey } from "../../src/utils" - -// Regression: when the dialog-closing handler ran with a dirty form, the -// previous fix destructured `formatMessage` from `useIntl()`. The storybook -// mocks shipped a shape without a top-level `formatMessage`, so the -// destructured value was `undefined` and triggered "formatMessage is not a -// function". This test pins the production wiring: as long as `useIntl()` -// exposes `formatMessage` directly (real shape from `@effect-app/vue`), -// the confirm message is the formatted string. -describe("usePreventClose i18n", () => { - it("calls window.confirm with the formatted message when dirty and dialog is closing", () => { - const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true) - const formatMessage = vi.fn( - (descriptor: { id: string; defaultMessage?: string }) => `de:${descriptor.id}` - ) - - let bus: ReturnType | undefined - const Outer = defineComponent({ - setup() { - bus = provideBus() - return () => h(Inner) - } - }) - const Inner = defineComponent({ - setup() { - usePreventClose(() => ref(true)) - return () => h("div") - } - }) - - mount(Outer, { - global: { - provide: { - [useIntlKey as unknown as symbol]: () => ({ - trans: (id: string) => id, - formatMessage - }) - } - } - }) - - bus!.emit("dialog-closing", {}) - - expect(formatMessage).toHaveBeenCalledTimes(1) - expect(formatMessage.mock.calls[0]?.[0]).toMatchObject({ id: "form.unsaved_changes_confirm" }) - expect(confirmSpy).toHaveBeenCalledWith("de:form.unsaved_changes_confirm") - - confirmSpy.mockRestore() - }) - - it("skips the confirm entirely when the form is not dirty", () => { - const confirmSpy = vi.spyOn(window, "confirm") - - let bus: ReturnType | undefined - const Outer = defineComponent({ - setup() { - bus = provideBus() - return () => h(Inner) - } - }) - const Inner = defineComponent({ - setup() { - usePreventClose(() => ref(false)) - return () => h("div") - } - }) - - mount(Outer) - - const evt: { prevent?: boolean | Promise } = {} - bus!.emit("dialog-closing", evt) - - expect(confirmSpy).not.toHaveBeenCalled() - expect(evt.prevent).toBeUndefined() - - confirmSpy.mockRestore() - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/EmailFormatDetection.test.ts b/packages/vue-components/__tests__/OmegaForm/EmailFormatDetection.test.ts deleted file mode 100644 index ff1a800bf..000000000 --- a/packages/vue-components/__tests__/OmegaForm/EmailFormatDetection.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema } from "../../src/components/OmegaForm" - -describe("S.Email format detection", () => { - it("flags S.Email fields with format: 'email' on their meta", () => { - const schema = S.Struct({ x: S.Email }) - const { meta } = generateMetaFromSchema(schema) - expect((meta.x as any)?.format).toBe("email") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/EmptyStringValidation.test.ts b/packages/vue-components/__tests__/OmegaForm/EmptyStringValidation.test.ts deleted file mode 100644 index be930ee75..000000000 --- a/packages/vue-components/__tests__/OmegaForm/EmptyStringValidation.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { flushPromises, mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, it } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -describe("Empty string validation for nested NonEmptyString fields", () => { - it("demonstrates current empty string behavior", async () => { - const schema = S.Struct({ - asder2: S.Struct({ - value: S.NonEmptyString100 - }) - }) - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - defaultValues: { - asder2: { - value: "" - } - }, - onSubmit: async ({ value }) => { - console.log("Form submitted with:", value) - } - }) - return { form } - } - }) - - await wrapper.vm.$nextTick() - - // Click submit with empty string - await wrapper.find("[data-testid='submit']").trigger("click") - await wrapper.vm.$nextTick() - await flushPromises() - - // Check for validation error - expect(wrapper.text()).toContain("validation.empty") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/GeneralErrors.test.ts b/packages/vue-components/__tests__/OmegaForm/GeneralErrors.test.ts deleted file mode 100644 index 33a03f7ea..000000000 --- a/packages/vue-components/__tests__/OmegaForm/GeneralErrors.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defineComponent, h, nextTick } from "vue" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaErrorsInternal from "../../src/components/OmegaForm/OmegaErrorsInternal.vue" - -describe("OmegaForm general errors", () => { - it("renders a string returned by an onSubmit validator and clears it after correction", async () => { - const message = "Passwörter stimmen nicht überein" - const Inner = defineComponent({ - setup() { - const form = useOmegaForm(S.Struct({ password: S.String, confirmation: S.String }), { - defaultValues: { password: "secret", confirmation: "different" }, - validators: { - onSubmit: ({ value }) => value.password === value.confirmation ? undefined : message - } - }) - return { form } - }, - template: `` - }) - const wrapper = mount(Inner) - - await wrapper.vm.form.handleSubmit() - await nextTick() - expect(wrapper.get("[role=\"alert\"]").text()).toContain(message) - - wrapper.vm.form.setFieldValue("confirmation", "secret") - await wrapper.vm.form.handleSubmit() - await nextTick() - expect(wrapper.find("[role=\"alert\"]").exists()).toBe(false) - wrapper.unmount() - }) - - it("preserves schema messages alongside strings and ignores unsupported values", () => { - const wrapper = mount(OmegaErrorsInternal, { - props: { - errors: [], - generalErrors: [ - "Form error", - { rows: [{ message: "Schema error" }, null, 42, { message: 123 }, { message: "" }] }, - null, - undefined, - false, - 42, - "", - { rows: "unsupported", other: null } - ] - }, - slots: { - default: ({ showedGeneralErrors }) => h("div", JSON.stringify(showedGeneralErrors)) - } - }) - - expect(wrapper.text()).toBe(JSON.stringify(["Form error", "Schema error"])) - wrapper.unmount() - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/IndexedInputErrors.test.ts b/packages/vue-components/__tests__/OmegaForm/IndexedInputErrors.test.ts deleted file mode 100644 index 5023651bd..000000000 --- a/packages/vue-components/__tests__/OmegaForm/IndexedInputErrors.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defineComponent, nextTick, ref } from "vue" -import { useOmegaForm } from "../../src/components/OmegaForm" - -describe("OmegaForm indexed inputs", () => { - it.each([0, 13])("renders rows[%i].text outside form.Array with one labelled error", async (index) => { - const wrapper = mount(defineComponent({ - setup() { - const form = useOmegaForm( - S.Struct({ - rows: S.Array(S.Struct({ text: S.String.pipe(S.check(S.isMinLength(2))) })) - }), - { - defaultValues: { - rows: Array.from({ length: index + 1 }, (_, i) => ({ text: i === index ? "" : "valid" })) - } - } - ) - return { form, name: `rows[${index}].text`, visible: ref(true) } - }, - template: ` - - - - - - - ` - })) - - const input = wrapper.get("input") - await wrapper.vm.form.handleSubmit() - await nextTick() - - expect(wrapper.get("[data-testid=\"field-errors\"]").text()).not.toBe("") - const entries = wrapper.findAll("[role=\"alert\"] .error-item") - expect(entries).toHaveLength(1) - expect(entries[0].get("label").text()).toBe("Row text") - expect(entries[0].get("label").attributes("for")).toBe(input.attributes("id")) - - wrapper.vm.visible = false - await nextTick() - await wrapper.vm.form.handleSubmit() - await nextTick() - - const unregistered = wrapper.findAll("[role=\"alert\"] .error-item") - expect(unregistered).toHaveLength(1) - expect(unregistered[0].get("label").attributes("for")).toBe(`rows.${index}.text`) - wrapper.unmount() - }) - - it("still resolves indexed metadata inside form.Array", () => { - const wrapper = mount(defineComponent({ - setup() { - return { - form: useOmegaForm(S.Struct({ rows: S.Array(S.Struct({ text: S.String })) }), { - defaultValues: { rows: [{ text: "existing" }] } - }) - } - }, - template: ` - - - - - - ` - })) - - expect(wrapper.get("input").element.value).toBe("existing") - wrapper.unmount() - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/InputAttributes.test.ts b/packages/vue-components/__tests__/OmegaForm/InputAttributes.test.ts deleted file mode 100644 index 6e8441959..000000000 --- a/packages/vue-components/__tests__/OmegaForm/InputAttributes.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it, vi } from "vitest" -import { defineComponent } from "vue" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -const VTextField = defineComponent({ - props: { - modelValue: { - type: String, - default: "" - } - }, - emits: ["update:modelValue"], - template: ` - - ` -}) - -const VSelect = defineComponent({ - props: { - items: { - type: Array, - default: () => [] - }, - modelValue: { - type: String, - default: "" - } - }, - emits: ["update:modelValue"], - template: ` - - ` -}) - -describe("OmegaForm input attributes", () => { - it("does not forward the internal form object as a native form attribute", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - password: S.NonEmptyString255 - }), - { - defaultValues: { - password: "" - } - } - ) - - return { form } - } - }, { - global: { - components: { VTextField, VSelect } - } - }) - - await wrapper.vm.$nextTick() - - const input = wrapper.find("input") - const nativeForm = wrapper.find("form").element - - expect(input.attributes("form")).toBeUndefined() - expect(input.attributes("validators")).toBeUndefined() - expect(Object.values(input.attributes())).not.toContain("[object Object]") - expect((input.element as HTMLInputElement).form).toBe(nativeForm) - }) - - it("renders a newly-supported native input type (tel) on the text-field branch", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ phone: S.String }), - { defaultValues: { phone: "" } } - ) - - return { form } - } - }, { - global: { - components: { VTextField } - } - }) - - await wrapper.vm.$nextTick() - - expect(wrapper.find("input").attributes("type")).toBe("tel") - }) - - it("falls back to a text input and warns for a type no branch handles", async () => { - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) - - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ mystery: S.String }), - { defaultValues: { mystery: "" } } - ) - - return { form } - } - }, { - global: { - components: { VTextField } - } - }) - - await wrapper.vm.$nextTick() - - // unhandled type → getInputType maps it to "text" - expect(wrapper.find("input").attributes("type")).toBe("text") - expect(warn).toHaveBeenCalledWith(expect.stringContaining("totally-custom")) - - warn.mockRestore() - }) - - it("keeps input configuration props off the renderer attrs", async () => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - choice: S.Literal("one") - }), - { - defaultValues: { - choice: "one" - } - } - ) - - return { form } - } - }, { - global: { - components: { VTextField, VSelect } - } - }) - - await wrapper.vm.$nextTick() - - const select = wrapper.find("select") - - expect(select.attributes("options")).toBeUndefined() - expect(select.attributes("items")).toBeUndefined() - expect(Object.values(select.attributes())).not.toContain("[object Object]") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/InputReactivity.test.ts b/packages/vue-components/__tests__/OmegaForm/InputReactivity.test.ts deleted file mode 100644 index 37b2b4996..000000000 --- a/packages/vue-components/__tests__/OmegaForm/InputReactivity.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -describe("OmegaForm input reactivity", () => { - it("reflects value changes from the slot `state` in the view", async () => { - const schema = S.Struct({ - name: S.String - }) - - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - defaultValues: { name: "" } - }) - return { form } - } - }) - - await wrapper.vm.$nextTick() - - const input = wrapper.find("[data-testid='input']") - await input.setValue("hello") - await wrapper.vm.$nextTick() - - // The slot `state` must stay in sync with the form store - expect(wrapper.find("[data-testid='display']").text()).toBe("hello") - expect((input.element as HTMLInputElement).value).toBe("hello") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/InputRegistry.test.ts b/packages/vue-components/__tests__/OmegaForm/InputRegistry.test.ts deleted file mode 100644 index 822fd6555..000000000 --- a/packages/vue-components/__tests__/OmegaForm/InputRegistry.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defineComponent } from "vue" -import { createUseFormWithCustomInput, useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -const VTextField = defineComponent({ - props: { modelValue: { type: String, default: "" } }, - emits: ["update:modelValue"], - template: `` -}) - -const RatingInput = defineComponent({ - props: { - inputProps: { type: Object, required: true }, - field: { type: Object, required: true }, - state: { type: Object, required: true } - }, - template: `
{{ inputProps.label }}
` -}) - -const UniversalInput = defineComponent({ - props: { - inputProps: { type: Object, required: true }, - field: { type: Object, required: true }, - state: { type: Object, required: true } - }, - template: `
{{ inputProps.label }}
` -}) - -const mountWith = (omegaConfig: any, type: string, name = "x") => - mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - `, - setup() { - const form = useOmegaForm(S.Struct({ x: S.String }), { defaultValues: { x: "" } }, omegaConfig) - return { form } - } - }, { global: { components: { VTextField } } }) - -describe("OmegaForm input registry", () => { - it("renders the registered component for a custom type", async () => { - const wrapper = mountWith({ inputs: { rating: RatingInput } }, "rating") - await wrapper.vm.$nextTick() - expect(wrapper.find("[data-testid=\"rating\"]").exists()).toBe(true) - }) - - it("overrides a built-in type when registered", async () => { - const wrapper = mountWith({ inputs: { string: RatingInput } }, "string") - await wrapper.vm.$nextTick() - expect(wrapper.find("[data-testid=\"rating\"]").exists()).toBe(true) - }) - - it("falls back to the built-in renderer when no entry matches", async () => { - const wrapper = mountWith({ inputs: { rating: RatingInput } }, "string") - await wrapper.vm.$nextTick() - expect(wrapper.find("[data-testid=\"rating\"]").exists()).toBe(false) - expect(wrapper.find("input").exists()).toBe(true) - }) -}) - -describe("createUseFormWithCustomInput + inputs", () => { - it("uses a per-type registered component, else the universal custom input", async () => { - const useForm = createUseFormWithCustomInput(UniversalInput) - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useForm( - S.Struct({ score: S.Number, nickname: S.String }), - { defaultValues: { score: 0, nickname: "" } }, - { inputs: { rating: RatingInput } } - ) - return { form } - } - }, { global: { components: { VTextField } } }) - await wrapper.vm.$nextTick() - // `score` (type="rating") resolves the registered component... - expect(wrapper.find("[data-testid=\"rating\"]").exists()).toBe(true) - // ...while `nickname` falls back to the universal custom input. - expect(wrapper.find("[data-testid=\"universal\"]").exists()).toBe(true) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/IntersectionError.test.ts b/packages/vue-components/__tests__/OmegaForm/IntersectionError.test.ts deleted file mode 100644 index 00e3ba9a5..000000000 --- a/packages/vue-components/__tests__/OmegaForm/IntersectionError.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { flushPromises, mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { beforeEach, describe, expect, it } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -// Mock components that integrate with TanStack Form -const mockComponents = { - CustomInput: { - template: ` -
- - - -
- `, - props: ["form", "name", "field", "label", "type", "options"], - inheritAttrs: false - }, - OmegaErrors: { - template: `
{{ JSON.stringify(errors) }}
`, - props: ["errors", "generalErrors", "showErrors"] - } -} - -describe("OmegaForm Intersection/Union", () => { - const AlphaSchema = S.Struct({ - first: S.Literal("alpha"), - alpha: S.NonEmptyString - }) - - const BetaSchema = S.Struct({ - first: S.Literal("beta"), - beta: S.NonEmptyString - }) - - const MySchema = S.Struct({ - myUnion: S.Union([AlphaSchema, BetaSchema]) - }) - - const createWrapper = () => - mount({ - components: { - OmegaIntlProvider, - CustomInput: mockComponents.CustomInput - }, - template: ` - - - - - - `, - setup() { - let submittedValue: any = null - const form = useOmegaForm(MySchema, { - onSubmit: async ({ value }) => { - submittedValue = value - } - }) - return { form, getSubmittedValue: () => submittedValue } - } - }, { - global: { - stubs: { - CustomInput: mockComponents.CustomInput, - OmegaErrorsInternal: mockComponents.OmegaErrors - } - } - }) - - let wrapper: ReturnType - - beforeEach(() => { - wrapper = createWrapper() - }) - - it("handles discriminated union with conditional fields", async () => { - // Initially, neither alpha nor beta input should be visible - expect(wrapper.find("[data-testid='alpha-input']").exists()).toBe(false) - expect(wrapper.find("[data-testid='beta-input']").exists()).toBe(false) - - // Select "alpha" from the dropdown - const selectInput = wrapper.find("select[name='myUnion.first']") - await selectInput.setValue("alpha") - await wrapper.vm.$nextTick() - - // Alpha input should now be visible, beta should not - expect(wrapper.find("[data-testid='alpha-input']").exists()).toBe(true) - expect(wrapper.find("[data-testid='beta-input']").exists()).toBe(false) - - // Switch to "beta" - await selectInput.setValue("beta") - await wrapper.vm.$nextTick() - - // Beta input should now be visible, alpha should not - expect(wrapper.find("[data-testid='alpha-input']").exists()).toBe(false) - expect(wrapper.find("[data-testid='beta-input']").exists()).toBe(true) - }) - - it("clears validation errors when switching union types", async () => { - const selectInput = wrapper.find("select[name='myUnion.first']") - - // Select "alpha" and submit without filling required field to trigger error - await selectInput.setValue("alpha") - - const submitButton = wrapper.find("[data-testid='submit']") - await submitButton.trigger("submit") - await wrapper.vm.$nextTick() - await flushPromises() - - // Verify error exists for alpha field - const errorDiv = wrapper.find(".mock-omega-errors") - expect(errorDiv.exists()).toBe(true) - expect(errorDiv.text()).toContain("alpha") - - // Switch to "beta" - error should be cleared - await selectInput.setValue("beta") - await wrapper.vm.$nextTick() - - const errorDivAfter = wrapper.find(".mock-omega-errors") - expect(errorDivAfter.text()).not.toContain("alpha") - }) - - it("clears form values when switching union types and handles submission", async () => { - const selectInput = wrapper.find("select[name='myUnion.first']") - - // Select "alpha" and fill the input - await selectInput.setValue("alpha") - await wrapper.vm.$nextTick() - - const alphaInput = wrapper.find("input[name='myUnion.alpha']") - await alphaInput.setValue("test") - await wrapper.vm.$nextTick() - - // Check that "test" is in the values - const valuesText = wrapper.find("[data-testid='values']").text() - expect(valuesText).toContain("test") - - // Switch to "beta" - await selectInput.setValue("beta") - await wrapper.vm.$nextTick() - - // // Check that alpha "test" value is not there anymore - // const valuesAfterSwitch = wrapper.find("[data-testid='values']").text() - // expect(valuesAfterSwitch).not.toContain("test") - - // Fill beta with "here we go" - const betaInput = wrapper.find("input[name='myUnion.beta']") - await betaInput.setValue("here we go") - await wrapper.vm.$nextTick() - - // Click submit and check submission works - const form = wrapper.find("form") - await form.trigger("submit") - await wrapper.vm.$nextTick() - // Wait a bit for async operations - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Verify the form data was submitted with the correct beta values - const vm = wrapper.vm as any - expect(vm.getSubmittedValue()).toEqual({ - myUnion: { - first: "beta", - beta: "here we go" - } - }) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/Meta.test.ts b/packages/vue-components/__tests__/OmegaForm/Meta.test.ts deleted file mode 100644 index 0ed85b1b1..000000000 --- a/packages/vue-components/__tests__/OmegaForm/Meta.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -// __tests__/OmegaForm/Meta.test.ts -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema } from "../../src/components/OmegaForm" - -const subStruct = { - a: S.NullOr(S.String), - b: S.UndefinedOr(S.Finite), - c: S.NullishOr(S.Finite), - d: S.String, - e: S.Finite, - f: S.Boolean -} - -const schema = S.Struct({ - ...subStruct, - struct: S.Struct(subStruct), - nullableStruct: S.NullOr(S.Struct(subStruct)) -}) - -const expectedSubMeta = { - a: { required: false, nullableOrUndefined: "null", type: "string" }, - b: { required: false, nullableOrUndefined: "undefined", type: "number" }, - c: { required: false, nullableOrUndefined: "undefined", type: "number" }, - d: { type: "string", required: false, nullableOrUndefined: false }, - e: { type: "number", required: true, nullableOrUndefined: false }, - f: { type: "boolean", required: true, nullableOrUndefined: false } -} - -describe("Meta story characterization", () => { - it("matches the rendered meta keys and values", () => { - const { meta } = generateMetaFromSchema(schema) - - for (const [key, value] of Object.entries(expectedSubMeta)) { - expect(meta[key as keyof typeof meta], `root.${key}`).toMatchObject(value) - expect(meta[`struct.${key}` as keyof typeof meta], `struct.${key}`).toMatchObject(value) - expect(meta[`nullableStruct.${key}` as keyof typeof meta], `nullableStruct.${key}`).toMatchObject(value) - } - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/NullableFieldInitialization.test.ts b/packages/vue-components/__tests__/OmegaForm/NullableFieldInitialization.test.ts deleted file mode 100644 index a59acb468..000000000 --- a/packages/vue-components/__tests__/OmegaForm/NullableFieldInitialization.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it, vi } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -describe("Nullable field initialization", () => { - it("should initialize nullable fields with null when missing from defaultValues", async () => { - const schema = S.Struct({ - aString: S.NullOr(S.NonEmptyString255).withConstructorDefault, - bString: S.NullOr(S.NonEmptyString255).withConstructorDefault, - cStruct: S - .NullOr(S.Struct({ - dString: S.NullOr(S.NonEmptyString255).withConstructorDefault - })) - .withConstructorDefault, - cLiteral: S.NullOr(S.Literal("test", "test2")).withConstructorDefault - }) - - let submittedValue: Record | null = null - let submitError = null - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - defaultValues: { - aString: null - }, - onSubmit: async ({ value }) => { - try { - submittedValue = value - } catch (error) { - submitError = error - } - } - }) - return { form } - } - }) - - // Check that both fields are initialized with null (not empty string) - await vi.waitFor(() => { - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - expect(valuesText).toContain("\"aString\":null") - expect(valuesText).toContain("\"bString\":null") - expect(valuesText).toContain("\"cStruct\":null") - }) - - // Submit the form - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - - // Wait for the submission to complete - await vi.waitFor(() => { - expect(submittedValue).not.toBeNull() - }) - - // Check that form submitted successfully without errors - expect(submitError).toBeNull() - expect(submittedValue).toEqual({ - aString: null, - bString: null, - cStruct: null, - cLiteral: null - }) - }) - - it("should convert empty string to null for nullable fields", async () => { - const schema = S.Struct({ - aString: S.NullOr(S.NonEmptyString255).withConstructorDefault, - bString: S.NonEmptyString255 - }) - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - onSubmit: async () => { - // Should not be called since bString is required and missing - } - }) - return { form } - } - }) - - await wrapper.vm.$nextTick() - - // aString should be null (nullable field with empty string) - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - expect(valuesText).toContain("\"aString\":null") - - // Submit the form - should fail because bString is required - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - await wrapper.vm.$nextTick() - - // Check that there's an error for bString - const errors = wrapper.find("[data-testid=\"omega-errors\"]") - if (errors.exists()) { - expect(errors.text()).toContain("bString") - } - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/NullableNestedStructValidation.test.ts b/packages/vue-components/__tests__/OmegaForm/NullableNestedStructValidation.test.ts deleted file mode 100644 index 7e8b487f3..000000000 --- a/packages/vue-components/__tests__/OmegaForm/NullableNestedStructValidation.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it, vi } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -/** - * Regression repro for the configurator `LinkedOption.override` schema. - * - * `override` is a nullable struct (`S.NullOr(S.Struct({...})).withConstructorDefault`) - * whose own children are themselves nullable (`S.NullOr(...)`). The form starts - * with `override === null`. As soon as the user fills ONE child (`override.min`), - * the struct materialises while its siblings (`override.max`, `override.readOnly`) - * are still `undefined`. - * - * `undefined` is not a valid member of `S.NullOr(...)`, so validation rejected the - * untouched siblings with "field must not be empty" and the form couldn't submit. - * The configurator worked around this by making every child `S.optional`; the - * correct behaviour is for OmegaForm to treat an untouched nullable child as - * `null` once its nullable parent struct materialises. - */ -describe("Nullable nested struct validation", () => { - const schema = S.Struct({ - optionId: S.NullOr(S.String), - override: S - .NullOr(S.Struct({ - min: S.NullOr(S.NonNegativeNumber), - max: S.NullOr(S.NonNegativeNumber), - readOnly: S.NullOr(S.Boolean).withConstructorDefault, - isInteger: S.optional(S.NullOr(S.Boolean)) - })) - .withConstructorDefault - }) - - it("submits when only one child of a nullable struct is filled", async () => { - let submittedValue: Record | null = null - - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - onSubmit: async ({ value }) => { - submittedValue = value as Record - } - }) - return { form } - } - }) - - // The whole struct starts null. - await vi.waitFor(() => { - expect(wrapper.find("[data-testid=\"values\"]").text()).toContain("\"override\":null") - }) - - // User fills only `override.min`. - await wrapper.find("[data-testid=\"min-input\"]").setValue("100") - - // The struct materialised: its untouched nullable siblings are backfilled - // into the live form state, not left `undefined`. - await vi.waitFor(() => { - const valuesText = wrapper.find("[data-testid=\"values\"]").text() - expect(valuesText).toContain("\"min\":100") - expect(valuesText).toContain("\"max\":null") - expect(valuesText).toContain("\"readOnly\":null") - }) - - // Submit. - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - - // The untouched nullable siblings must NOT block submission; they decode as null. - await vi.waitFor(() => { - expect(submittedValue).not.toBeNull() - }) - - expect(submittedValue).toMatchObject({ - override: { - min: 100, - max: null, - readOnly: null - } - }) - }) - - it("keeps an untouched nullable struct as null", async () => { - let submittedValue: Record | null = null - - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema, { - onSubmit: async ({ value }) => { - submittedValue = value as Record - } - }) - return { form } - } - }) - - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - - await vi.waitFor(() => { - expect(submittedValue).not.toBeNull() - }) - - // Nothing was filled — `override` must stay null, not materialise. - expect(submittedValue).toMatchObject({ override: null }) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/OmegaAutoGen.test.ts b/packages/vue-components/__tests__/OmegaForm/OmegaAutoGen.test.ts deleted file mode 100644 index 2884bfb4c..000000000 --- a/packages/vue-components/__tests__/OmegaForm/OmegaAutoGen.test.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import * as Order from "effect/Order" -import { describe, expect, it } from "vitest" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -// We need to mock the components used by OmegaAutoGen.vue since they depend on Vuetify -const mockComponents = { - OmegaInput: { - template: ` -
- - -
- `, - props: ["form", "name", "label"] - } -} - -describe("OmegaAutoGen", () => { - it("generates components from schema with the correct props", () => { - const wrapper = mount( - { - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - string: S.String, - number: S.Finite - }) - ) - return { form } - } - }, - { - global: { - stubs: { - OmegaInput: mockComponents.OmegaInput - } - } - } - ) - - const inputs = wrapper.findAll(".mock-omega-input") - expect(inputs.length).toBe(2) // Should generate 2 inputs - - const firstInput = inputs[0] - expect(firstInput.find("label").attributes("for")).toBe("string") - expect(firstInput.find("input").attributes("name")).toBe("string") - - const secondInput = inputs[1] - expect(secondInput.find("label").attributes("for")).toBe("number") - expect(secondInput.find("input").attributes("name")).toBe("number") - }) - - // Pick option test - it("uses the pick option to include only specific fields", () => { - const wrapper = mount( - { - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - string: S.String, - number: S.Finite, - boolean: S.Boolean - }) - ) - return { form } - } - }, - { - global: { - stubs: { - OmegaInput: mockComponents.OmegaInput - } - } - } - ) - - // Should only have inputs for fields in the pick array - const inputs = wrapper.findAll(".mock-omega-input") - expect(inputs.length).toBe(1) - expect(inputs[0].find("input").attributes("name")).toBe("string") - }) - - // Omit option test - it("uses the omit option to exclude specific fields", () => { - const wrapper = mount( - { - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - string: S.String, - number: S.Finite, - boolean: S.Boolean - }) - ) - return { form } - } - }, - { - global: { - stubs: { - OmegaInput: mockComponents.OmegaInput - } - } - } - ) - - // Should only have inputs for fields not in the omit array - const inputs = wrapper.findAll(".mock-omega-input") - expect(inputs.length).toBe(1) - expect(inputs[0].find("input").attributes("name")).toBe("string") - }) - - // Order option test - it("uses the order option to control field order", () => { - const wrapper = mount( - { - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - string: S.String, - number: S.Finite, - boolean: S.Boolean - }) - ) - return { form } - } - }, - { - global: { - stubs: { - OmegaInput: mockComponents.OmegaInput - } - } - } - ) - - // Check that the fields are ordered according to the specified order - const inputs = wrapper.findAll(".mock-omega-input input") - const inputNames = inputs.map((input) => input.attributes("name")) - - // The order should match the specified order - expect(inputNames[0]).toBe("boolean") - expect(inputNames[1]).toBe("number") - expect(inputNames[2]).toBe("string") - }) - - // Sort option test - it("uses the sort option to sort fields", () => { - const wrapper = mount( - { - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - charlie: S.String, - alpha: S.String, - bravo: S.String - }) - ) - - // Create a sorter that sorts by name alphabetically - const sortByName = Order.mapInput( - Order.String, - (item: { name: string }) => item.name - ) - - return { form, sortByName } - } - }, - { - global: { - stubs: { - OmegaInput: mockComponents.OmegaInput - } - } - } - ) - - // Check that the fields are sorted alphabetically by name - const inputs = wrapper.findAll(".mock-omega-input input") - const inputNames = inputs.map((input) => input.attributes("name")) - - // The fields should be sorted alphabetically - expect(inputNames[0]).toBe("alpha") - expect(inputNames[1]).toBe("bravo") - expect(inputNames[2]).toBe("charlie") - }) - - // LabelMap - it("applies the labelMap option", () => { - const wrapper = mount( - { - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - string: S.String, - number: S.Finite - }) - ) - return { form } - } - }, - { - global: { - stubs: { - OmegaInput: mockComponents.OmegaInput - } - } - } - ) - - const labels = wrapper.findAll("label") - expect(labels[0].text()).toBe("Custom String Label") - expect(labels[1].text()).toBe("number") // Default label is the key - }) - - // Custom slot - it("allows custom slot content", () => { - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - - - `, - setup() { - const form = useOmegaForm( - S.Struct({ - string: S.String, - number: S.Finite - }) - ) - return { form } - } - }) - - const customElements = wrapper.findAll(".custom-slot") - expect(customElements.length).toBe(2) - - expect(wrapper.find("[data-testid=\"string-custom\"]").exists()).toBe(true) - expect(wrapper.find("[data-testid=\"number-custom\"]").exists()).toBe(true) - - const labels = wrapper.findAll("label") - expect(labels[0].text()).toBe("Custom string") - expect(labels[1].text()).toBe("Custom number") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/OptionalKey.test.ts b/packages/vue-components/__tests__/OmegaForm/OptionalKey.test.ts deleted file mode 100644 index 03363ab36..000000000 --- a/packages/vue-components/__tests__/OmegaForm/OptionalKey.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as Effect from "effect-app/Effect" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema } from "../../src/components/OmegaForm" - -describe("optionalKey required handling", () => { - it("should mark optionalKey fields as not required", () => { - const schema = S.Struct({ - number: S.optionalKey(S.Int.pipe(S.check(S.isBetween({ minimum: 1, maximum: 20 })))), - height: S.NonEmptyString100.pipe(S.check(S.isMinLength(10))) - }) - - const { meta } = generateMetaFromSchema(schema) - - // optionalKey field should NOT be required - expect(meta.number?.required).toBe(false) - - // regular field should still be required - expect(meta.height?.required).toBe(true) - }) - - it("should mark optionalKey with decodingDefault as not required", () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("defaultName"))), - age: S.NonEmptyString255 - }) - - const { meta } = generateMetaFromSchema(schema) - - expect(meta.name?.required).toBe(false) - expect(meta.age?.required).toBe(true) - }) - - it("should handle optionalKey in tagged union branches", () => { - const schema = S.Union([ - S.TaggedStruct("one", { - a: S.Struct({ - number: S.optionalKey(S.Int.pipe(S.check(S.isBetween({ minimum: 1, maximum: 20 })))), - height: S.NonEmptyString100.pipe(S.check(S.isMinLength(10))), - z: S.NonEmptyString100.pipe(S.check(S.isMinLength(10))) - }) - }), - S.TaggedStruct("two", { - a: S.Struct({ - number: S.optionalKey(S.Int.pipe(S.check(S.isBetween({ minimum: 1, maximum: 20 })))), - height: S.NonEmptyString100.pipe(S.check(S.isMinLength(10))), - y: S.NonEmptyString100.pipe(S.check(S.isMinLength(10))) - }) - }) - ]) - - const { meta } = generateMetaFromSchema(schema) - - // optionalKey in both union branches should not be required - expect(meta["a.number"]?.required).toBe(false) - - // regular fields should be required - expect(meta["a.height"]?.required).toBe(true) - expect(meta["a.z"]?.required).toBe(true) - expect(meta["a.y"]?.required).toBe(true) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/RedactedMeta.test.ts b/packages/vue-components/__tests__/OmegaForm/RedactedMeta.test.ts deleted file mode 100644 index e2d46c0a5..000000000 --- a/packages/vue-components/__tests__/OmegaForm/RedactedMeta.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import * as S from "effect-app/Schema" -import * as Redacted from "effect/Redacted" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema, toFormSchema } from "../../src/components/OmegaForm" - -describe("S.Redacted meta generation", () => { - it("should generate correct meta for S.Redacted(S.NonEmptyString255)", () => { - const schema = S.Struct({ - email: S.NonEmptyString255, - password: S.Redacted(S.NonEmptyString255) - }) - - const result = generateMetaFromSchema(schema) - - expect(result.meta["email"]).toMatchObject({ - type: "string", - minLength: 1, - maxLength: 255, - required: true, - nullableOrUndefined: false - }) - - expect(result.meta["password"]).toMatchObject({ - type: "string", - minLength: 1, - maxLength: 255, - required: true, - nullableOrUndefined: false - }) - }) - - it("should generate correct meta for S.Redacted(S.String) — not required since empty string is valid", () => { - const schema = S.Struct({ - secret: S.Redacted(S.String) - }) - - const result = generateMetaFromSchema(schema) - - expect(result.meta["secret"]).toMatchObject({ - type: "string", - required: false, - nullableOrUndefined: false - }) - }) - - it("should generate correct meta for S.NullOr(S.Redacted(S.NonEmptyString255))", () => { - const schema = S.Struct({ - optionalSecret: S.NullOr(S.Redacted(S.NonEmptyString255)) - }) - - const result = generateMetaFromSchema(schema) - - expect(result.meta["optionalSecret"]).toMatchObject({ - type: "string", - required: false, - nullableOrUndefined: "null" - }) - }) -}) - -describe("toFormSchema — S.Redacted validation", () => { - it("should accept plain strings for S.Redacted fields and decode to Redacted", () => { - const schema = S.Struct({ - email: S.NonEmptyString255, - password: S.Redacted(S.NonEmptyString255) - }) - - const formSchema = toFormSchema(schema) - const result = S.decodeUnknownSync(formSchema)({ - email: "test@test.com", - password: "secret123" - }) - - expect(result.email).toBe("test@test.com") - expect(Redacted.isRedacted(result.password)).toBe(true) - expect(Redacted.value(result.password as Redacted.Redacted)).toBe("secret123") - }) - - it("should accept plain strings for S.Redacted(S.String)", () => { - const schema = S.Struct({ - secret: S.Redacted(S.String) - }) - - const formSchema = toFormSchema(schema) - const result = S.decodeUnknownSync(formSchema)({ secret: "hello" }) - - expect(Redacted.isRedacted(result.secret)).toBe(true) - }) - - it("should pass standard schema validation with plain string values", () => { - const schema = S.Struct({ - email: S.NonEmptyString255, - password: S.Redacted(S.NonEmptyString255) - }) - - const formSchema = toFormSchema(schema) - const ssv = S.toStandardSchemaV1(formSchema) - const result = (ssv as any)["~standard"].validate({ - email: "test@test.com", - password: "secret123" - }) - - expect(result.issues).toBeUndefined() - }) - - it("should still reject invalid inner values", () => { - const schema = S.Struct({ - password: S.Redacted(S.NonEmptyString255) - }) - - const formSchema = toFormSchema(schema) - expect(() => S.decodeUnknownSync(formSchema)({ password: "" })).toThrow() - }) - - it("should handle NullOr(S.Redacted(...))", () => { - const schema = S.Struct({ - secret: S.NullOr(S.Redacted(S.NonEmptyString255)) - }) - - const formSchema = toFormSchema(schema) - - const result1 = S.decodeUnknownSync(formSchema)({ secret: "hello" }) - expect(Redacted.isRedacted(result1.secret)).toBe(true) - - const result2 = S.decodeUnknownSync(formSchema)({ secret: null }) - expect(result2.secret).toBeNull() - }) - - it("should return the original schema when no S.Redacted fields exist", () => { - const schema = S.Struct({ - email: S.NonEmptyString255, - name: S.String - }) - - const formSchema = toFormSchema(schema) - expect(formSchema).toBe(schema) - }) - - it("should work with S.Class schemas", () => { - class Credentials extends S.Class("Credentials")({ - email: S.NonEmptyString255, - password: S.Redacted(S.NonEmptyString255) - }) {} - - const formSchema = toFormSchema(Credentials) - const result = S.decodeUnknownSync(formSchema)({ - email: "test@test.com", - password: "secret" - }) - - expect(Redacted.isRedacted(result.password)).toBe(true) - }) - - it("toFormSchema rewrites NullOr(Redacted)", () => { - const schema = S.Struct({ - secret: S.NullOr(S.Redacted(S.String)) - }) - const formSchema = toFormSchema(schema) - // formSchema must accept a plain string (encoded side) for the secret field - const decoded = S.decodeUnknownSync(formSchema)({ secret: "hello" }) - expect(decoded.secret).toBeDefined() - }) - - it("toFormSchema rewrites UndefinedOr(Redacted)", () => { - const schema = S.Struct({ - secret: S.UndefinedOr(S.Redacted(S.String)) - }) - const formSchema = toFormSchema(schema) - const decoded = S.decodeUnknownSync(formSchema)({ secret: "hello" }) - expect(decoded.secret).toBeDefined() - }) - - it("toFormSchema rewrites NullishOr(Redacted)", () => { - const schema = S.Struct({ - secret: S.NullishOr(S.Redacted(S.String)) - }) - const formSchema = toFormSchema(schema) - const decoded = S.decodeUnknownSync(formSchema)({ secret: "hello" }) - expect(decoded.secret).toBeDefined() - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/SelectMultipleLocalization.test.ts b/packages/vue-components/__tests__/OmegaForm/SelectMultipleLocalization.test.ts deleted file mode 100644 index 542a0b302..000000000 --- a/packages/vue-components/__tests__/OmegaForm/SelectMultipleLocalization.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -// __tests__/OmegaForm/SelectMultipleLocalization.test.ts -import { mount } from "@vue/test-utils" -import * as Effect from "effect-app/Effect" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defineComponent } from "vue" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -const mountForm = (setup: () => T): T => { - let captured: T | undefined - const Inner = defineComponent({ - setup() { - captured = setup() - return {} - }, - template: "
" - }) - mount(defineComponent({ - components: { OmegaIntlProvider, Inner }, - template: "" - })) - if (!captured) throw new Error("no capture") - return captured -} - -const firstFieldError = (form: any, name: string): string | undefined => { - const errs = form.getAllErrors().fields[name]?.errors - if (!errs) return undefined - const flat = errs.flat ? errs.flat() : errs - return flat[0]?.message -} - -describe("select / multiple validation messages carry localized payload", () => { - it("S.Literals — invalid (non-empty) member emits 'select' message with members", async () => { - const schema = S.Struct({ color: S.Literals(["red", "blue", "green"]) }) - const form = mountForm(() => - useOmegaForm(schema, { - defaultValues: { color: "purple" } as any, - onSubmit: async () => undefined - }) - ) - await Effect.runPromise(form.handleSubmitEffect()) - const msg = firstFieldError(form, "color") - // OmegaIntlProvider's mock `trans` returns `${key}_translated` and - // ignores variables — so we just need the i18n key to flow, not the - // substituted variables. - expect(msg, `got: ${msg}`).toBe("validation.not_a_valid_translated") - }) - - it("S.Array(S.Literals) — non-array value emits 'multiple' message", async () => { - const schema = S.Struct({ tags: S.Array(S.Literals(["a", "b", "c"])) }) - const form = mountForm(() => - useOmegaForm(schema, { - defaultValues: { tags: "not-an-array" } as any, - onSubmit: async () => undefined - }) - ) - await Effect.runPromise(form.handleSubmitEffect()) - const msg = firstFieldError(form, "tags") - expect(msg, `got: ${msg}`).toBe("validation.not_a_valid_translated") - }) - - it("S.NullOr(S.Literals) — invalid member emits localized select message", async () => { - const schema = S.Struct({ color: S.NullOr(S.Literals(["red", "blue", "green"])) }) - const form = mountForm(() => - useOmegaForm(schema, { - defaultValues: { color: "purple" } as any, - onSubmit: async () => undefined - }) - ) - await Effect.runPromise(form.handleSubmitEffect()) - const msg = firstFieldError(form, "color") - expect(msg, `got: ${msg}`).toBe("validation.not_a_valid_translated") - }) - - it("S.UndefinedOr(S.Literals) — invalid member emits localized select message", async () => { - const schema = S.Struct({ color: S.UndefinedOr(S.Literals(["red", "blue", "green"])) }) - const form = mountForm(() => - useOmegaForm(schema, { - defaultValues: { color: "purple" } as any, - onSubmit: async () => undefined - }) - ) - await Effect.runPromise(form.handleSubmitEffect()) - const msg = firstFieldError(form, "color") - expect(msg, `got: ${msg}`).toBe("validation.not_a_valid_translated") - }) - - it("user-supplied .annotate({ message }) on S.Literals is preserved", async () => { - const schema = S.Struct({ - color: S.Literals(["red", "blue", "green"]).annotate({ - message: "user.custom.message" - }) - }) - const form = mountForm(() => - useOmegaForm(schema, { - defaultValues: { color: "purple" } as any, - onSubmit: async () => undefined - }) - ) - await Effect.runPromise(form.handleSubmitEffect()) - const msg = firstFieldError(form, "color") - // User annotation wins — verbatim, not piped through trans. - expect(msg).toBe("user.custom.message") - }) - - it("empty value on a required select also emits the localized 'select' message (matches main)", async () => { - // Main's `generateInputStandardSchemaFromFieldMeta` annotated the entire - // S.Literals schema with a `validation.not_a_valid` message, so any - // failure (including undefined) surfaced it. We preserve that behavior. - const schema = S.Struct({ color: S.Literals(["red", "blue", "green"]) }) - const form = mountForm(() => - useOmegaForm(schema, { - defaultValues: { color: undefined } as any, - onSubmit: async () => undefined - }) - ) - await Effect.runPromise(form.handleSubmitEffect()) - const msg = firstFieldError(form, "color") - expect(msg).toBe("validation.not_a_valid_translated") - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/SubmitEffect.test.ts b/packages/vue-components/__tests__/OmegaForm/SubmitEffect.test.ts deleted file mode 100644 index 2c4fd11fb..000000000 --- a/packages/vue-components/__tests__/OmegaForm/SubmitEffect.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -// __tests__/OmegaForm/SubmitEffect.test.ts -import { mount } from "@vue/test-utils" -import * as Effect from "effect-app/Effect" -import * as S from "effect-app/Schema" -import * as Cause from "effect/Cause" -import * as Exit from "effect/Exit" -import { describe, expect, it } from "vitest" -import { defineComponent } from "vue" -import { FormErrors, useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -const mountForm = (setupForm: () => T): T => { - let captured: T | undefined - const Inner = defineComponent({ - setup() { - captured = setupForm() - return {} - }, - template: "
" - }) - const Wrapper = defineComponent({ - components: { OmegaIntlProvider, Inner }, - template: "" - }) - mount(Wrapper) - if (!captured) throw new Error("setupForm did not return") - return captured -} - -describe("handleSubmitEffect", () => { - it("succeeds with void on valid input (no checkErrors)", async () => { - const form = mountForm(() => - useOmegaForm( - S.Struct({ x: S.String.pipe(S.check(S.isMinLength(2))) }), - { defaultValues: { x: "ok" }, onSubmit: async () => undefined } - ) - ) - const exit = await Effect.runPromise(Effect.exit(form.handleSubmitEffect())) - expect(Exit.isSuccess(exit)).toBe(true) - }) - - it("succeeds with void even on invalid input without checkErrors", async () => { - const form = mountForm(() => - useOmegaForm( - S.Struct({ x: S.String.pipe(S.check(S.isMinLength(2))) }), - { defaultValues: { x: "" }, onSubmit: async () => undefined } - ) - ) - const exit = await Effect.runPromise(Effect.exit(form.handleSubmitEffect())) - expect(Exit.isSuccess(exit)).toBe(true) - }) - - it("fails with FormErrors when checkErrors and validation fails", async () => { - const form = mountForm(() => - useOmegaForm( - S.Struct({ x: S.String.pipe(S.check(S.isMinLength(2))) }), - { defaultValues: { x: "" }, onSubmit: async () => undefined } - ) - ) - const exit = await Effect.runPromise( - Effect.exit(form.handleSubmitEffect({ checkErrors: true })) - ) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) { - const failReason = exit.cause.reasons.find(Cause.isFailReason) - expect(failReason?.error).toBeInstanceOf(FormErrors) - } - }) - - it("delivers decoded To to onSubmit (not raw From)", async () => { - let received: unknown - const form = mountForm(() => - useOmegaForm( - S.Struct({ n: S.FiniteFromString }), - { - defaultValues: { n: "42" }, - onSubmit: async ({ value }) => { - received = value - } - } - ) - ) - await Effect.runPromise(form.handleSubmitEffect()) - expect(received).toEqual({ n: 42 }) - }) - - it("awaits Promise-returning onSubmit", async () => { - let resolved = false - const form = mountForm(() => - useOmegaForm( - S.Struct({ x: S.String }), - { - defaultValues: { x: "" }, - onSubmit: async () => { - await new Promise((r) => setTimeout(r, 5)) - resolved = true - } - } - ) - ) - await Effect.runPromise(form.handleSubmitEffect()) - expect(resolved).toBe(true) - }) - - it("awaits Effect-returning onSubmit", async () => { - let resolved = false - const form = mountForm(() => - useOmegaForm( - S.Struct({ x: S.String }), - { - defaultValues: { x: "" }, - onSubmit: () => - Effect.sync(() => { - resolved = true - }) - } - ) - ) - await Effect.runPromise(form.handleSubmitEffect()) - expect(resolved).toBe(true) - }) - - it("awaits Fiber-returning onSubmit", async () => { - let resolved = false - const form = mountForm(() => - useOmegaForm( - S.Struct({ x: S.String }), - { - defaultValues: { x: "" }, - onSubmit: () => - Effect.runFork( - Effect.sync(() => { - resolved = true - }) - ) - } - ) - ) - await Effect.runPromise(form.handleSubmitEffect()) - expect(resolved).toBe(true) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/SubmitErrorClear.test.ts b/packages/vue-components/__tests__/OmegaForm/SubmitErrorClear.test.ts deleted file mode 100644 index 7d4bb7200..000000000 --- a/packages/vue-components/__tests__/OmegaForm/SubmitErrorClear.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -// __tests__/OmegaForm/SubmitErrorClear.test.ts -import { mount } from "@vue/test-utils" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defineComponent, nextTick } from "vue" -import { useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -describe("post-submit revalidation (TanStack default)", () => { - it("revalidates the form-level schema on value change after a failed submit", async () => { - const schema = S.Struct({ - a: S.String.pipe(S.check(S.isMinLength(2))), - b: S.String.pipe(S.check(S.isMinLength(2))) - }) - - let captured: ReturnType> | undefined - const Inner = defineComponent({ - setup() { - const form = useOmegaForm(schema, { defaultValues: { a: "", b: "" } }) - captured = form as any - return { form } - }, - // Render fields so they register with the form. Without mounted fields, - // setFieldValue cannot trigger validation (FormApi.validateField bails - // when fieldInfo[name].instance is undefined). - template: ` - - - - - ` - }) - const Wrapper = defineComponent({ - components: { OmegaIntlProvider, Inner }, - template: "" - }) - mount(Wrapper) - if (!captured) throw new Error("setupForm did not return") - const form = captured - - await form.handleSubmit() - await nextTick() - - // After failed submit, fix only field a; field b should still fail validation - // because TanStack revalidates the entire form-level schema. - form.setFieldValue("a", "ok") - await nextTick() - await nextTick() - - // canSubmit should still be false because b is still invalid - expect(form.store.state.canSubmit).toBe(false) - - // Now fix b too - form.setFieldValue("b", "ok") - await nextTick() - await nextTick() - expect(form.store.state.canSubmit).toBe(true) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaForm/TaggedUnionBehavior.test.ts b/packages/vue-components/__tests__/OmegaForm/TaggedUnionBehavior.test.ts deleted file mode 100644 index 7e229904e..000000000 --- a/packages/vue-components/__tests__/OmegaForm/TaggedUnionBehavior.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { mount } from "@vue/test-utils" -import * as Effect from "effect-app/Effect" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defineComponent, nextTick } from "vue" -import { createUseFormWithCustomInput } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -const HeadlessInput = defineComponent({ - inheritAttrs: false, - props: ["inputProps", "field", "state", "options"], - template: ` -
- - - {{ inputProps.errorMessages?.join(', ') }} -
- `, - methods: { - onInput(this: any, e: Event) { - const target = e.target as HTMLInputElement - const value = target.value - if (this.inputProps.type === "number") { - this.field.handleChange(value === "" ? null : Number(value)) - } else { - this.field.handleChange(value) - } - } - } -}) - -const useForm = createUseFormWithCustomInput(HeadlessInput) - -const flushAfterTagChange = async () => { - // OmegaTaggedUnionInternal watches _tag and on change calls form.reset(), - // then setTimeout(validate, 0). Give it room. - await nextTick() - await nextTick() - await new Promise((r) => setTimeout(r, 10)) - await nextTick() -} - -describe("RootLevelTaggedUnion behavior", () => { - const schema = S.Union([ - S.TaggedStruct("A", { - a: S.NonEmptyString255.pipe( - S.withConstructorDefault(Effect.succeed(S.NonEmptyString255("aaaa"))) - ), - common: S.NonEmptyString255 - }), - S.TaggedStruct("B", { - b: S.Finite, - nullableB: S.NullOr(S.Finite), - common: S.NullOr(S.String) - }) - ]) - - const mountForm = (onSubmit?: (value: any) => void) => { - return mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useForm(schema as any, { - defaultValues: { _tag: "A" } as any, - onSubmit: async ({ value }: any) => onSubmit?.(value) - }) - const tagOptions = [ - { value: "A", title: "Option A" }, - { value: "B", title: "Option B" } - ] - return { form, tagOptions } - } - }) - } - - it("renders branch A's slot by default and not B's", async () => { - const wrapper = mountForm() - await nextTick() - - expect(wrapper.find("[data-testid=\"input-a\"]").exists()).toBe(true) - expect(wrapper.find("[data-testid=\"input-common\"]").exists()).toBe(true) - expect(wrapper.find("[data-testid=\"input-b\"]").exists()).toBe(false) - expect(wrapper.find("[data-testid=\"input-nullableB\"]").exists()).toBe(false) - }) - - it("toggling _tag from A to B swaps the rendered branch slot", async () => { - const wrapper = mountForm() - await nextTick() - - const select = wrapper.find("[data-testid=\"select-_tag\"]") - expect(select.exists()).toBe(true) // Drive the change through the form API to mirror what the UI handler - // produces. setValue + change events on the bare - {{ inputProps.errorMessages.join(', ') }} - - `, - props: ["inputProps", "field"], - inheritAttrs: false -} - -describe("createUseFormWithCustomInput", () => { - const useForm = createUseFormWithCustomInput(CustomTestInput) - const schema = S.Struct({ testField: S.String }) - - const createWrapper = (options?: { - defaultValues?: any - onSubmit?: (data: any) => void | Promise - }) => { - let submittedValue: any = null - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useForm(schema, { - defaultValues: options?.defaultValues || { testField: "" }, - onSubmit: async ({ value }) => { - submittedValue = value - options?.onSubmit?.(value) - } - }) - return { form, getSubmittedValue: () => submittedValue } - } - }, { - global: { - stubs: { - CustomTestInput - } - } - }) - - return wrapper - } - - it("should render custom input component with correct props", async () => { - const wrapper = createWrapper({ defaultValues: { testField: "test value" } }) - await wrapper.vm.$nextTick() - - // Check that custom input was rendered - const customInput = wrapper.find("[data-testid=\"custom-input\"]") - expect(customInput.exists()).toBe(true) - expect(customInput.classes()).toContain("custom-test-input") - - // Verify the input has the correct value - const inputElement = wrapper.find("[data-testid=\"custom-input-field\"]") - expect(inputElement.exists()).toBe(true) - expect((inputElement.element as HTMLInputElement).value).toBe("test value") - - // Verify the label is rendered - const label = customInput.find("label") - expect(label.exists()).toBe(true) - expect(label.text()).toContain("Test Field") - }) - - it("should handle input changes and submission", async () => { - const wrapper = createWrapper({ defaultValues: { testField: "initial" } }) - await wrapper.vm.$nextTick() - - // Verify initial value - const input = wrapper.find("[data-testid=\"custom-input-field\"]") - expect((input.element as HTMLInputElement).value).toBe("initial") - - // Change the value - await input.setValue("changed value") - await wrapper.vm.$nextTick() - - // Verify the value changed in the form state - const valuesDiv = wrapper.find("[data-testid=\"values\"]") - expect(valuesDiv.text()).toContain("\"testField\":\"changed value\"") - - // Submit the form - await wrapper.find("[data-testid=\"submit\"]").trigger("click") - await wrapper.vm.$nextTick() - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Check that the form received the changed value - const vm = wrapper.vm as any - expect(vm.getSubmittedValue()).toEqual({ testField: "changed value" }) - }) - - it("should pass inputProps to custom component", async () => { - // Custom component that validates props - const PropsValidatingInput = { - template: ` -
-
{{ !!inputProps }}
-
{{ !!field }}
-
{{ inputProps.label }}
-
{{ field.name }}
-
{{ inputProps.id }}
-
- `, - props: ["inputProps", "field"] - } - - const useFormWithValidator = createUseFormWithCustomInput(PropsValidatingInput) - const validationSchema = S.Struct({ myField: S.String }) - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useFormWithValidator(validationSchema, { - defaultValues: { myField: "test-value" } - }) - return { form } - } - }, { - global: { - stubs: { - PropsValidatingInput - } - } - }) - - await wrapper.vm.$nextTick() - - const validator = wrapper.find("[data-testid=\"props-validator\"]") - expect(validator.exists()).toBe(true) - - // Check all required props are passed - expect(wrapper.find("[data-testid=\"has-inputprops\"]").text()).toBe("true") - expect(wrapper.find("[data-testid=\"has-field\"]").text()).toBe("true") - expect(wrapper.find("[data-testid=\"has-label\"]").text()).toContain("My Label") - expect(wrapper.find("[data-testid=\"has-id\"]").text()).toBeTruthy() - }) - - it("forwards an explicit `required` override to inputProps (survives OmegaInput's prop whitelist)", async () => { - const RequiredProbe = { - template: `
{{ inputProps.required }}
`, - props: ["inputProps", "field"] - } - - const useFormWithProbe = createUseFormWithCustomInput(RequiredProbe) - // optionalKey field => meta.required is false, so a `true` result proves the - // explicit prop overrode meta AND was not dropped on the way to the input. - const optionalSchema = S.Struct({ maybe: S.optionalKey(S.Number) }) - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useFormWithProbe(optionalSchema, { defaultValues: {} }) - return { form } - } - }, { - global: { - stubs: { - RequiredProbe - } - } - }) - - await wrapper.vm.$nextTick() - - expect(wrapper.find("[data-testid=\"required-probe\"]").text()).toBe("true") - }) - - it("falls back to schema-derived required when the prop is omitted", async () => { - const RequiredProbe = { - template: ` -
-
{{ inputProps.required }}
-
{{ inputProps.label }}
-
- `, - props: ["inputProps", "field"] - } - - const useFormWithProbe = createUseFormWithCustomInput(RequiredProbe) - const requiredSchema = S.Struct({ number: S.Number }) - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useFormWithProbe(requiredSchema) - return { form } - } - }, { - global: { - stubs: { - RequiredProbe - } - } - }) - - await wrapper.vm.$nextTick() - - expect(wrapper.find("[data-testid=\"required-probe\"]").text()).toBe("true") - expect(wrapper.find("[data-testid=\"label-probe\"]").text()).toBe("Number *") - }) - - it("should pass event listeners to custom input component", async () => { - // Custom component that emits focus and blur events - const EventEmittingInput = { - template: ` -
- -
- `, - props: ["inputProps", "field"], - emits: ["focus", "blur"], - inheritAttrs: false - } - - const useFormWithEvents = createUseFormWithCustomInput(EventEmittingInput) - const eventSchema = S.Struct({ eventField: S.String }) - - let focusCount = 0 - let blurCount = 0 - - const wrapper = mount({ - components: { - OmegaIntlProvider - }, - template: ` - - - - - - `, - setup() { - const form = useFormWithEvents(eventSchema, { - defaultValues: { eventField: "" } - }) - - const handleFocus = () => { - focusCount++ - } - - const handleBlur = () => { - blurCount++ - } - - return { form, handleFocus, handleBlur } - } - }, { - global: { - stubs: { - EventEmittingInput - } - } - }) - - await wrapper.vm.$nextTick() - - // Verify the custom input was rendered - const eventInput = wrapper.find("[data-testid=\"event-input\"]") - expect(eventInput.exists()).toBe(true) - - // Trigger focus event - await eventInput.trigger("focusin") - await wrapper.vm.$nextTick() - - // Verify focus handler was called - expect(focusCount).toBe(1) - - // Trigger blur event - await eventInput.trigger("focusout") - await wrapper.vm.$nextTick() - - // Verify blur handler was called - expect(blurCount).toBe(1) - - // Trigger multiple times to ensure it keeps working - await eventInput.trigger("focusin") - await eventInput.trigger("focusout") - await wrapper.vm.$nextTick() - - expect(focusCount).toBe(2) - expect(blurCount).toBe(2) - }) -}) diff --git a/packages/vue-components/__tests__/OmegaFormTest.vue b/packages/vue-components/__tests__/OmegaFormTest.vue deleted file mode 100644 index b84195d3f..000000000 --- a/packages/vue-components/__tests__/OmegaFormTest.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/packages/vue-components/__tests__/OmegaIntlProvider.vue b/packages/vue-components/__tests__/OmegaIntlProvider.vue deleted file mode 100644 index bfcf31cbc..000000000 --- a/packages/vue-components/__tests__/OmegaIntlProvider.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/packages/vue-components/__tests__/migration/schema-v4-migration.test.ts b/packages/vue-components/__tests__/migration/schema-v4-migration.test.ts deleted file mode 100644 index ad5f1393c..000000000 --- a/packages/vue-components/__tests__/migration/schema-v4-migration.test.ts +++ /dev/null @@ -1,703 +0,0 @@ -/** - * OmegaForm Schema Tests - * - * These tests pin the current behavior of generateMetaFromSchema and defaultsValueFromSchema, - * ensuring regression safety during refactors. - * - * Strategy: - * 1. "current" tests → pin behavior with current schema patterns - * 2. When refactoring internals, ALL tests must stay green - * - * NOTE: Uses `S.withConstructorDefault` from effect-app (wraps in Effect), - * NOT `S.withConstructorDefault` from native effect (expects raw Option return). - * - * ---- v4 breaking changes (from v3) ---- - * - * 1. Nested union _tag: FIXED — both patterns now produce "select" - * S.TaggedStruct produces a bare Literal AST node. - * Legacy S.Struct({ _tag: S.Literal("X") }) produces Union([Literal("X")]) - * after AST.toType. Fixed via unwrapSingleLiteralUnion helper. - * A console.warn is emitted for the legacy pattern to encourage migration. - * - * 2. Root-level union unionMeta: FIXED — both patterns now generate unionMeta - * Same root cause as #1: single-element Union wrapping prevented tag extraction. - * Fixed via the same unwrapSingleLiteralUnion helper in metadataFromAst. - * - * 3. UndefinedOr defaults include the key with explicit undefined - * v3: defaultsValueFromSchema skips keys where the recursive call returns - * undefined (if fieldValue !== undefined), so UndefinedOr fields are - * omitted from the result object entirely. - * v4: the key will be present in the result with an explicit undefined value. - * - * 4. S.optionalKey(X).pipe(S.withDecodingDefault(...)) support in defaultsValueFromSchema - * v3: not supported (v3 equivalent was S.optionalWith which encoded defaults - * inside a PropertySignatureTransformation, opaque to AST inspection). - * v4: defaultsValueFromSchema detects PropertySignatureTransformation - * and extracts defaults, enabling withDecodingDefault as a new pattern for - * declaring field defaults directly on the schema. - * - * ---- API mapping ---- - * - * | v4 | v3 | - * |--------------------------------------------|--------------------------------| - * | `S.check(S.isGreaterThanOrEqualTo(x))` | `S.greaterThanOrEqualTo(x)` | - * | `S.check(S.isLessThanOrEqualTo(x))` | `S.lessThanOrEqualTo(x)` | - * | `S.check(S.isGreaterThan(x))` | `S.greaterThan(x)` | - * | `S.check(S.isLessThan(x))` | `S.lessThan(x)` | - * | `S.check(S.isBetween({minimum, maximum}))` | `S.between(minimum, maximum)` | - * | `S.check(S.isInt())` | `S.int()` | - * | `S.check(S.isMinLength(x))` | `S.minLength(x)` | - * | `S.check(S.isMaxLength(x))` | `S.maxLength(x)` | - * | `S.Union([...members])` | `S.Union(...members)` | - * | `S.optionalKey(S.X)` | `S.optionalWith` | - * | `.pipe(S.withDecodingDefault(...))` | `(S.X, { default: ... })` | - */ - -import { mount } from "@vue/test-utils" -import * as Effect from "effect-app/Effect" -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { defaultsValueFromSchema, generateMetaFromSchema, useOmegaForm } from "../../src/components/OmegaForm" -import OmegaIntlProvider from "../OmegaIntlProvider.vue" - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Mount a form and extract its default values */ -const mountAndGetDefaults = async (schema: S.Schema, opts?: any) => { - const wrapper = mount({ - components: { OmegaIntlProvider }, - template: ` - - - - - - `, - setup() { - const form = useOmegaForm(schema as any, opts) - return { form } - } - }) - await wrapper.vm.$nextTick() - return JSON.parse(wrapper.find("[data-testid=\"values\"]").text()) -} - -// ============================================================================ -// 1. generateMetaFromSchema — pin current behavior -// ============================================================================ - -describe("generateMetaFromSchema", () => { - describe("simple struct", () => { - const schema = S.Struct({ - name: S.NonEmptyString255, - age: S.Finite.pipe(S.check(S.isGreaterThanOrEqualTo(0))), - active: S.Boolean - }) - - it("produces correct metadata for basic types", () => { - const { meta } = generateMetaFromSchema(schema) - expect(meta.name?.type).toBe("string") - expect(meta.name?.required).toBe(true) - expect(meta.name?.minLength).toBe(1) - expect(meta.name?.maxLength).toBe(255) - expect(meta.age?.type).toBe("number") - expect(meta.age?.required).toBe(true) - expect(meta.age?.minimum).toBe(0) - expect(meta.active?.type).toBe("boolean") - expect(meta.active?.required).toBe(true) - }) - }) - - describe("nullable fields", () => { - const schema = S.Struct({ - required: S.NonEmptyString255, - nullable: S.NullOr(S.String), - optional: S.UndefinedOr(S.String) - }) - - it("nullable/undefined fields are not required", () => { - const { meta } = generateMetaFromSchema(schema) - expect(meta.required?.required).toBe(true) - expect(meta.nullable?.required).toBe(false) - expect(meta.nullable?.nullableOrUndefined).toBe("null") - expect(meta.optional?.required).toBe(false) - expect(meta.optional?.nullableOrUndefined).toBe("undefined") - }) - }) - - describe("nested struct", () => { - const schema = S.Struct({ - outer: S.Struct({ - inner: S.NonEmptyString255, - deep: S.Struct({ - value: S.Finite - }) - }) - }) - - it("produces dot-notation keys for nested fields", () => { - const { meta } = generateMetaFromSchema(schema) - expect(meta["outer.inner"]).toBeDefined() - expect(meta["outer.inner"]?.type).toBe("string") - expect(meta["outer.deep.value"]).toBeDefined() - expect(meta["outer.deep.value"]?.type).toBe("number") - }) - }) - - describe("discriminated union (nested in struct)", () => { - const schema = S.Struct({ - union: S.Union([ - S.Struct({ _tag: S.tag("A"), a: S.NonEmptyString255 }), - S.Struct({ _tag: S.tag("B"), b: S.Finite }) - ]) - }) - - it("generates _tag as select with member values", () => { - const { meta } = generateMetaFromSchema(schema) - // Works for both S.TaggedStruct and legacy S.Struct({ _tag: S.Literal(...) }) - // thanks to single-element Union unwrapping in createMeta - expect(meta["union._tag"]?.type).toBe("select") - expect(meta["union._tag"]?.required).toBe(true) - expect(meta["union._tag"]?.members).toBeDefined() - }) - - it("generates field metadata for each union branch", () => { - const { meta } = generateMetaFromSchema(schema) - expect(meta["union.a"]?.type).toBe("string") - expect(meta["union.b"]?.type).toBe("number") - }) - - it("nested union now populates unionMeta after unification (Phase 2.4)", () => { - // After unification of root + nested union handling, nested unions also - // populate unionMeta, mirroring root-level behavior. - const { unionMeta } = generateMetaFromSchema(schema) - expect(Object.keys(unionMeta).length).toBe(2) - expect(unionMeta["A"]).toBeDefined() - expect(unionMeta["B"]).toBeDefined() - }) - }) - - describe("nullable discriminated union", () => { - const schema = S.Struct({ - union: S.NullOr( - S.Union([ - S.Struct({ _tag: S.Literal("A"), a: S.NonEmptyString255, common: S.String }), - S.Struct({ _tag: S.Literal("B"), b: S.Finite, common: S.String }) - ]) - ) - }) - - it("_tag is not required, other fields are required based on their type", () => { - const { meta } = generateMetaFromSchema(schema) - expect(meta["union._tag"]?.required).toBe(false) - expect(meta["union.a"]?.required).toBe(true) - expect(meta["union.b"]?.required).toBe(true) - }) - }) - - describe("root-level discriminated union (legacy Literal pattern)", () => { - const schema = S.Union([ - S.Struct({ _tag: S.Literal("A"), a: S.NonEmptyString255 }), - S.Struct({ _tag: S.Literal("B"), b: S.Finite }) - ]) - - it("generates _tag select and per-member fields with unionMeta", () => { - const { meta, unionMeta } = generateMetaFromSchema(schema) - - expect(meta["_tag"]?.type).toBe("select") - expect(meta["_tag"]?.required).toBe(true) - expect(meta.a?.type).toBe("string") - expect(meta.b?.type).toBe("number") - expect(Object.keys(unionMeta).length).toBe(2) - }) - }) - - describe("root-level discriminated union (TaggedStruct pattern)", () => { - const schema = S.Union([ - S.TaggedStruct("A", { a: S.NonEmptyString255 }), - S.TaggedStruct("B", { b: S.Finite }) - ]) - - it("generates _tag select and per-member fields with unionMeta", () => { - const { meta, unionMeta } = generateMetaFromSchema(schema) - - expect(meta["_tag"]?.type).toBe("select") - expect(meta["_tag"]?.required).toBe(true) - expect(meta["_tag"]?.members).toEqual(["A", "B"]) - expect(meta.a?.type).toBe("string") - expect(meta.b?.type).toBe("number") - expect(Object.keys(unionMeta).length).toBe(2) - expect(unionMeta["A"]).toBeDefined() - expect(unionMeta["B"]).toBeDefined() - }) - - it("unionMeta contains per-branch field metadata", () => { - const { unionMeta } = generateMetaFromSchema(schema) - - expect(unionMeta["A"]?.["a"]?.type).toBe("string") - expect(unionMeta["A"]?.["a"]?.required).toBe(true) - expect(unionMeta["B"]?.["b"]?.type).toBe("number") - expect(unionMeta["B"]?.["b"]?.required).toBe(true) - // Each branch should NOT contain the other branch's fields - expect(unionMeta["A"]?.["b"]).toBeUndefined() - expect(unionMeta["B"]?.["a"]).toBeUndefined() - }) - }) - - describe("literal union (select)", () => { - const schema = S.Struct({ - direction: S.Union([S.Literal("left"), S.Literal("right"), S.Literal("both")]) - }) - - it("generates select type with members", () => { - const { meta } = generateMetaFromSchema(schema) - expect(meta.direction?.type).toBe("select") - expect(meta.direction?.members).toEqual(["left", "right", "both"]) - }) - }) -}) - -// ============================================================================ -// 2. defaultsValueFromSchema — pin current behavior -// ============================================================================ - -describe("defaultsValueFromSchema", () => { - describe("primitive defaults", () => { - it("String defaults to empty string", () => { - const schema = S.Struct({ name: S.String }) - expect(defaultsValueFromSchema(schema)).toEqual({ name: "" }) - }) - - it("Boolean defaults to false", () => { - const schema = S.Struct({ active: S.Boolean }) - expect(defaultsValueFromSchema(schema)).toEqual({ active: false }) - }) - - it("NullOr defaults to null", () => { - const schema = S.Struct({ value: S.NullOr(S.String) }) - expect(defaultsValueFromSchema(schema)).toEqual({ value: null }) - }) - - it("UndefinedOr defaults include the key with undefined value", () => { - const schema = S.Struct({ value: S.UndefinedOr(S.String) }) - const result = defaultsValueFromSchema(schema) - expect("value" in result).toBe(true) - expect(result.value).toBeUndefined() - }) - }) - - describe("withConstructorDefault (effect-app wrapper)", () => { - it("respects constructor defaults", () => { - const schema = S.Struct({ - name: S.String.pipe(S.withConstructorDefault(Effect.succeed("hello"))), - count: S.Finite.pipe(S.withConstructorDefault(Effect.succeed(42))) - }) - const defaults = defaultsValueFromSchema(schema) - expect(defaults.name).toBe("hello") - expect(defaults.count).toBe(42) - }) - - it("constructor defaults in union members", () => { - const schema = S.Union([ - S.Struct({ - _tag: S.Literal("A").pipe(S.withConstructorDefault(Effect.succeed("A"))), - a: S.String.pipe(S.withConstructorDefault(Effect.succeed("defaultA"))) - }), - S.Struct({ - _tag: S.Literal("B"), - b: S.String - }) - ]) - const defaults = defaultsValueFromSchema(schema) - expect(defaults._tag).toBe("A") - expect(defaults.a).toBe("defaultA") - }) - }) - - describe("nested struct defaults", () => { - it("recursively extracts defaults from nested structs", () => { - const schema = S.Struct({ - outer: S.Struct({ - name: S.String.pipe(S.withConstructorDefault(Effect.succeed("nested"))), - value: S.Boolean - }) - }) - const defaults = defaultsValueFromSchema(schema) - expect(defaults.outer.name).toBe("nested") - expect(defaults.outer.value).toBe(false) - }) - }) - - describe("NullOr struct defaults", () => { - it("NullOr struct defaults to null", () => { - const schema = S.Struct({ - nested: S.NullOr(S.Struct({ x: S.String })) - }) - expect(defaultsValueFromSchema(schema)).toEqual({ nested: null }) - }) - }) -}) - -// ============================================================================ -// 3. withDecodingDefault — decoding defaults -// ============================================================================ - -describe("withDecodingDefault decoding defaults", () => { - it("withDecodingDefault fills value during decoding", () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("defaultName"))), - age: S.optionalKey(S.Finite).pipe(S.withDecodingDefault(Effect.succeed(0))) - }) - - const decoded = S.decodeUnknownSync(schema)({}) - expect(decoded).toEqual({ name: "defaultName", age: 0 }) - }) - - it("withDecodingDefault respects provided values", () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("defaultName"))) - }) - - const decoded = S.decodeUnknownSync(schema)({ name: "provided" }) - expect(decoded).toEqual({ name: "provided" }) - }) - - it("mixed withConstructorDefault and withDecodingDefault", () => { - const schema = S.Struct({ - constructorDefault: S.String.pipe(S.withConstructorDefault(Effect.succeed("fromConstructor"))), - decodingDefault: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("fromDecoding"))) - }) - - // defaultsValueFromSchema should pick up constructor default - const defaults = defaultsValueFromSchema(schema) - expect(defaults.constructorDefault).toBe("fromConstructor") - - // Decoding should pick up decoding default - const decoded = S.decodeUnknownSync(schema)({ constructorDefault: "x" }) - expect(decoded).toEqual({ constructorDefault: "x", decodingDefault: "fromDecoding" }) - }) - - it("defaultsValueFromSchema extracts withDecodingDefault alongside regular fields", () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("decodingDefault"))), - required: S.String - }) - - const defaults = defaultsValueFromSchema(schema) - expect(defaults.required).toBe("") - expect(defaults.name).toBe("decodingDefault") - }) - - it("withDecodingDefault can replace manual form default initialization", () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("John"))), - age: S.optionalKey(S.Finite).pipe(S.withDecodingDefault(Effect.succeed(25))), - active: S.optionalKey(S.Boolean).pipe(S.withDecodingDefault(Effect.succeed(true))) - }) - - // Single decode call produces all defaults — no AST walking needed - const defaults = S.decodeUnknownSync(schema)({}) - expect(defaults).toEqual({ name: "John", age: 25, active: true }) - - // Partial input fills in missing defaults - const partial = S.decodeUnknownSync(schema)({ name: "Dave" }) - expect(partial).toEqual({ name: "Dave", age: 25, active: true }) - }) -}) - -// ============================================================================ -// 4. Form-level integration tests — defaults flow end-to-end -// ============================================================================ - -describe("Form defaults integration", () => { - it("schema defaults flow into form values", async () => { - const schema = S.Struct({ - name: S.String.pipe(S.withConstructorDefault(Effect.succeed("hello"))), - active: S.Boolean, - nullable: S.NullOr(S.String) - }) - - const values = await mountAndGetDefaults(schema) - expect(values.name).toBe("hello") - expect(values.active).toBe(false) - expect(values.nullable).toBe(null) - }) - - it("union defaults flow into form values", async () => { - const schema = S.Union([ - S.Struct({ - _tag: S.Literal("A").pipe(S.withConstructorDefault(Effect.succeed("A"))), - value: S.String.pipe(S.withConstructorDefault(Effect.succeed("default"))) - }), - S.Struct({ - _tag: S.Literal("B"), - value: S.Finite - }) - ]) - - const values = await mountAndGetDefaults(schema) - expect(values._tag).toBe("A") - expect(values.value).toBe("default") - }) - - it("tanstack defaultValues override schema defaults", async () => { - const schema = S.Struct({ - name: S.String.pipe(S.withConstructorDefault(Effect.succeed("fromSchema"))), - age: S.Finite.pipe(S.withConstructorDefault(Effect.succeed(0))) - }) - - const values = await mountAndGetDefaults(schema, { - defaultValues: { name: "fromTanstack" } - }) - expect(values.name).toBe("fromTanstack") - expect(values.age).toBe(0) - }) -}) - -// ============================================================================ -// 5. Meta generation for number constraints -// ============================================================================ - -describe("number constraint metadata", () => { - it("separate isGreaterThanOrEqualTo + isLessThanOrEqualTo extracts min and max", () => { - const schema = S.Struct({ - value: S.Finite.pipe( - S.check(S.isGreaterThanOrEqualTo(10)), - S.check(S.isLessThanOrEqualTo(20)) - ) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.value?.type).toBe("number") - expect(meta.value?.minimum).toBe(10) - expect(meta.value?.maximum).toBe(20) - }) - - it("isBetween extracts min and max", () => { - const schema = S.Struct({ - value: S.Finite.pipe(S.check(S.isBetween({ minimum: 10, maximum: 20 }))) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.value?.type).toBe("number") - expect(meta.value?.minimum).toBe(10) - expect(meta.value?.maximum).toBe(20) - }) - - it("isInt extracts refinement", () => { - const schema = S.Struct({ - value: S.Finite.pipe(S.check(S.isInt())) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.value?.refinement).toBe("int") - }) - - it("isGreaterThan extracts exclusive minimum", () => { - const schema = S.Struct({ - value: S.Finite.pipe(S.check(S.isGreaterThan(5))) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.value?.exclusiveMinimum).toBe(5) - }) - - it("isLessThan extracts exclusive maximum", () => { - const schema = S.Struct({ - value: S.Finite.pipe(S.check(S.isLessThan(100))) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.value?.exclusiveMaximum).toBe(100) - }) -}) - -// ============================================================================ -// 6. Meta generation for string constraints -// ============================================================================ - -describe("string constraint metadata", () => { - it("isMinLength and isMaxLength", () => { - const schema = S.Struct({ - value: S.String.pipe(S.check(S.isMinLength(3)), S.check(S.isMaxLength(50))) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.value?.minLength).toBe(3) - expect(meta.value?.maxLength).toBe(50) - }) - - it("Email format detection", () => { - const schema = S.Struct({ - email: S.Email - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.email?.format).toBe("email") - expect(meta.email?.type).toBe("string") - }) -}) - -// ============================================================================ -// 7. TaggedStruct API -// ============================================================================ - -describe("TaggedStruct API", () => { - it("TaggedStruct creates struct with _tag literal", () => { - const ts = S.TaggedStruct("A", { a: S.String }) - expect(ts.fields._tag).toBeDefined() - expect(ts.fields.a).toBeDefined() - - const tagProp = ts.ast.propertySignatures.find((p: any) => p.name === "_tag") - expect(S.AST.isLiteral(tagProp!.type)).toBe(true) - expect((tagProp!.type as any).literal).toBe("A") - }) - - it("Union([TaggedStruct, TaggedStruct]) generates field metadata", () => { - const taggedSchema = S.Struct({ - union: S.Union([ - S.TaggedStruct("A", { a: S.NonEmptyString255 }), - S.TaggedStruct("B", { b: S.Finite }) - ]) - }) - - const { meta } = generateMetaFromSchema(taggedSchema) - - expect(meta["union.a"]?.type).toBe("string") - expect(meta["union.b"]?.type).toBe("number") - expect(meta["union._tag"]).toBeDefined() - }) - - it("NullOr(Union([TaggedStruct])) works for nullable discriminated unions", () => { - const schema = S.Struct({ - union: S.NullOr( - S.Union([ - S.TaggedStruct("A", { a: S.NonEmptyString255, common: S.String }), - S.TaggedStruct("B", { b: S.Finite, common: S.String }) - ]) - ) - }) - - const { meta } = generateMetaFromSchema(schema) - expect(meta["union._tag"]?.required).toBe(false) - expect(meta["union.a"]?.required).toBe(true) - expect(meta["union.b"]?.required).toBe(true) - }) - - it("TaggedStruct defaults work with withConstructorDefault", () => { - const schema = S.Union([ - S.TaggedStruct("A", { - a: S.String.pipe(S.withConstructorDefault(Effect.succeed("defaultA"))) - }), - S.TaggedStruct("B", { b: S.Finite }) - ]) - - const defaults = defaultsValueFromSchema(schema) - expect(defaults.a).toBe("defaultA") - }) - - it("TaggedStruct decoding works correctly", () => { - const schema = S.Union([ - S.TaggedStruct("A", { a: S.String }), - S.TaggedStruct("B", { b: S.Finite }) - ]) - - const decoded = S.decodeUnknownSync(schema)({ _tag: "A", a: "hello" }) - expect(decoded).toEqual({ _tag: "A", a: "hello" }) - - const decoded2 = S.decodeUnknownSync(schema)({ _tag: "B", b: 42 }) - expect(decoded2).toEqual({ _tag: "B", b: 42 }) - }) -}) - -// ============================================================================ -// 8. Array metadata -// ============================================================================ - -describe("array metadata", () => { - it("Array of primitives generates multiple type", () => { - const schema = S.Struct({ - tags: S.Array(S.String) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.tags?.type).toBe("multiple") - }) - - it("Array of structs generates nested field metadata", () => { - const schema = S.Struct({ - items: S.Array(S.Struct({ - name: S.NonEmptyString255, - value: S.Finite - })) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta["items.name"]).toBeDefined() - expect(meta["items.name"]?.type).toBe("string") - expect(meta["items.value"]?.type).toBe("number") - }) -}) - -// ============================================================================ -// 9. withDecodingDefault form integration -// ============================================================================ - -describe("withDecodingDefault form integration", () => { - it("withDecodingDefault defaults flow into form values", async () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("John"))), - age: S.optionalKey(S.Finite).pipe(S.withDecodingDefault(Effect.succeed(25))), - active: S.Boolean - }) - - const values = await mountAndGetDefaults(schema) - expect(values.name).toBe("John") - expect(values.age).toBe(25) - expect(values.active).toBe(false) - }) - - it("withDecodingDefault mixed with withConstructorDefault", async () => { - const schema = S.Struct({ - fromDecoding: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("decoding"))), - fromConstructor: S.String.pipe(S.withConstructorDefault(Effect.succeed("constructor"))), - plain: S.Boolean - }) - - const values = await mountAndGetDefaults(schema) - expect(values.fromDecoding).toBe("decoding") - expect(values.fromConstructor).toBe("constructor") - expect(values.plain).toBe(false) - }) - - it("tanstack defaultValues override withDecodingDefault", async () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("fromSchema"))), - other: S.optionalKey(S.Finite).pipe(S.withDecodingDefault(Effect.succeed(99))) - }) - - const values = await mountAndGetDefaults(schema, { - defaultValues: { name: "fromTanstack" } - }) - expect(values.name).toBe("fromTanstack") - expect(values.other).toBe(99) - }) -}) - -// ============================================================================ -// 10. Regression guards -// ============================================================================ - -describe("regression guards", () => { - it("isBetween extracts min and max metadata", () => { - const schema = S.Struct({ - value: S.Finite.pipe(S.check(S.isBetween({ minimum: 5, maximum: 15 }))) - }) - const { meta } = generateMetaFromSchema(schema) - expect(meta.value?.minimum).toBe(5) - expect(meta.value?.maximum).toBe(15) - }) - - it("withDecodingDefault extracted by defaultsValueFromSchema", () => { - const schema = S.Struct({ - name: S.optionalKey(S.String).pipe(S.withDecodingDefault(Effect.succeed("myDefault"))) - }) - const defaults = defaultsValueFromSchema(schema) - expect(defaults.name).toBe("myDefault") - }) -}) diff --git a/packages/vue-components/__tests__/setup.ts b/packages/vue-components/__tests__/setup.ts deleted file mode 100644 index 45a27c16d..000000000 --- a/packages/vue-components/__tests__/setup.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { config } from "@vue/test-utils" -import { useIntlKey } from "../src/utils" - -const mockUseIntl = () => - ({ - trans: (k: string) => `${k}_translated`, - formatMessage: ( - descriptor: { id?: string; defaultMessage?: string }, - _values?: Record - ) => `${descriptor.id ?? descriptor.defaultMessage ?? ""}_formatted` - }) as any - -config.global.provide = { - ...(config.global.provide ?? {}), - [useIntlKey]: mockUseIntl -} diff --git a/packages/vue-components/__tests__/test-array-meta.test.ts b/packages/vue-components/__tests__/test-array-meta.test.ts deleted file mode 100644 index ec3566229..000000000 --- a/packages/vue-components/__tests__/test-array-meta.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema } from "../src/components/OmegaForm" - -const schema = S.Struct({ - a: S.Array(S.Struct({ - b: S.String, - c: S.Array(S.Struct({ - d: S.String - })), - e: S.Array(S.Finite) - })), - aMultiple: S.Array(S.String), - aNullable: S.NullOr(S.Struct({ - a: S.String - })), - foo: S - .NullOr( - S.Struct({ - bar: S.UndefinedOr(S.Array(S.Struct({ - baz: S.String, - items: S.NonEmptyArray(S.Struct({ - baz: S.String, - value: S.Finite - })) - }))) - }) - ) -}) - -describe("test-array-meta", () => { - it("should generate metadata for arrays", () => { - const result = generateMetaFromSchema(schema) - console.log("Generated metadata:", JSON.stringify(result.meta, null, 2)) - console.log("Keys:", Object.keys(result.meta)) - - expect(result.meta).toHaveProperty("a.b") - expect(result.meta["a.b"].type).toBe("string") - expect(result.meta).toHaveProperty("a.c.d") - expect(result.meta["a.c.d"].type).toBe("string") - expect(result.meta).toHaveProperty("a.e") - expect(result.meta["a.e"].type).toBe("multiple") - expect(result.meta).toHaveProperty("aMultiple") - expect(result.meta["aMultiple"]?.type).toBe("multiple") - expect(result.meta).toHaveProperty("aNullable.a") - expect(result.meta["aNullable.a"]?.type).toBe("string") - expect(result.meta).not.toHaveProperty("aNullable") - expect(result.meta).toHaveProperty("foo.bar.items.baz") - expect(result.meta["foo.bar.items.baz"]?.type).toBe("string") - expect(result.meta).toHaveProperty("foo.bar.items.value") - expect(result.meta["foo.bar.items.value"]?.type).toBe("number") - expect(result.meta).toHaveProperty("foo.bar.baz") - expect(result.meta["foo.bar.baz"]?.type).toBe("string") - }) -}) diff --git a/packages/vue-components/__tests__/test-nullor-duplicate.test.ts b/packages/vue-components/__tests__/test-nullor-duplicate.test.ts deleted file mode 100644 index 9a39284e9..000000000 --- a/packages/vue-components/__tests__/test-nullor-duplicate.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema } from "../src/components/OmegaForm" - -// Test schema from Array.vue story -const schema = S.Struct({ - height: S - .NullOr( - S.Struct({ - left: S.String, - right: S.String, - internal: S.String - }) - ), - width: S.Struct({ - left: S.String, - right: S.String - }) -}) - -describe("test-nullor-duplicate", () => { - it("should not create duplicate height keys for NullOr struct", () => { - const result = generateMetaFromSchema(schema) - console.log("Generated metadata:", JSON.stringify(result.meta, null, 2)) - console.log("Keys:", Object.keys(result.meta)) - - // Should have nested fields for height - expect(result.meta).toHaveProperty("height.left") - expect(result.meta).toHaveProperty("height.right") - expect(result.meta).toHaveProperty("height.internal") - - // Should have nested fields for width - expect(result.meta).toHaveProperty("width.left") - expect(result.meta).toHaveProperty("width.right") - - // Should NOT have a parent "height" key (this was the duplicate issue) - expect(result.meta).not.toHaveProperty("height") - - // Regular structs also don't get parent keys, only their nested fields - expect(result.meta).not.toHaveProperty("width") - }) -}) diff --git a/packages/vue-components/__tests__/test-union-meta.test.ts b/packages/vue-components/__tests__/test-union-meta.test.ts deleted file mode 100644 index 5e11fe6cc..000000000 --- a/packages/vue-components/__tests__/test-union-meta.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as S from "effect-app/Schema" -import { describe, expect, it } from "vitest" -import { generateMetaFromSchema } from "../src/components/OmegaForm" - -const schema = S.Struct({ - mine: S.Struct({ their: S.Union([S.String, S.Struct({ yours: S.String })]) }) -}) - -describe("test-union-meta", () => { - it("should generate metadata for union", () => { - const result = generateMetaFromSchema(schema) - console.log("Generated metadata:", JSON.stringify(result.meta, null, 2)) - - expect(result.meta).toHaveProperty("mine.their") - expect(result.meta).toHaveProperty("mine.their.yours") - }) -}) diff --git a/packages/vue-components/__tests__/tsconfig.json b/packages/vue-components/__tests__/tsconfig.json deleted file mode 100644 index be9137dfa..000000000 --- a/packages/vue-components/__tests__/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": [ - "vite/client", - "vitest/globals" - ] - } -} diff --git a/packages/vue-components/docs/superpowers/plans/2026-04-27-omegaform-refactor.md b/packages/vue-components/docs/superpowers/plans/2026-04-27-omegaform-refactor.md deleted file mode 100644 index db53cdf8e..000000000 --- a/packages/vue-components/docs/superpowers/plans/2026-04-27-omegaform-refactor.md +++ /dev/null @@ -1,2655 +0,0 @@ -# OmegaForm Refactor Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Decompose `OmegaFormStuff.ts` (1422 lines) and `useOmegaForm.ts` (1022 lines) into focused modules, slim the schema-meta extraction, centralize validation onto a single localized form-level schema, and remove dead workarounds. Public API unchanged. - -**Architecture:** Five sequential phases. Phase 0 writes characterization tests against current behavior. Phase 1 splits files by concern (no behavior change). Phase 2 slims `createMeta` and adopts the v4 `format` annotation for email detection. Phase 3 centralizes validation and deletes the per-field schema and the errorMap-clearing workaround. Phase 4 is independent cleanup. Each task makes one self-contained, atomically committable change. - -**Tech Stack:** - -- TypeScript, Vue 3 (` -``` - -And replace the template's `` with: - -```vue - -``` - -Note the deletions: `:key="fieldKey"`, `:validators="{ ...validators, onSubmit: schema }"`, the `schema` computed, the `composeStandardSchemas` helper, the `hasIssues` helper, the `useIntl` call, the `fieldKey` computed. - -- [ ] **Step 2: Run tests + smoke** - -Run: `pnpm test:run` -Expected: green. - -Manually open the `IntegerValidationGerman` story and verify German validation messages still appear. They should — they now come from the form-level localized schema, not the per-field one. If they don't appear, debug Task 3.1. - -- [ ] **Step 3: Commit** - -```bash -git add src/components/OmegaForm/OmegaInput.vue -git commit -m "refactor(omegaform): delete per-field schema and fieldKey re-mount" -``` - ---- - -### Task 3.5: Delete `originalSchema` plumbing and `generateInputStandardSchemaFromFieldMeta` - -**Files:** - -- Modify: `src/components/OmegaForm/meta/types.ts` -- Modify: `src/components/OmegaForm/meta/createMeta.ts` -- Modify: `src/components/OmegaForm/validation/localized.ts` -- Modify: `src/components/OmegaForm/index.ts` -- Delete: `__tests__/OmegaForm/DiscriminatorFieldValidation.test.ts` - -- [ ] **Step 1: Delete the type field** - -In `meta/types.ts`, remove the `originalSchema?: StandardSchemaV1` field from `BaseFieldMeta`. - -- [ ] **Step 2: Delete the attachment plumbing** - -In `meta/createMeta.ts`, delete: - -- `attachOriginalSchemas` -- `toFieldStandardSchema` -- `fieldAstByPath` collection variable everywhere it's plumbed through `createMeta` / `walk` -- All call sites of `attachOriginalSchemas(...)` - -- [ ] **Step 3: Delete the per-field schema generator** - -In `validation/localized.ts`, delete `generateInputStandardSchemaFromFieldMeta` (~150 lines). - -- [ ] **Step 4: Update `index.ts`** - -Remove the export of `generateInputStandardSchemaFromFieldMeta`. - -- [ ] **Step 5: Delete `DiscriminatorFieldValidation.test.ts`** - -This test directly references `originalSchema` / `originalCodec` / `generateInputStandardSchemaFromFieldMeta` via `Reflect.get`. With those gone, the test is testing a deleted API. The form-level schema already validates discriminator values; that's covered by `TaggedUnionRoot.test.ts`'s `_tag` assertions plus the runtime story. - -```bash -git rm __tests__/OmegaForm/DiscriminatorFieldValidation.test.ts -``` - -- [ ] **Step 6: Run tests + tsc + lint** - -Run: `pnpm test:run && pnpm check && pnpm lint` -Expected: green. - -- [ ] **Step 7: Commit** - -```bash -git add -A -git commit -m "refactor(omegaform): delete originalSchema plumbing and per-field schema generator" -``` - ---- - -### Task 3.6: Phase 3 green-bar checkpoint - -- [ ] **Step 1: Full test suite** - -Run: `pnpm test:run && pnpm check && pnpm lint` -Expected: green. - -- [ ] **Step 2: Smoke check all five stories** - -Especially `IntegerValidationGerman` (proves form-level localization works) and `RootLevelTaggedUnion` (proves union meta still drives the right-required-flag-per-tag behavior). - -- [ ] **Step 3: Tag** - -```bash -git tag omegaform-refactor-phase3-green -``` - ---- - -## Phase 4 — Cleanup (each task is an independent commit) - -These tasks can be done in any order or interleaved with bug-fix work; they don't have inter-dependencies. - -### Task 4.1: Translate the German confirm dialog - -**Files:** - -- Modify: `src/components/OmegaForm/blockDialog.ts` -- Modify: `stories/OmegaForm.stories.ts` (add the translation key to the German mock) - -- [ ] **Step 1: Modify `blockDialog.ts`** - -Find the line: - -```ts -if (!confirm("Es sind ungespeicherte Änderungen vorhanden. Wirklich schließen?")) { -``` - -Replace with: - -```ts -const message = formatMessage({ - id: "form.unsaved_changes_confirm", - defaultMessage: "There are unsaved changes. Are you sure you want to close?" -}) -if (!confirm(message)) { -``` - -You'll need to inject `useIntl` at the top of the function: - -```ts -import { useIntl } from "../../utils" -``` - -(Or accept `formatMessage` as an optional dependency injected by the caller — match the existing pattern in `errors.ts`.) - -- [ ] **Step 2: Add the German translation in storybook decorator** - -In `stories/OmegaForm.stories.ts`, add to `germanTranslations`: - -```ts -"form.unsaved_changes_confirm": "Es sind ungespeicherte Änderungen vorhanden. Wirklich schließen?" -``` - -- [ ] **Step 3: Smoke-check** - -Open the `DialogBlockingExamples` story (with German locale toggled on) and trigger the confirm dialog by editing a field then attempting to close. Confirm the German text appears. - -- [ ] **Step 4: Commit** - -```bash -git add src/components/OmegaForm/blockDialog.ts stories/OmegaForm.stories.ts -git commit -m "i18n(omegaform): translate unsaved changes confirm dialog" -``` - ---- - -### Task 4.2: Drop deprecated string-typed fields _(conditional on consumer audit)_ - -**Files:** - -- Modify: `src/components/OmegaForm/types.ts` (formerly `OmegaFormStuff.ts`) -- Modify: `src/components/OmegaForm/useOmegaForm.ts` - -- [ ] **Step 1: Audit consumer apps** - -Run from the repo root: - -```bash -grep -rn 'overrideDefaultValues\|"deprecated:' apps/ 2>/dev/null -grep -rn 'defaultFromSchema' apps/ 2>/dev/null -grep -rn 'OmegaArrayProps.*items' apps/ 2>/dev/null -``` - -If any matches: skip this task, file an issue, keep the deprecated field one more cycle. - -If no matches: proceed. - -- [ ] **Step 2: Remove the deprecated string-typed fields** - -In `OmegaConfig` (in `useOmegaForm.ts` or wherever it landed): - -- Remove `overrideDefaultValues?: "deprecated: use defaultValuesPriority"` -- Remove `defaultFromSchema?: "deprecated: use defaultValuesPriority"` - -In `OmegaArrayProps`: - -- Remove `items?: "please use \`defaultItems\` instead"` - -- [ ] **Step 3: Run tests + tsc** - -Run: `pnpm test:run && pnpm check` -Expected: green. - -- [ ] **Step 4: Commit** - -```bash -git add -A -git commit -m "refactor(omegaform): drop deprecated string-typed config fields" -``` - ---- - -### Task 4.3: Drop legacy `field` slot in OmegaArray.vue _(conditional)_ - -**Files:** - -- Modify: `src/components/OmegaForm/OmegaArray.vue` - -- [ ] **Step 1: Audit consumer apps** - -```bash -grep -rn '

: P - -export const nominal: >() => Constructor = < - A extends B.Brand ->(): Constructor< - A -> => B.nominal() as any diff --git a/packages/effect-app/src/Schema/email.ts b/packages/effect-app/src/Schema/email.ts deleted file mode 100644 index 46cc36d28..000000000 --- a/packages/effect-app/src/Schema/email.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Refinement } from "effect-app/Function" -import { isValidEmail } from "effect-app/validation" -import * as S from "effect/Schema" -import type { Simplify } from "effect/Types" -import type { B } from "./schema.ts" -import type { NonEmptyStringBrand } from "./strings.ts" - -export interface EmailBrand extends Simplify> {} - -export type Email = string & EmailBrand - -export const Email = S - .String - .pipe( - S.annotate({ - title: "Email", - description: "an email according to RFC 5322", - format: "email" - }), - S.check(S.isMinLength(3), /* a@b */ S.isMaxLength(998)), - S.refine(isValidEmail as Refinement, { - identifier: "Email", - description: "an email according to RFC 5322", - // JSON Schema minLength counts code points; isMinLength(3) is UTF-16 units (ceil(3/2)=2). - jsonSchema: { format: "email", minLength: 2, maxLength: 998 }, - // Native Arbitrary cannot invert `isValidEmail`; without a constraint, rejection sampling - // discards (almost) every random string. - arbitraryConstraint: { - patterns: [{ source: "^[a-z]{3,10}@[a-z]{3,10}\\.(?:com|net|org|de)$", flags: "" }] - } - }) - ) diff --git a/packages/effect-app/src/Schema/ext.ts b/packages/effect-app/src/Schema/ext.ts deleted file mode 100644 index 788f96689..000000000 --- a/packages/effect-app/src/Schema/ext.ts +++ /dev/null @@ -1,681 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/** - * # `withConstructorDefault` policy - * - * The `withConstructorDefault` properties exported throughout this module - * (and from `numbers.ts`, `moreStrings.ts`, `ids.ts`) attach a default value - * that is **only** applied during construction — i.e. when the field is - * omitted from the input to a Schema constructor / `.make(...)` call. - * - * They are **NOT** applied during `decode` (JSON, database rows, RPC payloads, - * etc.). Decoding a payload with a missing field will still fail with a parse - * error, exactly as if the default were not present. - * - * Concretely this means `withConstructorDefault` MUST NOT be relied on as a - * just-in-time migration mechanism for database fields. If a stored record is - * missing a newly added field, the constructor default will not fill it in on - * read — decoding will fail. - * - * ## Don't reach for `withDecodingDefault*` either - * - * The sibling `withDecodingDefaultType` (and `withDecodingDefault`) extensions - * exist, but they are discouraged for migrating persisted data. A missing - * field in a stored record is just as likely to be data corruption as it is - * an old-shape document; silently substituting a default hides the problem - * and can poison downstream aggregates. - * - * Prefer an **explicit, preferably versioned** migration of database data - * (a schema-version field, a one-shot backfill, or a transform on read that - * is gated on an explicit version marker) over shoving missing fields under - * the rug with a decode-time default. - */ -import * as Config from "effect/Config" -import * as Effect from "effect/Effect" -import { pipe } from "effect/Function" -import * as Function from "effect/Function" -import * as Option from "effect/Option" -import * as Order from "effect/Order" -import * as S from "effect/Schema" -import * as SchemaIssue from "effect/SchemaIssue" -import * as SchemaTransformation from "effect/SchemaTransformation" -import * as Struct from "effect/Struct" -import { type NonEmptyReadonlyArray } from "../Array.ts" -import * as Context from "../Context.ts" -import * as SchemaAST from "../SchemaAST.ts" -import { extendM } from "../utils.ts" -import { type AST } from "./schema.ts" - -type ProvidedCodec = S.Codec< - Self["Type"], - Self["Encoded"], - Exclude, - Exclude -> - -const concurrencySetting = Effect.runSync( - Config - .Literal("unbounded", "SCHEMA_CONCURRENCY") - .pipe(Config.orElse(() => Config.Number("SCHEMA_CONCURRENCY")), Config.option) -) - -export const DefaultParseOptions: SchemaAST.ParseOptions = { - concurrency: Option.getOrElse(concurrencySetting, () => "unbounded" as const) -} - -/** - * Parse-options annotation used on schema constructors for decode paths where callers - * cannot currently pass parse options (notably some RPC / HttpApi integration paths). - * - * Keep this annotation in place so those framework-managed decodes still run with - * unbounded concurrency by default. - */ -export const concurrencyUnbounded = { parseOptions: DefaultParseOptions } as const - -type DecodeLike = (schema: any) => (input: any, options?: SchemaAST.ParseOptions) => any - -export const withDefaultParseOptions = ( - decode: Decode, - defaultParseOptions: SchemaAST.ParseOptions = DefaultParseOptions -): Decode => - ((schema: any) => { - const run = decode(schema) - return (input: any, options?: SchemaAST.ParseOptions) => run(input, { ...defaultParseOptions, ...options }) - }) as Decode - -const DateString = S.String.annotate({ - identifier: "DateOrInvalid", - description: "an ISO 8601 date string that will be decoded as a Date (may be invalid)", - format: "date-time" -}) - -/** - * Schema type for {@link DateFromString}. - * - * @category Schemas - * @since 4.0.0 - */ -export interface DateFromString extends S.decodeTo {} - -/** - * A transformation schema that parses an ISO 8601 string into a `Date`. - * - * Decoding: - * - A `string` is decoded as a `Date`. - * - * Encoding: - * - A `Date` is encoded as a `string`. - * - * Use this when the Encoded form must be JSON (`string`). Domain models should - * prefer {@link Date}, whose Encoded form is `Date`; JSON stores convert via - * `Schema.toCodecJson`. - * - * @since 4.0.0 - */ -export const DateFromString: DateFromString = DateString.pipe(S.decodeTo(S.Date, SchemaTransformation.dateFromString)) - -const dateHelpers = (s: S.Date) => ({ - /** - * Construction-only default `new Date()`. Applied only when the field is - * omitted from `.make(...)` input. NOT applied during decode — cannot be - * used to JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new globalThis.Date()))), - /** - * Decode-time default `new Date()`. **Discouraged for persisted data:** a - * missing field may be data corruption, not an old-shape document; silently - * substituting `new Date()` hides the problem. Prefer an explicit, - * preferably versioned migration over a decode-time fallback. See - * file-level note. - */ - withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new globalThis.Date()))) -}) - -/** Like the default Schema `Date` (Encoded is `Date`) with default helpers. */ -export const Date = extendM(S.Date, dateHelpers) - -/** Like the default Schema `Boolean` but with default helpers. */ -export const Boolean = Object.assign(S.Boolean, { - /** - * Construction-only default `false`. Applied only when the field is - * omitted from `.make(...)` input. NOT applied during decode — cannot be - * used to JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: S.Boolean.pipe(S.withConstructorDefault(Effect.succeed(false))), - /** - * Decode-time default `false`. **Discouraged for persisted data:** a - * missing field may be data corruption, not an old-shape document; silently - * substituting `false` hides the problem. Prefer an explicit, preferably - * versioned migration over a decode-time fallback. See file-level note. - */ - withDecodingDefaultType: S.Boolean.pipe(S.withDecodingDefaultType(Effect.succeed(false))) -}) - -/** - * You probably want to use `Finite` instead of this. Like the default Schema - * `Number` but with default helpers. - */ -export const Number = Object.assign(S.Number, { - /** - * Construction-only default `0`. Applied only when the field is omitted - * from `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: S.Number.pipe(S.withConstructorDefault(Effect.succeed(0))), - /** - * Decode-time default `0`. **Discouraged for persisted data:** a missing - * field may be data corruption, not an old-shape document; silently - * substituting `0` hides the problem. Prefer an explicit, preferably - * versioned migration over a decode-time fallback. See file-level note. - */ - withDecodingDefaultType: S.Number.pipe(S.withDecodingDefaultType(Effect.succeed(0))) -}) - -// Generation-only: native Arbitrary otherwise samples up to ±Number.MAX_VALUE, so -// derived totals overflow to Infinity. Validation and JSON Schema are unaffected. -const realisticFiniteSamples = S.makeFilter(() => undefined, { - arbitraryConstraint: { order: Order.Number, minimum: -1_000_000, maximum: 1_000_000 } -}) -const FiniteSchema = S.Finite.pipe(S.check(realisticFiniteSamples)) - -/** Like the default Schema `Finite` but with default helpers. */ -export const Finite = Object.assign(FiniteSchema, { - /** - * Construction-only default `0`. Applied only when the field is omitted - * from `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ - withConstructorDefault: FiniteSchema.pipe(S.withConstructorDefault(Effect.succeed(0))), - /** - * Decode-time default `0`. **Discouraged for persisted data:** a missing - * field may be data corruption, not an old-shape document; silently - * substituting `0` hides the problem. Prefer an explicit, preferably - * versioned migration over a decode-time fallback. See file-level note. - */ - withDecodingDefaultType: FiniteSchema.pipe(S.withDecodingDefaultType(Effect.succeed(0))) -}) - -/** Like the default Schema `Literals` but with default helpers. Default value is `literals[0]`. */ -export const Literals = >(literals: Literals) => - pipe( - S.Literals(literals), - (s) => - Object.assign(s, { - /** Override the default literal value used by `withConstructorDefault` / `withDecodingDefaultType`. */ - changeDefault: (a: A) => { - return Object.assign(S.Literals(literals), { - Default: a, - /** - * Construction-only default. Applied only when the field is - * omitted from `.make(...)` input. NOT applied during decode — - * cannot be used to JIT-migrate database fields. See file-level - * note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.succeed(a))), - /** - * Decode-time default. **Discouraged for persisted data:** a - * missing field may be data corruption, not an old-shape - * document; silently substituting hides the problem. Prefer an - * explicit, preferably versioned migration over a decode-time - * fallback. See file-level note. - */ - withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(a))) - }) // todo: copy annotations from original? - }, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- load-bearing: Object.assign widens the field type without it, breaking `expectTypeOf(l.Default).toEqualTypeOf<"a">()` in tests - Default: literals[0] as Literals[0], - /** - * Construction-only default `literals[0]`. Applied only when the - * field is omitted from `.make(...)` input. NOT applied during - * decode — cannot be used to JIT-migrate database fields. See - * file-level note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.succeed(literals[0]))), - /** - * Decode-time default `literals[0]`. **Discouraged for persisted - * data:** a missing field may be data corruption, not an old-shape - * document; silently substituting hides the problem. Prefer an - * explicit, preferably versioned migration over a decode-time - * fallback. See file-level note. - */ - withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(literals[0]))) - }) - ) - -/** Like the default Schema `Array` but with default helpers. */ -export function Array(value: ValueSchema) { - return pipe( - S.Array(value).annotate(concurrencyUnbounded), - (s) => - Object.assign(s, { - /** - * Construction-only default `[]`. Applied only when the field is - * omitted from `.make(...)` input. NOT applied during decode — - * cannot be used to JIT-migrate database fields. See file-level - * note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => []))), - /** - * Decode-time default `[]`. **Discouraged for persisted data:** a - * missing field may be data corruption, not an old-shape document; - * silently substituting `[]` hides the problem. Prefer an explicit, - * preferably versioned migration over a decode-time fallback. See - * file-level note. - */ - withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => []))) - }) - ) -} - -/** - * An annotated `S.Array` of unique items that decodes to a `ReadonlySet`. - */ -export const ReadonlySetFromArray = (value: ValueSchema) => { - const from = S - .Array(value) - .annotate({ ...concurrencyUnbounded, expected: "an array of unique items that will be decoded as a ReadonlySet" }) - const to = S.instanceOf(Set) as S.instanceOf> - const schema = from.pipe( - S.decodeTo( - to, - SchemaTransformation.transform({ - decode: (arr) => new Set(arr) as ReadonlySet, - encode: (set) => [...set] - }) - ) - ) - return schema -} - -/** - * An annotated `S.Array` of key-value tuples that decodes to a `ReadonlyMap`. - */ -export const ReadonlyMapFromArray = (pair: { - readonly key: KeySchema - readonly value: ValueSchema -}) => { - const from = S - .Array(S.Tuple([pair.key, pair.value])) - .annotate({ - ...concurrencyUnbounded, - expected: "an array of key-value tuples that will be decoded as a ReadonlyMap" - }) - const to = S.instanceOf(Map) as S.instanceOf< - ReadonlyMap - > - const schema = from.pipe( - S.decodeTo( - to, - SchemaTransformation.transform({ - decode: ( - arr - ) => new Map(arr) as ReadonlyMap, - encode: ( - map - ) => [...map.entries()] as any // fu - }) - ) - ) - return schema -} - -/** Like the default Schema `ReadonlySet` (Encoded is `Set`) with default helpers. */ -export const ReadonlySet = (value: ValueSchema) => - pipe( - S.ReadonlySet(value), - (s) => - Object.assign(s, { - /** - * Construction-only default `new Set()`. Applied only when the field - * is omitted from `.make(...)` input. NOT applied during decode — - * cannot be used to JIT-migrate database fields. See file-level - * note. - */ - withConstructorDefault: s.pipe( - S.withConstructorDefault(Effect.sync(() => new Set())) - ), - /** - * Decode-time default `new Set()`. **Discouraged for persisted - * data:** a missing field may be data corruption, not an old-shape - * document; silently substituting an empty set hides the problem. - * Prefer an explicit, preferably versioned migration over a - * decode-time fallback. See file-level note. - */ - withDecodingDefaultType: s.pipe( - S.withDecodingDefaultType(Effect.sync(() => new Set())) - ) - }) - ) - -/** Like the default Schema `ReadonlyMap` (Encoded is `Map`) with default helpers. */ -export const ReadonlyMap = (pair: { - readonly key: KeySchema - readonly value: ValueSchema -}) => - pipe( - S.ReadonlyMap(pair.key, pair.value), - (s) => - Object.assign(s, { - /** - * Construction-only default `new Map()`. Applied only when the field - * is omitted from `.make(...)` input. NOT applied during decode — - * cannot be used to JIT-migrate database fields. See file-level - * note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Map()))), - /** - * Decode-time default `new Map()`. **Discouraged for persisted - * data:** a missing field may be data corruption, not an old-shape - * document; silently substituting an empty map hides the problem. - * Prefer an explicit, preferably versioned migration over a - * decode-time fallback. See file-level note. - */ - withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new Map()))) - }) - ) - -/** Like the default Schema `NullOr` but with default helpers. */ -export const NullOr = (self: Schema) => - pipe( - S.NullOr(self), - (s) => - Object.assign(s, { - /** - * Construction-only default `null`. Applied only when the field is - * omitted from `.make(...)` input. NOT applied during decode — - * cannot be used to JIT-migrate database fields. See file-level - * note. - */ - withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.succeed(null))), - /** - * Decode-time default `null`. **Discouraged for persisted data:** a - * missing field may be data corruption, not an old-shape document; - * silently substituting `null` hides the problem. Prefer an - * explicit, preferably versioned migration over a decode-time - * fallback. See file-level note. - */ - withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(null))) - }) - ) - -/** - * Attach a `withConstructorDefault` of `new Date()` to any schema. - * - * **Construction-only.** Applied only when the field is omitted from - * `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ -export const defaultDate = (schema: Schema) => - schema.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))) - -/** - * Attach a `withConstructorDefault` of `false` to any schema. - * - * **Construction-only.** Applied only when the field is omitted from - * `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ -export const defaultBool = (schema: Schema) => - schema.pipe(S.withConstructorDefault(Effect.succeed(false))) - -/** - * Attach a `withConstructorDefault` of `null` to any schema. - * - * **Construction-only.** Applied only when the field is omitted from - * `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ -export const defaultNullable = (schema: Schema) => - schema.pipe(S.withConstructorDefault(Effect.succeed(null))) - -/** - * Attach a `withConstructorDefault` of `[]` to any schema. - * - * **Construction-only.** Applied only when the field is omitted from - * `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ -export const defaultArray = (schema: Schema) => - schema.pipe(S.withConstructorDefault(Effect.sync(() => []))) - -/** - * Attach a `withConstructorDefault` of `new Map()` to any schema. - * - * **Construction-only.** Applied only when the field is omitted from - * `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ -export const defaultMap = (schema: Schema) => - schema.pipe(S.withConstructorDefault(Effect.sync(() => new Map()))) - -/** - * Attach a `withConstructorDefault` of `new Set()` to any schema. - * - * **Construction-only.** Applied only when the field is omitted from - * `.make(...)` input. NOT applied during decode — cannot be used to - * JIT-migrate database fields. See file-level note. - */ -export const defaultSet = (schema: Schema) => - schema.pipe(S.withConstructorDefault(Effect.sync(() => new Set()))) - -export const withDefaultMake = (s: Self) => { - const a = Object.assign(S.decodeSync(s as any) as WithDefaults, s) - Object.setPrototypeOf(a, s) - return a - - // return s as Self & WithDefaults -} - -export type WithDefaults = ( - i: Self["Encoded"], - options?: SchemaAST.ParseOptions -) => Self["Type"] - -// type GetKeys = U extends Record ? K : never -// type UnionToIntersection2 = { -// readonly [K in GetKeys]: U extends Record ? T : never -// } - -// export type Test