diff --git a/.context/effect/.changeset/add-adaptive-rate-limiter-store.md b/.context/effect/.changeset/add-adaptive-rate-limiter-store.md new file mode 100644 index 000000000..c191add81 --- /dev/null +++ b/.context/effect/.changeset/add-adaptive-rate-limiter-store.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add adaptive consume and feedback operations to the unstable persistent RateLimiterStore API, including in-memory and Redis-backed bounded cooldown, learning, learned pacing, and expiry behavior for 429 Retry-After feedback. diff --git a/.context/effect/.changeset/add-values-unprepared.md b/.context/effect/.changeset/add-values-unprepared.md new file mode 100644 index 000000000..d0ac10cb9 --- /dev/null +++ b/.context/effect/.changeset/add-values-unprepared.md @@ -0,0 +1,17 @@ +--- +"effect": patch +"@effect/sql-clickhouse": patch +"@effect/sql-d1": patch +"@effect/sql-libsql": patch +"@effect/sql-mssql": patch +"@effect/sql-mysql2": patch +"@effect/sql-pg": patch +"@effect/sql-pglite": patch +"@effect/sql-sqlite-bun": patch +"@effect/sql-sqlite-do": patch +"@effect/sql-sqlite-node": patch +"@effect/sql-sqlite-react-native": patch +"@effect/sql-sqlite-wasm": patch +--- + +Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. diff --git a/.context/effect/.changeset/better-shrimps-follow.md b/.context/effect/.changeset/better-shrimps-follow.md new file mode 100644 index 000000000..95cb59640 --- /dev/null +++ b/.context/effect/.changeset/better-shrimps-follow.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +prefix opentelemetry modules to avoid collisions diff --git a/.context/effect/.changeset/calm-tracers-sample.md b/.context/effect/.changeset/calm-tracers-sample.md new file mode 100644 index 000000000..3bbb0ad84 --- /dev/null +++ b/.context/effect/.changeset/calm-tracers-sample.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Preserve sampling decisions when adapting generic Effect external spans to OpenTelemetry. diff --git a/.context/effect/.changeset/curvy-birds-float.md b/.context/effect/.changeset/curvy-birds-float.md new file mode 100644 index 000000000..e0a68dddc --- /dev/null +++ b/.context/effect/.changeset/curvy-birds-float.md @@ -0,0 +1,10 @@ +--- +"effect": patch +--- + +Update `Schema.Void` to model ignored `void` return values. + +Runtime parsing now accepts any present value and discards it as `undefined`. +This matches TypeScript `void` return values, where callers do not observe the +returned value. Use `Schema.Undefined` when the input must be exactly +`undefined`. diff --git a/.context/effect/.changeset/fix-2497.md b/.context/effect/.changeset/fix-2497.md new file mode 100644 index 000000000..4425db712 --- /dev/null +++ b/.context/effect/.changeset/fix-2497.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schedule.andThenResult` to emit `self` outputs as `Failure` and `other` outputs as `Success`, closes #2497. diff --git a/.context/effect/.changeset/fix-2499.md b/.context/effect/.changeset/fix-2499.md new file mode 100644 index 000000000..9e26f847e --- /dev/null +++ b/.context/effect/.changeset/fix-2499.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix excess property handling in schema-backed class constructors, closes #2499. diff --git a/.context/effect/.changeset/fix-anthropic-caller-toolid.md b/.context/effect/.changeset/fix-anthropic-caller-toolid.md new file mode 100644 index 000000000..866bc4dc8 --- /dev/null +++ b/.context/effect/.changeset/fix-anthropic-caller-toolid.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix non-streaming Anthropic responses throwing when a tool call carries `caller` metadata. The mapper emitted `caller.toolId: undefined`, but `ProviderMetadata` is `Record(String, NullOr(Json))` and `undefined` is not a valid Json value, so decoding the model's own response threw `Expected JSON value`. Emit `null` instead, matching the streaming mappers. diff --git a/.context/effect/.changeset/fix-config-array-default.md b/.context/effect/.changeset/fix-config-array-default.md new file mode 100644 index 000000000..129e3d182 --- /dev/null +++ b/.context/effect/.changeset/fix-config-array-default.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Config.schema` so missing array values are treated as missing data, allowing `Config.withDefault` to apply. diff --git a/.context/effect/.changeset/fix-cron-next-missing-day-overflow.md b/.context/effect/.changeset/fix-cron-next-missing-day-overflow.md new file mode 100644 index 000000000..6b392d177 --- /dev/null +++ b/.context/effect/.changeset/fix-cron-next-missing-day-overflow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cron.next` skipping earlier matching days when the upcoming day-of-month does not exist in the current month. diff --git a/.context/effect/.changeset/fix-from-json-string-identifier.md b/.context/effect/.changeset/fix-from-json-string-identifier.md new file mode 100644 index 000000000..b2e478fe7 --- /dev/null +++ b/.context/effect/.changeset/fix-from-json-string-identifier.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Preserve content schema identifiers when emitting JSON Schema for `Schema.fromJsonString`. + +This keeps user-defined identifiers attached to the decoded JSON payload while giving the generated JSON string wrapper its own derived name, avoiding client codegen outputs where the payload type is renamed behind the transport wrapper. diff --git a/.context/effect/.changeset/fix-http-tracer-response-cause.md b/.context/effect/.changeset/fix-http-tracer-response-cause.md new file mode 100644 index 000000000..30a5589df --- /dev/null +++ b/.context/effect/.changeset/fix-http-tracer-response-cause.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Exclude response metadata from HTTP server span failures after response headers have been sent. diff --git a/.context/effect/.changeset/fix-httpapi-malformed-json-400.md b/.context/effect/.changeset/fix-httpapi-malformed-json-400.md new file mode 100644 index 000000000..2f704e0e8 --- /dev/null +++ b/.context/effect/.changeset/fix-httpapi-malformed-json-400.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Map HttpApi json defects to SchemaError diff --git a/.context/effect/.changeset/fix-rpc-unknown-tag-isolation.md b/.context/effect/.changeset/fix-rpc-unknown-tag-isolation.md new file mode 100644 index 000000000..f8eb1b6bf --- /dev/null +++ b/.context/effect/.changeset/fix-rpc-unknown-tag-isolation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Localize missing rpc method errors to the provided request id diff --git a/.context/effect/.changeset/fix-schema-encoding-checks.md b/.context/effect/.changeset/fix-schema-encoding-checks.md new file mode 100644 index 000000000..ba56790cb --- /dev/null +++ b/.context/effect/.changeset/fix-schema-encoding-checks.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Fix Schema handling of encoded-side checks for container ASTs. + +Checks added after `flip` are now preserved as `encodingChecks` across +`Declaration`, `Arrays`, `Objects`, and `Union`, even when rebuilding the AST +does not change child nodes. `toType` now projects those checks consistently, +and parsing applies encoded-side checks to the local encoded value when an +encoding chain is present without allowing encoded-side `parseOptions` +annotations to affect the current parser side. diff --git a/.context/effect/.changeset/fix-string-case-digits.md b/.context/effect/.changeset/fix-string-case-digits.md new file mode 100644 index 000000000..5aaa2cd15 --- /dev/null +++ b/.context/effect/.changeset/fix-string-case-digits.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `String.camelCase` and `String.pascalCase` handling of numeric word segments, and add `String.configCase` for configuration key casing. diff --git a/.context/effect/.changeset/fix-structural-proto-equality.md b/.context/effect/.changeset/fix-structural-proto-equality.md new file mode 100644 index 000000000..eae88bf89 --- /dev/null +++ b/.context/effect/.changeset/fix-structural-proto-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix structural equality for request-style values when structural hashes collide. diff --git a/.context/effect/.changeset/fuzzy-crews-fold.md b/.context/effect/.changeset/fuzzy-crews-fold.md new file mode 100644 index 000000000..1bf7243e5 --- /dev/null +++ b/.context/effect/.changeset/fuzzy-crews-fold.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add custom error callbacks to Effect.fromOption. diff --git a/.context/effect/.changeset/major-chairs-design.md b/.context/effect/.changeset/major-chairs-design.md new file mode 100644 index 000000000..6e00b3ce6 --- /dev/null +++ b/.context/effect/.changeset/major-chairs-design.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix RequestResolver interruption diff --git a/.context/effect/.changeset/many-badgers-obey.md b/.context/effect/.changeset/many-badgers-obey.md new file mode 100644 index 000000000..6ec45f57f --- /dev/null +++ b/.context/effect/.changeset/many-badgers-obey.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow schemas provided to CLI flags / arguments to utilize the environment required by the CLI diff --git a/.context/effect/.changeset/neat-windows-buy.md b/.context/effect/.changeset/neat-windows-buy.md new file mode 100644 index 000000000..661cff0cf --- /dev/null +++ b/.context/effect/.changeset/neat-windows-buy.md @@ -0,0 +1,10 @@ +--- +"@effect/sql-mssql": patch +--- + +Added some options for the MssqlClient: + +- multiSubnetFailover +- cancelTimeout +- connectionRetryInterval +- maxRetriesOnTransientErrors diff --git a/.context/effect/.changeset/openapi-generator-sse-constraint-decoder.md b/.context/effect/.changeset/openapi-generator-sse-constraint-decoder.md new file mode 100644 index 000000000..358451002 --- /dev/null +++ b/.context/effect/.changeset/openapi-generator-sse-constraint-decoder.md @@ -0,0 +1,7 @@ +--- +"@effect/openapi-generator": patch +--- + +Fix the generated SSE `sseRequest` helper to reference `Schema.ConstraintDecoder` instead of the no-longer-exported `Schema.Decoder`. + +For specs with `text/event-stream` responses the generator emitted a helper typed as `Schema.Decoder`, but `Schema` exports that decode-only interface as `ConstraintDecoder`, so generated clients failed to compile (`'"effect/Schema"' has no exported member named 'Decoder'`). The emitted helper now uses `Schema.ConstraintDecoder`. diff --git a/.context/effect/.changeset/opentelemetry-render-causes.md b/.context/effect/.changeset/opentelemetry-render-causes.md new file mode 100644 index 000000000..4ebcd6ef3 --- /dev/null +++ b/.context/effect/.changeset/opentelemetry-render-causes.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Add `includeCauseInStack` option to `Cause.prettyErrors` diff --git a/.context/effect/.changeset/pre.json b/.context/effect/.changeset/pre.json index 16d94cb52..ceebab7cd 100644 --- a/.context/effect/.changeset/pre.json +++ b/.context/effect/.changeset/pre.json @@ -40,6 +40,7 @@ "@effect/jsdocs": "0.0.0" }, "changesets": [ + "add-adaptive-rate-limiter-store", "add-bigdecimal-sumall-multiplyall", "add-chunk-schema", "add-command-hidden", @@ -65,6 +66,7 @@ "add-standard-jsdoc-rule", "add-stream-broadcastn", "add-unstable-encoding-export", + "add-values-unprepared", "afraid-cobras-like", "ai-openai-config-field-leak", "ai-openai-file-nullable-fields", @@ -74,6 +76,7 @@ "beige-paths-sort", "better-apples-nail", "better-rocks-arrive", + "better-shrimps-follow", "big-pans-look", "blue-dingos-greet", "blue-ligers-cheat", @@ -101,6 +104,7 @@ "calm-panthers-nail", "calm-seas-smile", "calm-squids-hug", + "calm-tracers-sample", "chatty-poets-type", "chilled-mice-wash", "chilly-pumas-rule", @@ -128,6 +132,7 @@ "curly-poems-talk", "curly-spies-relax", "curvy-apples-float", + "curvy-birds-float", "custom-http-security-openapi-generator", "cute-heads-thank", "cyan-loops-grow", @@ -231,26 +236,34 @@ "fix-2384", "fix-2414", "fix-2419", + "fix-2497", + "fix-2499", "fix-ai-empty-params-structured-output", "fix-ai-text-toolkit-typing", + "fix-anthropic-caller-toolid", "fix-catch-orelse-error-erasure", "fix-class-constructor-defaults", "fix-cli-mixed-global-flag-context", + "fix-config-array-default", "fix-config-withDefault", "fix-config-withdefault-filter", + "fix-cron-next-missing-day-overflow", "fix-datetime-gmt", "fix-devtools-flush-on-teardown", "fix-duration-symmetric-rounding", "fix-entity-manager-defect-replay", "fix-entity-proxy-rpc-handler-context", "fix-entity-proxy-server-path-params", + "fix-from-json-string-identifier", "fix-from-readable-stream-cancel-defect", "fix-graph-undirected-traversal", "fix-has-interrupts-only-empty", "fix-hashmap-bit31-ordering", "fix-headers-proto-enumerability", "fix-http-incoming-message-parse-options", + "fix-http-tracer-response-cause", "fix-httpapi-endpoint-error-inference", + "fix-httpapi-malformed-json-400", "fix-httpapi-schema-types", "fix-httpapi-security-middleware-cache", "fix-is-json-dag", @@ -276,6 +289,7 @@ "fix-retry-transient-autocomplete", "fix-rpc-http-requestids-finalizer", "fix-rpc-json-id-edges", + "fix-rpc-unknown-tag-isolation", "fix-schedule-fixed-double-exec", "fix-schedule-reduce-sync-state", "fix-schema-arbitrary-exclusive-bounds", @@ -283,6 +297,7 @@ "fix-schema-encode-keys-property-keys", "fix-schema-encodekeys-class", "fix-schema-encodekeys-struct", + "fix-schema-encoding-checks", "fix-schema-identifier-expected-message", "fix-schema-is-uuid", "fix-schema-option-non-schema-failures", @@ -293,8 +308,10 @@ "fix-stream-run-for-each-while", "fix-stream-scan-effect", "fix-stream-scoped-scope", + "fix-string-case-digits", "fix-strip-approval-artifacts-multi-round", "fix-struct-utility-types-simplify", + "fix-structural-proto-equality", "fix-structwithrest-index-signatures", "fix-tagged-union-class-sentinels", "fix-tagged-union-match-unify", @@ -329,6 +346,7 @@ "funny-crabs-hang", "funny-forks-move", "fuzzy-camels-hunt", + "fuzzy-crews-fold", "fuzzy-dodos-help", "fuzzy-lions-perform", "fuzzy-planets-sneeze", @@ -383,6 +401,8 @@ "lucky-buttons-jump", "lucky-phones-listen", "lucky-worms-type", + "major-chairs-design", + "many-badgers-obey", "mean-dingos-share", "mean-trains-smash", "metal-parts-yell", @@ -395,6 +415,7 @@ "neat-lions-rest", "neat-snails-wash", "neat-taxis-notice", + "neat-windows-buy", "new-dogs-swim", "new-toes-stop", "ninety-geese-exist", @@ -413,7 +434,9 @@ "open-hotels-remain", "openai-compat-nullable-tool-name", "openai-compat-reasoning", + "openapi-generator-sse-constraint-decoder", "openrouter-input-audio", + "opentelemetry-render-causes", "otel-resource-env-precedence", "perfect-buckets-tickle", "petite-months-allow", @@ -424,6 +447,7 @@ "polite-pigs-speak", "polite-tables-kneel", "port-react-hydration", + "pretty-moments-clap", "public-deer-ring", "public-jeans-stop", "pubsub-publish-false", @@ -442,6 +466,8 @@ "quiet-lamps-jam", "quiet-radios-wave", "quiet-redis-scripts", + "quiet-tigers-yell", + "quiet-turtles-smile", "random-choice", "ready-olives-divide", "real-trains-ring", @@ -454,6 +480,7 @@ "remove-http-span-counter", "remove-nullor", "remove-openapi-fromapi-options", + "remove-schema-stringtree-keep-declarations", "remove-types-mergerecord", "remove-unused-utils-apis", "rename-rebuild-out", @@ -462,6 +489,7 @@ "rich-hoops-nail", "rich-sloths-draw", "ripe-lies-battle", + "rpc-client-http-early-close", "rpc-middleware-provides-fix", "scalar-custom-fetch", "schema-as-class", @@ -471,6 +499,7 @@ "schema-decoding-defaults-services", "schema-defaults-issue-channel", "schema-dollar-prefix", + "schema-lazy-bottom", "schema-missing-self-generic", "schema-ordered-arbitrary-constraints", "schema-parser-adapter-errors", @@ -485,6 +514,7 @@ "shaggy-birds-stay", "shaggy-cities-push", "shaggy-numbers-accept", + "shaky-beans-throw", "sharp-emus-applaud", "sharp-goats-wink", "sharp-pandas-care", @@ -494,6 +524,7 @@ "shiny-trains-hug", "short-cows-relate", "short-foxes-admire", + "short-stamps-throw", "shy-cycles-flow", "shy-geckos-sniff", "silent-geckos-matter", @@ -509,6 +540,8 @@ "six-cups-taste", "sixty-mails-shout", "sixty-socks-yell", + "slick-signs-wish", + "slick-toes-rush", "slimy-planets-divide", "slimy-turtles-juggle", "slow-beans-battle", @@ -533,6 +566,8 @@ "sparkly-coins-sit", "spotty-comics-fry", "sql-migrator-mjs-mts", + "sqlite-bun-prepare-error-channel", + "sqlite-do-durable-object-transactions", "stale-dots-tell", "stale-snakes-know", "strict-areas-end", @@ -571,6 +606,7 @@ "tiny-buckets-wave", "tiny-lilies-flash", "tiny-rabbits-smile", + "to-codec-json-schema", "tocodecjson-return-json-type", "tool-get-json-schema-tests", "true-actors-battle", @@ -581,6 +617,7 @@ "unify-error-defect-stack-options", "update-schema-arbitrary-report", "upset-colts-stick", + "use-url-can-parse", "vast-bananas-send", "vast-deserts-travel", "violet-peaches-feel", diff --git a/.context/effect/.changeset/pretty-moments-clap.md b/.context/effect/.changeset/pretty-moments-clap.md new file mode 100644 index 000000000..0dd186ea2 --- /dev/null +++ b/.context/effect/.changeset/pretty-moments-clap.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure handler errors don't cause httpapi security middleware to fallback diff --git a/.context/effect/.changeset/quiet-tigers-yell.md b/.context/effect/.changeset/quiet-tigers-yell.md new file mode 100644 index 000000000..3c567ce80 --- /dev/null +++ b/.context/effect/.changeset/quiet-tigers-yell.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Update `NodeHttpServer.layerConfig`'s type to report the same provided Node services as `NodeHttpServer.layer`. diff --git a/.context/effect/.changeset/quiet-turtles-smile.md b/.context/effect/.changeset/quiet-turtles-smile.md new file mode 100644 index 000000000..ef13d5bf9 --- /dev/null +++ b/.context/effect/.changeset/quiet-turtles-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Move `UrlParams.makeUrl` to `Url.make` and return `Url.UrlError` for URL construction failures. diff --git a/.context/effect/.changeset/remove-schema-stringtree-keep-declarations.md b/.context/effect/.changeset/remove-schema-stringtree-keep-declarations.md new file mode 100644 index 000000000..f09c22a1b --- /dev/null +++ b/.context/effect/.changeset/remove-schema-stringtree-keep-declarations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the `keepDeclarations` option from `Schema.toCodecStringTree`. diff --git a/.context/effect/.changeset/rpc-client-http-early-close.md b/.context/effect/.changeset/rpc-client-http-early-close.md new file mode 100644 index 000000000..ce2b353d6 --- /dev/null +++ b/.context/effect/.changeset/rpc-client-http-early-close.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fail RpcClient HTTP requests with a defect when the response stream closes before the request receives a terminal response. diff --git a/.context/effect/.changeset/schema-lazy-bottom.md b/.context/effect/.changeset/schema-lazy-bottom.md new file mode 100644 index 000000000..81ece1964 --- /dev/null +++ b/.context/effect/.changeset/schema-lazy-bottom.md @@ -0,0 +1,13 @@ +--- +"effect": patch +--- + +Improve Schema type-level performance by lazily computing schema views, +specializing common struct projections, and using lighter schema constraints at +API boundaries that do not need the full schema protocol. + +This also adds the Schema type-performance benchmark suite, introduces +`Schema.toCodecArrayFromSingle`, preserves canonical StringTree array codecs, +renames the arbitrary-generation annotation constraint for clarity, and updates +affected codec, parser, channel, SQL, HTTP API, persistence, RPC, AI, OpenAPI, +and workflow typings to match the refined Schema surface. diff --git a/.context/effect/.changeset/shaky-beans-throw.md b/.context/effect/.changeset/shaky-beans-throw.md new file mode 100644 index 000000000..e0fa124dd --- /dev/null +++ b/.context/effect/.changeset/shaky-beans-throw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +accept UrlParams.Input in some UrlParams apis diff --git a/.context/effect/.changeset/short-stamps-throw.md b/.context/effect/.changeset/short-stamps-throw.md new file mode 100644 index 000000000..1948e6788 --- /dev/null +++ b/.context/effect/.changeset/short-stamps-throw.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +do not use performance.timeOrigin and calculate origins lazily diff --git a/.context/effect/.changeset/slick-signs-wish.md b/.context/effect/.changeset/slick-signs-wish.md new file mode 100644 index 000000000..d7490cbb2 --- /dev/null +++ b/.context/effect/.changeset/slick-signs-wish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `isOpen` to `Latch` to allow querying the latch's open state diff --git a/.context/effect/.changeset/slick-toes-rush.md b/.context/effect/.changeset/slick-toes-rush.md new file mode 100644 index 000000000..ff74b18b5 --- /dev/null +++ b/.context/effect/.changeset/slick-toes-rush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +render causes in OtlpTracer exception events diff --git a/.context/effect/.changeset/sqlite-bun-prepare-error-channel.md b/.context/effect/.changeset/sqlite-bun-prepare-error-channel.md new file mode 100644 index 000000000..ab744c5f4 --- /dev/null +++ b/.context/effect/.changeset/sqlite-bun-prepare-error-channel.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-bun": patch +--- + +Fail with a typed `SqlError` when Bun SQLite statement preparation throws (for example a missing table or a syntax error), instead of letting the driver error escape as a defect, closes #2385. diff --git a/.context/effect/.changeset/sqlite-do-durable-object-transactions.md b/.context/effect/.changeset/sqlite-do-durable-object-transactions.md new file mode 100644 index 000000000..b0016a9fc --- /dev/null +++ b/.context/effect/.changeset/sqlite-do-durable-object-transactions.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-do": patch +--- + +Support Cloudflare Durable Object SQLite transactions by allowing `SqliteClient` to be configured with `DurableObjectStorage` and routing `withTransaction` through `storage.transaction`. diff --git a/.context/effect/.changeset/to-codec-json-schema.md b/.context/effect/.changeset/to-codec-json-schema.md new file mode 100644 index 000000000..2cee85ea0 --- /dev/null +++ b/.context/effect/.changeset/to-codec-json-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose the original input schema on `Schema.toType`, `Schema.toEncoded`, `Schema.toCodecJson`, and `Schema.toCodecStringTree` results via the `schema` property. This aligns these schema wrappers with other wrappers that retain their source schema for type-level and runtime introspection. diff --git a/.context/effect/.changeset/use-url-can-parse.md b/.context/effect/.changeset/use-url-can-parse.md new file mode 100644 index 000000000..959e2e641 --- /dev/null +++ b/.context/effect/.changeset/use-url-can-parse.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use `URL.canParse` to validate URL string schema decoding before constructing a `URL`. This avoids relying on thrown exceptions for routine validation while preserving the same invalid URL issue and successful decode output. diff --git a/.context/effect/.oxlintrc.json b/.context/effect/.oxlintrc.json index 998e52ea7..645b34173 100644 --- a/.context/effect/.oxlintrc.json +++ b/.context/effect/.oxlintrc.json @@ -7,6 +7,7 @@ "**/build", "**/docs", "**/.tsbuildinfo", + "packages/effect/typeperf/**/*", "packages/**/CHANGELOG.md", "!scratchpad/**/*", ".agents/**/*", diff --git a/.context/effect/cookbooks/schedule.md b/.context/effect/cookbooks/schedule.md index 3526494ce..084f286d4 100644 --- a/.context/effect/cookbooks/schedule.md +++ b/.context/effect/cookbooks/schedule.md @@ -1,3507 +1,735 @@ # Schedule Cookbook -Use this cookbook by problem shape: retry failed work, poll successful status values, repeat background work, or observe a schedule. - -## Before Choosing A Recipe - -- `Effect.retry` observes typed failures and stops on the first success. -- `Effect.repeat` observes successful values and stops on the first failure. -- Recurrence counts are follow-up runs after the first execution, so `Schedule.recurs(3)` allows three more attempts or repeats. -- Schedule output is policy output. Use `Schedule.passthrough` when a repeated program should return the latest successful value. -- Backoff, jitter, and spacing control timing. They do not classify errors, make writes idempotent, or add a stop condition by themselves. -- Bound request-path recurrence with a count, elapsed budget, or both. Leave unbounded schedules to explicitly owned background fibers. - -| Problem | Start with | -| ---------------------------------- | -------------------------------------------------------------------------------------------------- | -| Retry a flaky operation | `Effect.retry` plus `Schedule.exponential`, usually with `Schedule.recurs` and `Schedule.jittered` | -| Poll until a status is terminal | `Effect.repeat` plus `Schedule.spaced`, `Schedule.passthrough`, and `Schedule.while` | -| Run periodic background work | `Effect.repeat` plus `Schedule.spaced`, `Schedule.fixed`, `Schedule.windowed`, or `Schedule.cron` | -| Avoid overload across many callers | Add jitter, backoff, count limits, elapsed limits, and idempotency where side effects are retried | - -## Core Building Blocks - -### Exponential Backoff - -Create an Effect Schedule value named `retryBackoff` that uses exponential backoff starting at 100 milliseconds. +Use this cookbook when you need to define a `Schedule` value. The examples are +ordered from small single-purpose policies to larger real-world policies that +combine timing, input classification, output shaping, and observation. + +This cookbook intentionally defines schedules only. It does not apply them with +`Effect.retry`, `Effect.repeat`, streams, or channels. + +## Before Choosing A Schedule + +- `Schedule.recurs(n)` counts recurrences after the first run. +- `Schedule.spaced` waits after each completed run; `Schedule.fixed` uses an + aligned cadence; `Schedule.windowed` recurs on window boundaries. +- `Schedule.duration` performs exactly one recurrence after the duration. +- `Schedule.during` is an elapsed-time budget, not a delay by itself. +- Schedule output is policy output. Use `Schedule.passthrough`, + `Schedule.delays`, `Schedule.map`, or `Schedule.reduce` when the output shape + matters. +- `Schedule.both` continues only while both schedules continue. +- `Schedule.either` continues while either schedule can continue. +- `Schedule.jittered` spreads callers out. It does not add a recurrence limit. +- `Schedule.addDelay` adds extra delay based on schedule output. +- `Schedule.modifyDelay` replaces or adjusts the selected delay. +- Leave unbounded schedules to explicitly owned background work. + +## Choose By Problem Shape + +| Problem shape | Start with | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Bounded retry | `Schedule.exponential` or `Schedule.fibonacci`, then `Schedule.recurs` and optionally `Schedule.during` | +| Poll latest status | `Schedule.spaced` or `Schedule.fixed`, then `Schedule.setInputType`, `Schedule.passthrough`, and `Schedule.while` | +| Inspect selected delays | `Schedule.delays`, then `Schedule.map` | +| Adapt delay from output | `Schedule.addDelay` | +| Replace or cap selected delay | `Schedule.modifyDelay` | +| Run phases in sequence | `Schedule.andThen` | +| Preserve phase in output | `Schedule.andThenResult` | +| Continue while both policies continue | `Schedule.both` | +| Continue while either policy continues | `Schedule.either` | +| Keep input or output history | `Schedule.collectInputs`, `Schedule.collectOutputs`, or `Schedule.collectWhile` | +| Maintain a running aggregate | `Schedule.reduce` | +| Observe decisions without changing output | `Schedule.tap`, `Schedule.tapInput`, or `Schedule.tapOutput` | +| Build a local state machine | `Schedule.unfold` | + +## Table Of Contents + +1. [Single-Policy Schedules](#single-policy-schedules) +2. [Shape Schedule Outputs](#shape-schedule-outputs) +3. [Combine Policies](#combine-policies) +4. [Work With Inputs](#work-with-inputs) +5. [Accumulate State](#accumulate-state) +6. [Adapt Delays](#adapt-delays) +7. [Observe Schedule Decisions](#observe-schedule-decisions) +8. [Build Local State Machines](#build-local-state-machines) +9. [Realistic Policies](#realistic-policies) + +## Single-Policy Schedules + +### Retry A Profile Fetch Three Times + +Goal: Create a retry policy for loading a user profile that allows at most 3 +recurrences and outputs the recurrence count. ```ts import { Schedule } from "effect" -const retryBackoff = Schedule.exponential("100 millis") +const profileRetry = Schedule.recurs(3) ``` -### Limit Recurrences +### Poll A Queue Once Per Second -Create an Effect Schedule value named `threeRetries` that allows at most 3 recurrences. +Goal: Create a cadence for polling queue depth about 1 second after each +completed check. ```ts import { Schedule } from "effect" -const threeRetries = Schedule.recurs(3) +const queueDepthPolling = Schedule.spaced("1 second") ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +### Run A Heartbeat On An Aligned Cadence -### Space Repetitions - -Create an Effect Schedule value named `pollEverySecond` that recurs about once per second after each completed run. +Goal: Create a heartbeat cadence that runs on aligned 30-second boundaries +instead of waiting 30 seconds after each run finishes. ```ts import { Schedule } from "effect" -const pollEverySecond = Schedule.spaced("1 second") +const heartbeatCadence = Schedule.fixed("30 seconds") ``` -### Use Fixed Cadence +### Warm A Cache Once After Deployment -Create an Effect Schedule value named `heartbeatCadence` that runs on an aligned 30 second cadence instead of waiting 30 seconds after each run finishes. +Goal: Create a follow-up cache warmup that recurs exactly once after about 1 +minute. ```ts import { Schedule } from "effect" -const heartbeatCadence = Schedule.fixed("30 seconds") +const cacheWarmupFollowUp = Schedule.duration("1 minute") ``` -### Run One Follow-Up +### Keep A Health Probe Inside A Time Budget -Create an Effect Schedule value named `oneFollowUpRun` that performs exactly one additional recurrence after about 10 seconds. +Goal: Create a budget that allows health probes to continue only while about 1 +minute has elapsed or less. ```ts import { Schedule } from "effect" -const oneFollowUpRun = Schedule.duration("10 seconds") +const healthProbeBudget = Schedule.during("1 minute") ``` -### Stop After Elapsed Time +### Retry A Config Fetch With Exponential Backoff -Create an Effect Schedule value named `thirtySecondBudget` that can be used to stop repetitions or retries after about 30 seconds have elapsed. +Goal: Create a retry policy for fetching remote configuration, starting with a +100 millisecond exponential backoff. ```ts import { Schedule } from "effect" -const thirtySecondBudget = Schedule.during("30 seconds") +const configFetchBackoff = Schedule.exponential("100 millis") ``` -### Add Jitter +### Probe A Cold Replica With Fibonacci Backoff -Create an Effect Schedule value named `jitteredBackoff` that starts with 100 ms exponential backoff and adds a little randomness to each delay. +Goal: Create a gentler startup probe for a cold search replica, using Fibonacci +backoff and taking the first 4 outputs. ```ts import { Schedule } from "effect" -const jitteredBackoff = Schedule.exponential("100 millis").pipe( - Schedule.jittered +const searchReplicaWarmup = Schedule.fibonacci("100 millis").pipe( + Schedule.take(4) ) ``` -Explanation: jitter randomly changes each computed delay a little. This helps when many callers fail at the same time, because they will not all retry again at the exact same moment. - -Caution: jitter spreads delay across callers; it does not add a retry limit. +### Run A Worker Loop Forever -### Combine Backoff With Count And Time Limits - -Create an Effect Schedule value named `boundedBackoff` that uses exponential backoff starting at 100 ms, allows at most 5 recurrences, and also stops after about 10 seconds have elapsed. +Goal: Create an unbounded worker-loop counter with no added delay. ```ts import { Schedule } from "effect" -const boundedBackoff = Schedule.exponential("100 millis").pipe( - Schedule.both(Schedule.recurs(5)), - Schedule.both(Schedule.during("10 seconds")) -) +const workerLoopCounter = Schedule.forever ``` -Explanation: `Schedule.both` runs both schedules against the same input. The combined schedule continues only while both schedules continue. Its output is a pair, which matters when another operator reads the schedule output. - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +### Trigger A Nightly Report -### Keep The Left Schedule Output - -Create an Effect Schedule value named `limitedBackoff` that combines exponential backoff starting at 100 ms with a maximum of 5 recurrences, while keeping the backoff delay as the schedule output. +Goal: Create a schedule for a nightly billing report at 02:00. ```ts import { Schedule } from "effect" -const limitedBackoff = Schedule.exponential("100 millis").pipe( - Schedule.bothLeft(Schedule.recurs(5)) -) +const nightlyBillingReport = Schedule.cron("0 2 * * *") ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +### Sample Five-Minute Windows -### Keep The Right Schedule Output - -Create an Effect Schedule value named `retryAttemptCount` that combines exponential backoff starting at 100 ms with a maximum of 5 recurrences, while keeping the recurrence count as the schedule output. +Goal: Create a schedule that recurs on 5-minute window boundaries. ```ts import { Schedule } from "effect" -const retryAttemptCount = Schedule.exponential("100 millis").pipe( - Schedule.bothRight(Schedule.recurs(5)) -) +const fiveMinuteWindows = Schedule.windowed("5 minutes") ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +## Shape Schedule Outputs -### Return The Latest Input +### Echo Feature Flag Inputs -Create an Effect Schedule value named `keepLatestStatus` that checks about once per second and outputs the latest successful `Status` value instead of the schedule's internal counter. +Goal: Create a schedule for feature flag snapshots that immediately outputs +each input unchanged and takes the first 3 samples. ```ts import { Schedule } from "effect" -type Status = "Pending" | "Done" +type FeatureFlagSnapshot = { readonly enabled: boolean } -const keepLatestStatus = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough +const featureFlagSamples = Schedule.identity().pipe( + Schedule.take(3) ) ``` -Explanation: `Schedule.setInputType` tells TypeScript what kind of input the schedule receives. `Schedule.passthrough` changes the schedule output to the latest input, so `Effect.repeat` can return the latest successful value instead of the schedule's counter or delay. - -### Continue While Input Matches +### Label Retry Attempts -Create an Effect Schedule value named `pollWhilePending` that checks about once per second, keeps returning the latest successful `Status`, and stops when the latest status is no longer `Pending`. +Goal: Create a retry-attempt schedule that turns recurrence counts into labels +such as `attempt-1`, `attempt-2`, and `attempt-3`. ```ts import { Schedule } from "effect" -type Status = "Pending" | "Done" - -const pollWhilePending = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input === "Pending") +const retryAttemptLabels = Schedule.recurs(3).pipe( + Schedule.map((count) => `attempt-${count + 1}`) ) ``` -### Run One Policy Then Another +### Report Selected Poll Delays -Create an Effect Schedule value named `warmThenSteady` that recurs 3 times 100 ms apart, then switches to recurring every 5 seconds. +Goal: Create a telemetry schedule that runs on an aligned 2-second cadence, +outputs selected delays, maps each delay to milliseconds, and takes 3 outputs. ```ts -import { Schedule } from "effect" +import { Duration, Schedule } from "effect" -const warmThenSteady = Schedule.spaced("100 millis").pipe( - Schedule.bothLeft(Schedule.recurs(3)), - Schedule.andThen(Schedule.spaced("5 seconds")) +const pollDelayReport = Schedule.fixed("2 seconds").pipe( + Schedule.delays, + Schedule.map((delay) => ({ millis: Duration.toMillis(delay) })), + Schedule.take(3) ) ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +Explanation: `Schedule.delays` changes the schedule output to the selected +delay. Use it when observability or downstream policy needs the actual delay +rather than the cadence counter. -### Retry With A Schedule +### Report Elapsed Runtime -Write the Effect code that retries `request` with exponential backoff starting at 100 ms and at most 3 retries. +Goal: Create a runtime sampler that outputs elapsed time in milliseconds and +takes 4 samples. ```ts -import { Effect, Schedule } from "effect" - -type RequestError = unknown +import { Duration, Schedule } from "effect" -declare const request: Effect.Effect - -const retryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.both(Schedule.recurs(3)) +const elapsedRuntimeReport = Schedule.elapsed.pipe( + Schedule.map((elapsed) => ({ millis: Duration.toMillis(elapsed) })), + Schedule.take(4) ) - -const program = request.pipe(Effect.retry(retryPolicy)) ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +## Combine Policies -### Repeat With A Schedule +### Add Jitter To Webhook Backoff -Write the Effect code that repeats `refresh` after each successful run, waiting about 1 minute between runs. +Goal: Create a webhook retry backoff that starts at 200 milliseconds, adds +jitter, outputs selected delays, and takes 3 outputs. ```ts -import { Effect, Schedule } from "effect" - -type RefreshError = unknown - -declare const refresh: Effect.Effect - -const refreshCadence = Schedule.spaced("1 minute") +import { Schedule } from "effect" -const program = refresh.pipe(Effect.repeat(refreshCadence)) +const jitteredWebhookBackoff = Schedule.exponential("200 millis").pipe( + Schedule.jittered, + Schedule.delays, + Schedule.take(3) +) ``` -Caution: `Effect.repeat` observes successful values and stops on the first failure. +### Stop Deployment Hook Retries By Count And Time -### Retry Only Selected Failures - -Write the Effect code that retries `callService` at most 3 times, but only when the failure is `Transient`. +Goal: Create a deployment hook retry budget that uses jittered exponential +backoff, allows at most 5 recurrences, and also stops after about 20 seconds. ```ts -import { Effect, Schedule } from "effect" - -type ServiceResponse = unknown - -type ServiceError = - | { readonly _tag: "Transient" } - | { readonly _tag: "Permanent" } - -declare const callService: Effect.Effect +import { Schedule } from "effect" -const program = callService.pipe( - Effect.retry({ - schedule: Schedule.recurs(3), - while: (error) => error._tag === "Transient" - }) +const deploymentHookRetryBudget = Schedule.exponential("200 millis").pipe( + Schedule.jittered, + Schedule.both(Schedule.recurs(5)), + Schedule.both(Schedule.during("20 seconds")) ) ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +Explanation: the resulting output is nested because `Schedule.both` preserves +both schedule outputs. Use `Schedule.map` when a smaller output is needed. -### Repeat Until A Successful Value Matches +### Continue While Either Probe Is Active -Write the Effect code that repeats `readStatus` about once per second until the successful status is `Complete`. +Goal: Create a service readiness policy that continues while either 2 immediate +warmup probes or a slower 500 millisecond probe schedule still wants to recur. ```ts -import { Effect, Schedule } from "effect" - -type StatusError = unknown - -type Status = "Pending" | "Complete" - -declare const readStatus: Effect.Effect +import { Schedule } from "effect" -const program = readStatus.pipe( - Effect.repeat({ - schedule: Schedule.spaced("1 second"), - until: (status) => status === "Complete" - }) +const readinessWarmupOrSlowProbe = Schedule.recurs(2).pipe( + Schedule.either(Schedule.spaced("500 millis").pipe(Schedule.take(5))) ) ``` -Caution: `Effect.repeat` observes successful values and stops on the first failure. +Explanation: `Schedule.either` keeps recurring while at least one side can +continue. Its output preserves both sides, so map the result if callers should +see a smaller shape. -### Observe Schedule Input +### Warm Up Fast, Then Settle Into Maintenance -Create an Effect Schedule value named `loggingRetryPolicy` that uses exponential backoff starting at 100 ms and logs each retry input error tag before the delay is selected. +Goal: Create a cache invalidation sequence that runs 2 quick recurrences 100 +milliseconds apart, then 3 slower recurrences 30 seconds apart, then stops. ```ts -import { Console, Schedule } from "effect" - -type RequestError = { readonly _tag: string } +import { Schedule } from "effect" -const loggingRetryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.tapInput((error: RequestError) => Console.log(error._tag)) +const cacheInvalidationSequence = Schedule.spaced("100 millis").pipe( + Schedule.take(2), + Schedule.andThen(Schedule.spaced("30 seconds").pipe(Schedule.take(3))) ) ``` -### Observe Schedule Output +### Preserve Warmup And Steady Phases -Create an Effect Schedule value named `measuredBackoff` that uses exponential backoff starting at 100 ms and records each selected retry delay. +Goal: Create a retry classifier with a fast exponential phase and a steady +Fibonacci phase, preserving the phase in the output. ```ts -import { Duration, Effect, Schedule } from "effect" - -declare const Metrics: { - readonly recordRetryDelay: (delay: Duration.Duration) => Effect.Effect -} +import { Result, Schedule } from "effect" -const measuredBackoff = Schedule.exponential("100 millis").pipe( - Schedule.tapOutput((delay) => Metrics.recordRetryDelay(delay)) +const phasedRetryClassifier = Schedule.exponential("100 millis").pipe( + Schedule.take(2), + Schedule.andThenResult(Schedule.fibonacci("500 millis").pipe(Schedule.take(3))), + Schedule.map((result) => + Result.match(result, { + onFailure: (delay) => ({ phase: "steady", delay }), + onSuccess: (delay) => ({ phase: "fast", delay }) + }) + ) ) ``` -### Run On Time Windows - -Create an Effect Schedule value named `minuteWindow` that recurs on one-minute window boundaries. - -```ts -import { Schedule } from "effect" +Explanation: `Schedule.andThenResult` keeps phase information in the output. +The first schedule is represented by the success side, and the second schedule +is represented by the failure side. -const minuteWindow = Schedule.windowed("1 minute") -``` +## Work With Inputs -### Run From A Cron Expression +### Poll Upload Progress Until Complete -Create an Effect Schedule value named `nightlyAtTwo` that recurs every night at 02:00 using a cron expression. +Goal: Create an upload-progress schedule that waits about 1 second between +checks, outputs the latest progress object, and continues only while the upload +is incomplete. ```ts import { Schedule } from "effect" -const nightlyAtTwo = Schedule.cron("0 2 * * *") -``` - -## Retry And Recovery +type UploadProgress = { readonly percent: number } -### Retry HTTP GET Timeouts - -Write the Effect code that retries `getUser("user-123")` only when it fails with `HttpTimeout`. - -```ts -import { Effect, Schedule } from "effect" - -type User = unknown - -type HttpTimeout = { readonly _tag: "HttpTimeout" } -type HttpStatusError = { readonly _tag: "HttpStatusError"; readonly status: number } -type DecodeError = { readonly _tag: "DecodeError" } - -declare const getUser: ( - id: string -) => Effect.Effect - -const isHttpTimeout = ( - error: HttpTimeout | HttpStatusError | DecodeError -): error is HttpTimeout => error._tag === "HttpTimeout" - -const retryGetUserTimeouts = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(3)), - Schedule.both(Schedule.during("2 seconds")) -) - -const program = getUser("user-123").pipe( - Effect.retry({ - schedule: retryGetUserTimeouts, - while: isHttpTimeout - }) +const uploadProgressUntilComplete = Schedule.spaced("1 second").pipe( + Schedule.setInputType(), + Schedule.passthrough, + Schedule.while(({ input }) => input.percent < 100) ) ``` -Caution: jitter spreads delay across callers; it does not add a retry limit. +Explanation: `Schedule.setInputType` tells TypeScript which input each step +receives. `Schedule.passthrough` makes the output the latest input, so a polling +schedule can return the final status instead of a counter. -### Retry An Idempotent Order Submission +### Keep A History Of Retry Error Codes -Write the Effect code that retries a temporarily failed order submission. +Goal: Create a schedule for string error-code inputs that checks about once per +second and outputs all input codes seen so far. ```ts -import { Effect, Schedule } from "effect" - -type Order = unknown - -type PostOrderError = - | { readonly _tag: "Timeout" } - | { readonly _tag: "ConnectionReset" } - | { readonly _tag: "BadGateway" } - | { readonly _tag: "ServiceUnavailable" } - | { readonly _tag: "RateLimited" } - | { readonly _tag: "InvalidRequest" } - | { readonly _tag: "Unauthorized" } - -declare const postOrder: ( - request: { - readonly customerId: string - readonly sku: string - readonly quantity: number - readonly idempotencyKey: string - } -) => Effect.Effect - -const idempotencyKey = "order-key-123" - -const isRetryablePostOrderError = (error: PostOrderError): boolean => { - switch (error._tag) { - case "Timeout": - case "ConnectionReset": - case "BadGateway": - case "ServiceUnavailable": - case "RateLimited": - return true - case "InvalidRequest": - case "Unauthorized": - return false - } -} - -const retryOrderSubmission = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(4)), - Schedule.both(Schedule.during("3 seconds")) -) +import { Schedule } from "effect" -const program = postOrder({ - customerId: "customer-1", - sku: "sku-1", - quantity: 2, - idempotencyKey -}).pipe( - Effect.retry({ - schedule: retryOrderSubmission, - while: isRetryablePostOrderError - }) +const retryErrorCodeHistory = Schedule.collectInputs( + Schedule.spaced("1 second").pipe( + Schedule.setInputType() + ) ) ``` -Caution: the schedule only times retries; the stable idempotency key is what makes duplicate attempts safe. +Explanation: `Schedule.collectInputs` is useful when the input stream is the +thing to audit, such as retry errors or status samples. -### Retry HTTP GET 503 Responses +### Collect Deployment Phases Until Failure -Write the Effect code that retries `getCatalog("https://api.example.test/catalog")` only when the error value is an HTTP `GET` response with status `503`. +Goal: Create a schedule that outputs the collected deployment phases while the +input phase is not `failed`. ```ts -import { Effect, Schedule } from "effect" +import { Schedule } from "effect" -type TransportError = { readonly _tag: "TransportError" } -type HttpResponseError = { - readonly _tag: "HttpResponseError" - readonly method: "GET" | "POST" - readonly status: number +type DeploymentPhase = { + readonly phase: "created" | "deploying" | "verifying" | "failed" } -type GetCatalogError = TransportError | HttpResponseError - -declare const getCatalog: ( - url: string -) => Effect.Effect - -const isServiceUnavailableGet = (error: GetCatalogError): boolean => - error._tag === "HttpResponseError" && - error.method === "GET" && - error.status === 503 - -const retry503WithBackoff = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(4)), - Schedule.both(Schedule.during("3 seconds")) -) -const program = getCatalog("https://api.example.test/catalog").pipe( - Effect.retry({ - schedule: retry503WithBackoff, - while: isServiceUnavailableGet - }) +const deploymentPhaseHistory = Schedule.identity().pipe( + Schedule.collectWhile(({ input }) => input.phase !== "failed") ) ``` -Caution: jitter spreads delay across callers; it does not add a retry limit. +Explanation: `Schedule.collectWhile` accumulates accepted outputs and stops +collecting when the predicate is false. This is cheaper to read than manually +combining `identity`, `while`, and `reduce` for simple histories. -### Respect Retry-After On Rate Limits +### Collect Heartbeat Counts -Write the Effect code that retries `callApi` only for `RateLimited` errors. +Goal: Create a schedule that recurs forever, collects recurrence counts produced +so far, and takes only the first 3 collected outputs. ```ts -import { Duration, Effect, Schedule } from "effect" - -type HttpError = - | { readonly _tag: "RateLimited"; readonly retryAfter: Duration.Duration | undefined } - | { readonly _tag: "Unauthorized" | "Forbidden" | "BadRequest" | "Unavailable" } - -declare const callApi: Effect.Effect +import { Schedule } from "effect" -const rateLimitPolicy = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.identity()), - Schedule.modifyDelay(([_, error], delay) => - Effect.succeed( - error._tag === "RateLimited" && error.retryAfter !== undefined - ? Duration.max(delay, error.retryAfter) - : delay - ) - ), - Schedule.both(Schedule.recurs(5)), - Schedule.while(({ input }) => input._tag === "RateLimited") +const heartbeatCountHistory = Schedule.collectOutputs(Schedule.forever).pipe( + Schedule.take(3) ) - -const program = Effect.retry(callApi, rateLimitPolicy) ``` -Explanation: `Schedule.identity()` keeps the retry error available as schedule output. `Schedule.modifyDelay` can then compare the normal backoff delay with `retryAfter` and choose the longer delay. +Explanation: `Schedule.collectOutputs` collects the schedule output, not the +input. Add `Schedule.take` or another bound when collecting from an unbounded +schedule. -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. +## Accumulate State -### Retry Startup Config Fetches +### Accumulate Request Latency Stats -Write the Effect code that retries startup config loading only for temporary network/service failures. Use exponential backoff starting at about 100 ms. Try at most 3 more times. +Goal: Create a schedule for latency samples that inspects the first 5 inputs and +outputs running `min`, `max`, `total`, and `count` fields. ```ts -import { Effect, Schedule } from "effect" - -type ClientConfig = unknown - -type ConfigFetchError = - | { readonly _tag: "NetworkUnavailable" } - | { readonly _tag: "ServiceUnavailable" } - | { readonly _tag: "MalformedConfig" } - -declare const fetchStartupConfig: Effect.Effect - -const isTransientConfigFailure = (error: ConfigFetchError): boolean => - error._tag === "NetworkUnavailable" || error._tag === "ServiceUnavailable" - -const startupConfigRetryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.both(Schedule.recurs(3)) -) +import { Schedule } from "effect" -const program = fetchStartupConfig.pipe( - Effect.retry({ - schedule: startupConfigRetryPolicy, - while: isTransientConfigFailure - }) +const requestLatencyStats = Schedule.identity().pipe( + Schedule.take(5), + Schedule.reduce( + () => ({ min: Number.POSITIVE_INFINITY, max: 0, total: 0, count: 0 }), + (state, latency) => ({ + min: Math.min(state.min, latency), + max: Math.max(state.max, latency), + total: state.total + latency, + count: state.count + 1 + }) + ) ) ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +Explanation: `Schedule.reduce` is for a running aggregate. Prefer it over +collecting every value when callers only need a summary. -### Retry Transient Profile Loads +### Count Recent Worker Failures -Write the Effect code that retries `fetchProfile("user-123")` only for temporary profile-load failures. Use exponential backoff starting at about 100 ms, add a little randomness so callers do not retry together, and try at most 3 more times. +Goal: Create a schedule for boolean failure samples that inspects the first 5 +inputs and outputs the running count of failed samples. ```ts -import { Effect, Schedule } from "effect" - -type Profile = unknown -type ProfileLoadError = unknown - -declare const fetchProfile: ( - userId: string -) => Effect.Effect - -declare const isTransientProfileLoadError: ( - error: ProfileLoadError -) => boolean - -const retryTransientProfileLoad = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(3)) -) +import { Schedule } from "effect" -const program = fetchProfile("user-123").pipe( - Effect.retry({ - schedule: retryTransientProfileLoad, - while: isTransientProfileLoadError - }) +const recentWorkerFailureCount = Schedule.identity().pipe( + Schedule.take(5), + Schedule.reduce(() => 0, (count, failed) => failed ? count + 1 : count) ) ``` -Caution: jitter spreads delay across callers; it does not add a retry limit. +## Adapt Delays -### Retry Token Refresh Briefly +### Slow A Queue Consumer Under Backpressure -Write the Effect code that retries token refresh only for temporary refresh failures. Use exponential backoff starting at about 100 ms. Add a little randomness, try at most 2 more times, stop after about 1 second, and do not retry rejected, revoked, or expired refresh tokens. +Goal: Create a queue backpressure schedule that outputs each queue snapshot, +continues while the queue is not paused, adds 5 seconds of delay when depth is +above 1000, adds 500 milliseconds otherwise, and takes 10 outputs. ```ts import { Effect, Schedule } from "effect" -type Tokens = unknown - -type RefreshTimeout = { readonly _tag: "RefreshTimeout" } -type RefreshServiceUnavailable = { readonly _tag: "RefreshServiceUnavailable" } -type RefreshRejected = { readonly _tag: "RefreshRejected" } -type RefreshError = RefreshTimeout | RefreshServiceUnavailable | RefreshRejected - -declare const postRefreshToken: ( - refreshToken: string -) => Effect.Effect - -const isTransientRefreshFailure = ( - error: RefreshError -): error is RefreshTimeout | RefreshServiceUnavailable => - error._tag === "RefreshTimeout" || - error._tag === "RefreshServiceUnavailable" - -const retryTokenRefreshBriefly = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(2)), - Schedule.both(Schedule.during("1 second")) -) +type QueueSnapshot = { readonly depth: number; readonly paused: boolean } -const program = postRefreshToken("refresh-token-1").pipe( - Effect.retry({ - schedule: retryTokenRefreshBriefly, - while: isTransientRefreshFailure - }) +const queueBackpressureSchedule = Schedule.identity().pipe( + Schedule.while(({ input }) => !input.paused), + Schedule.addDelay((snapshot) => Effect.succeed(snapshot.depth > 1000 ? "5 seconds" : "500 millis")), + Schedule.take(10) ) ``` -Caution: jitter spreads delay across callers; it does not add a retry limit. +Explanation: `Schedule.addDelay` adds delay based on the current schedule +output. It is a good fit for input-driven pacing when the output is already the +latest input. -### Reconnect A WebSocket With Capped Backoff +### Cap WebSocket Reconnect Delays -Write the Effect code that reconnects a WebSocket. Use exponential backoff starting at about 100 ms, never wait more than 5 seconds before the next attempt, and try at most 8 more times. +Goal: Create a reconnect policy that uses jittered exponential backoff, caps +selected delays at 5 seconds, allows at most 8 recurrences, and outputs the +selected delay. ```ts import { Duration, Effect, Schedule } from "effect" -type LiveSocket = unknown -type WebSocketOpenError = unknown - -declare const openLiveSocket: Effect.Effect -declare const isRetryableOpenError: (error: WebSocketOpenError) => boolean - -const websocketReconnectPolicy = Schedule.exponential("100 millis").pipe( +const websocketReconnectDelays = Schedule.exponential("100 millis").pipe( + Schedule.jittered, Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.seconds(5)))), - Schedule.both(Schedule.recurs(8)) -) - -const program = openLiveSocket.pipe( - Effect.retry({ - schedule: websocketReconnectPolicy, - while: isRetryableOpenError - }) + Schedule.both(Schedule.recurs(8)), + Schedule.delays ) ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +Explanation: `Schedule.modifyDelay` receives the selected delay and returns the +replacement delay. Use it for caps, floors, clamps, or provider-provided delay +hints. -### Reconnect A WebSocket With Jitter +## Observe Schedule Decisions -Write the Effect code that reconnects a WebSocket. Add a little randomness so clients do not reconnect together, use exponential backoff starting at about 100 ms, never wait more than 5 seconds before the next attempt, try at most 8 more times, and stop retrying non-retryable reconnect errors. - -```ts -import { Duration, Effect, Schedule } from "effect" +### Log Heartbeat Inputs -type WebSocketReconnectError = unknown +Goal: Create a heartbeat schedule that runs on an aligned 10-second cadence, +logs each service id input, outputs the input unchanged, and takes 2 outputs. -declare const reconnectWebSocket: Effect.Effect -declare const isRetryableReconnect: (error: WebSocketReconnectError) => boolean +```ts +import { Console, Schedule } from "effect" -const webSocketReconnectPolicy = Schedule.exponential("100 millis").pipe( - Schedule.setInputType(), - Schedule.jittered, - Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.seconds(5)))), - Schedule.both(Schedule.recurs(8)), - Schedule.while(({ input }) => isRetryableReconnect(input)) -) +type HeartbeatStatus = { readonly id: string } -const program = reconnectWebSocket.pipe( - Effect.retry(webSocketReconnectPolicy) +const heartbeatInputLogs = Schedule.fixed("10 seconds").pipe( + Schedule.setInputType(), + Schedule.tapInput((input) => Console.log(`heartbeat:${input.id}`)), + Schedule.passthrough, + Schedule.take(2) ) ``` -Caution: jitter spreads delay across callers; it does not add a retry limit. - -### Retry Startup Dependency Checks +### Record Backoff Delays -Write the Effect code that retries startup dependency checks only for temporary startup failures. Use exponential backoff starting at about 100 ms, never wait more than 2 seconds before the next retry, try at most 5 more times, and do not keep startup waiting for more than about 20 seconds. +Goal: Create a retry schedule that uses Fibonacci backoff, takes 5 outputs, and +logs each selected delay without changing the schedule output. ```ts -import { Duration, Effect, Schedule } from "effect" - -type DependencyCheckError = - | { readonly reason: "DnsLookup" | "ConnectionRefused" | "Timeout" } - | { readonly reason: "BadCredentials" | "SchemaMismatch" } - -declare const checkDatabase: Effect.Effect - -const isRetryableStartupFailure = (error: DependencyCheckError) => - error.reason === "DnsLookup" || - error.reason === "ConnectionRefused" || - error.reason === "Timeout" - -const startupDependencyPolicy = Schedule.exponential("100 millis").pipe( - Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.seconds(2)))), - Schedule.both(Schedule.recurs(5)), - Schedule.both(Schedule.during("20 seconds")) -) +import { Console, Schedule } from "effect" -const program = checkDatabase.pipe( - Effect.retry({ - schedule: startupDependencyPolicy, - while: isRetryableStartupFailure - }) +const loggedBackoffDelays = Schedule.fibonacci("200 millis").pipe( + Schedule.take(5), + Schedule.tapOutput((delay) => Console.log(delay)) ) ``` -Caution: `recurs(n)` counts follow-up recurrences after the first execution. +### Log Attempt Metadata -### Retry A Duplicate-Safe Deployment Hook - -Write the Effect code that retries a duplicate-safe deployment hook only for temporary hook failures. +Goal: Create a telemetry backoff that logs each attempt number and selected +delay in milliseconds without changing the schedule output. ```ts -import { Duration, Effect, Schedule } from "effect" - -type HookReceipt = unknown - -type DeploymentHookError = { - readonly status: number -} - -declare const invokeDeploymentHook: ( - request: { - readonly deploymentId: string - readonly hookName: string - readonly idempotencyKey: string - } -) => Effect.Effect - -const isRetryableHookError = (error: DeploymentHookError) => - error.status === 408 || - error.status === 409 || - error.status === 429 || - error.status >= 500 - -const deploymentHookRetryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.setInputType(), - Schedule.jittered, - Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.seconds(2)))), - Schedule.both(Schedule.recurs(5)), - Schedule.while(({ input }) => isRetryableHookError(input)) -) +import { Console, Duration, Schedule } from "effect" -const program = invokeDeploymentHook({ - deploymentId: "deploy-1", - hookName: "post-deploy-smoke-test", - idempotencyKey: "deploy-1:post-deploy-smoke-test" -}).pipe( - Effect.retry(deploymentHookRetryPolicy) +const telemetryBackoffPolicy = Schedule.exponential("250 millis").pipe( + Schedule.take(5), + Schedule.tap(({ attempt, output }) => Console.log(`attempt-${attempt}: ${Duration.toMillis(output)}ms`)) ) ``` -Caution: the schedule only times retries; the stable idempotency key is what makes duplicate attempts safe. +Explanation: use `tapInput` when the input matters, `tapOutput` when only the +output matters, and `tap` when metadata such as attempt number or selected +duration matters. None of these operators changes the schedule output. + +## Build Local State Machines -### Retry Infrastructure API Calls +### Track Scheduler Ticks -Write the Effect code that retries a safe infrastructure API call for timeout, temporary unavailability, or rate limit failures. Reuse the same `clientToken`, use exponential backoff starting at about 100 ms, add a little randomness, try at most 5 more times, stop after about 30 seconds, and do not retry invalid requests or authorization failures. +Goal: Create a local scheduler tick state that starts at 1, increments after +each output, maps each output to a `tick` object, and takes 4 outputs. ```ts import { Effect, Schedule } from "effect" -type InfrastructureApiError = - | { readonly _tag: "ApiTimeout" } - | { readonly _tag: "ApiUnavailable" } - | { readonly _tag: "RateLimited" } - | { readonly _tag: "InvalidRequest" } - | { readonly _tag: "Unauthorized" } - | { readonly _tag: "Forbidden" } - -declare const createSubnet: ( - request: { - readonly vpcId: string - readonly cidrBlock: string - readonly clientToken: string - } -) => Effect.Effect - -const retryInfrastructureApi = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(5)), - Schedule.both(Schedule.during("30 seconds")) -) - -const program = createSubnet({ - vpcId: "vpc-123", - cidrBlock: "10.0.8.0/24", - clientToken: "deploy-1-subnet-10-0-8" -}).pipe( - Effect.retry({ - schedule: retryInfrastructureApi, - while: (error) => - error._tag === "ApiTimeout" || - error._tag === "ApiUnavailable" || - error._tag === "RateLimited" - }) +const schedulerTickState = Schedule.unfold(1, (n) => Effect.succeed(n + 1)).pipe( + Schedule.map((tick) => ({ tick })), + Schedule.take(4) ) ``` -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. +Explanation: `Schedule.unfold` builds a schedule from local state. Use it when +the next output depends on private scheduler state rather than on the latest +input. -### Retry Export Generation +### Cycle A Maintenance Phase Machine -Write the Effect code that retries idempotent export generation only for temporary database, renderer, or object-storage failures. Use a stable `exportId`, use exponential backoff starting at about 100 ms, add a little randomness, try at most 4 more times, and do not retry invalid requests or permission failures. +Goal: Create a local phase machine for maintenance work. Start at `warming`, +move to `active`, then `cooling`, then back to `active`, and take 6 outputs. ```ts import { Effect, Schedule } from "effect" -type ExportFile = unknown +type MaintenancePhase = "warming" | "active" | "cooling" -type ExportRequest = { - readonly exportId: string - readonly accountId: string - readonly format: "csv" | "json" -} - -type ExportError = - | { readonly _tag: "DatabaseUnavailable" } - | { readonly _tag: "RendererBusy" } - | { readonly _tag: "ObjectStorageUnavailable" } - | { readonly _tag: "InvalidExportRequest" } - | { readonly _tag: "PermissionDenied" } - -declare const generateExport: ( - request: ExportRequest -) => Effect.Effect - -const isTransientExportError = (error: ExportError): boolean => { - switch (error._tag) { - case "DatabaseUnavailable": - case "RendererBusy": - case "ObjectStorageUnavailable": - return true - case "InvalidExportRequest": - case "PermissionDenied": - return false - default: - return false +const maintenancePhaseMachine = Schedule.unfold( + "warming", + (phase) => { + switch (phase) { + case "warming": + return Effect.succeed("active") + case "active": + return Effect.succeed("cooling") + case "cooling": + return Effect.succeed("active") + } } -} - -const retryTransientExportFailures = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(4)) -) - -const request: ExportRequest = { - exportId: "export-2026-05-17", - accountId: "acct-123", - format: "csv" -} - -const program = generateExport(request).pipe( - Effect.retry({ - schedule: retryTransientExportFailures, - while: isTransientExportError - }) +).pipe( + Schedule.take(6) ) ``` -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. +## Realistic Policies -### Retry Object Storage Uploads +### Retry An HTTP Gateway With A Delay Envelope -Write the Effect code that retries an idempotent object storage upload only for temporary storage failures. Reuse deterministic object identity, add a little randomness so concurrent upload retries do not line up, use exponential backoff starting at about 250 ms, try at most 5 more times, and stop after about 30 seconds. +Goal: Create an HTTP gateway retry schedule. Retry only network failures, status +429, and status 500, 502, or 503. Use jittered exponential backoff starting at +100 milliseconds, cap selected delays at 2 seconds, allow at most 6 recurrences, +and output the selected delay. ```ts -import { Effect, Schedule } from "effect" - -type UploadRequest = { - readonly bucket: string - readonly key: string - readonly body: Uint8Array - readonly checksumSha256: string - readonly idempotencyKey: string -} - -type UploadError = - | { readonly reason: "Timeout" } - | { readonly reason: "Throttled" } - | { readonly reason: "Unavailable" } - | { readonly reason: "ChecksumMismatch" } - | { readonly reason: "Forbidden" } - | { readonly reason: "BadRequest" } - -declare const body: Uint8Array - -declare const uploadObject: ( - request: UploadRequest -) => Effect.Effect +import { Duration, Effect, Schedule } from "effect" -const isTransientStorageError = (error: UploadError) => - error.reason === "Timeout" || - error.reason === "Throttled" || - error.reason === "Unavailable" +type GraphqlGatewayError = + | { readonly _tag: "Network" } + | { readonly _tag: "HttpStatus"; readonly status: number } + | { readonly _tag: "BadRequest" } -const uploadRetryPolicy = Schedule.exponential("250 millis").pipe( +const isRetryableGraphqlGatewayError = ( + error: GraphqlGatewayError +): boolean => + error._tag === "Network" || + (error._tag === "HttpStatus" && + (error.status === 429 || + error.status === 500 || + error.status === 502 || + error.status === 503)) + +const graphqlGatewayRetry = Schedule.exponential("100 millis").pipe( Schedule.jittered, - Schedule.both(Schedule.recurs(5)), - Schedule.both(Schedule.during("30 seconds")) -) - -const program = uploadObject({ - bucket: "reports", - key: "daily/sha256-demo.json", - body, - checksumSha256: "sha256-demo", - idempotencyKey: "sha256-demo" -}).pipe( - Effect.retry({ - schedule: uploadRetryPolicy, - while: isTransientStorageError - }) + Schedule.setInputType(), + Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.seconds(2)))), + Schedule.both(Schedule.recurs(6)), + Schedule.while(({ input }) => isRetryableGraphqlGatewayError(input)), + Schedule.delays ) ``` -Caution: the schedule only times retries; the stable idempotency key is what makes duplicate attempts safe. +### Poll A Rollout With A Deadline -### Retry Import Processing - -Write the Effect code that retries one idempotent import batch only for storage, database, or enrichment outages. Use exponential backoff starting at about 100 ms, add a little randomness, try at most 4 more times, stop after about 20 seconds, and do not retry malformed input or domain validation failures. +Goal: Create a rollout watcher that starts from an aligned 1-second cadence, +jitters the selected delay, outputs the latest status, continues only while the +rollout is running, and stops after about 2 minutes. ```ts -import { Effect, Schedule } from "effect" - -type ImportBatch = unknown -type ImportSummary = unknown - -type ImportError = - | { readonly _tag: "StorageTimeout" } - | { readonly _tag: "DatabaseUnavailable" } - | { readonly _tag: "EnrichmentUnavailable" } - | { readonly _tag: "MalformedInput" } - | { readonly _tag: "ValidationFailed" } - -declare const processImportBatch: ( - batch: ImportBatch -) => Effect.Effect - -declare const batch: ImportBatch +import { Schedule } from "effect" -const isTransientImportError = (error: ImportError): boolean => - error._tag === "StorageTimeout" || - error._tag === "DatabaseUnavailable" || - error._tag === "EnrichmentUnavailable" +type RolloutStatus = { + readonly state: "running" | "succeeded" | "failed" +} -const importRetryPolicy = Schedule.exponential("100 millis").pipe( +const rolloutStatusWatcher = Schedule.fixed("1 second").pipe( + Schedule.setInputType(), + Schedule.passthrough, Schedule.jittered, - Schedule.both(Schedule.recurs(4)), - Schedule.both(Schedule.during("20 seconds")) -) - -const program = processImportBatch(batch).pipe( - Effect.retry({ - schedule: importRetryPolicy, - while: isTransientImportError - }) + Schedule.both(Schedule.during("2 minutes")), + Schedule.while(({ input }) => input.state === "running"), + Schedule.map(([status]) => status) ) ``` -Caution: jitter spreads delay across callers; it does not add a retry limit. +### Respect A Provider Retry-After Header -### Retry Payment Status Fetches - -Write the Effect code that retries a safe payment status read only for temporary status-fetch failures such as 408, 429, or 5xx responses. Use exponential backoff starting at about 100 ms, add a little randomness, and try at most 5 more times. +Goal: Create a provider retry schedule. Retry status 429, 500, and 503. Use +exponential backoff starting at 1 second. When `retryAfter` is present, use it as +a lower bound. Cap selected delays at 1 minute, allow at most 6 recurrences, and +output the selected delay. ```ts -import { Effect, Schedule } from "effect" - -type PaymentStatus = unknown +import { Duration, Effect, Schedule } from "effect" -type PaymentStatusFetchError = { - readonly status: number +type PushProviderResponse = { + readonly status: 429 | 500 | 503 | 400 + readonly retryAfter: Duration.Duration | undefined } -declare const fetchPaymentStatus: Effect.Effect - -const isRetryableStatusFetch = (error: PaymentStatusFetchError) => - error.status === 408 || error.status === 429 || error.status >= 500 - -const paymentStatusFetchRetry = Schedule.exponential("100 millis").pipe( - Schedule.setInputType(), - Schedule.jittered, - Schedule.both(Schedule.recurs(5)), - Schedule.while(({ input }) => isRetryableStatusFetch(input)) -) - -const program = fetchPaymentStatus.pipe( - Effect.retry(paymentStatusFetchRetry) +const pushNotificationProviderRetry = Schedule.exponential("1 second").pipe( + Schedule.setInputType(), + Schedule.passthrough, + Schedule.modifyDelay((response, delay) => + Effect.succeed( + Duration.min( + response.retryAfter === undefined + ? delay + : Duration.max(delay, response.retryAfter), + Duration.minutes(1) + ) + ) + ), + Schedule.both(Schedule.recurs(6)), + Schedule.while(({ input }) => input.status === 429 || input.status === 500 || input.status === 503), + Schedule.delays ) ``` -Caution: jitter spreads delay across callers; it does not add a retry limit. +### Poll An OAuth Device Code Flow -### Retry Notification Delivery - -Write the Effect code that retries notification delivery only for timeout or provider-unavailable failures while preserving a stable notification idempotency key. Use exponential backoff starting at about 100 ms, add a little randomness, try at most 5 more times, and do not retry invalid recipients, malformed messages, or unauthorized requests. +Goal: Create an OAuth device-code polling schedule. Poll every 5 seconds, add +another 5 seconds for `slow_down`, output the latest input, continue only for +`authorization_pending` and `slow_down`, and stop after about 15 minutes. ```ts import { Effect, Schedule } from "effect" -type Notification = { - readonly idempotencyKey: string - readonly recipient: string - readonly body: string +type OAuthDeviceCodeStatus = { + readonly error: + | "authorization_pending" + | "slow_down" + | "access_denied" + | "expired_token" } -type DeliveryError = - | { readonly _tag: "Timeout" } - | { readonly _tag: "ProviderUnavailable" } - | { readonly _tag: "InvalidRecipient" } - | { readonly _tag: "MalformedMessage" } - | { readonly _tag: "Unauthorized" } - -declare const sendWithIdempotency: ( - notification: Notification -) => Effect.Effect - -const isRetryableDeliveryError = (error: DeliveryError): boolean => - error._tag === "Timeout" || error._tag === "ProviderUnavailable" - -const retryTransientDelivery = Schedule.exponential("100 millis").pipe( - Schedule.setInputType(), - Schedule.jittered, - Schedule.both(Schedule.recurs(5)) -) - -const program = sendWithIdempotency({ - idempotencyKey: "notification-01HZYX8R7P0J9PAW4Q6V7N3QYB", - recipient: "user@example.com", - body: "Your export is ready." -}).pipe( - Effect.retry({ - schedule: retryTransientDelivery, - while: isRetryableDeliveryError - }) +const oauthDeviceCodePolling = Schedule.spaced("5 seconds").pipe( + Schedule.setInputType(), + Schedule.passthrough, + Schedule.addDelay((status) => Effect.succeed(status.error === "slow_down" ? "5 seconds" : "0 millis")), + Schedule.both(Schedule.during("15 minutes")), + Schedule.while(({ input }) => input.error === "authorization_pending" || input.error === "slow_down"), + Schedule.map(([status]) => status) ) ``` -Caution: the schedule only times retries; the stable idempotency key is what makes duplicate attempts safe. +### Escalate Incident Notifications In Phases -### Retry An Unstable Remote API - -Write the Effect code for a request-path call to `callRemoteApi`. +Goal: Create an incident escalation cadence that emits 3 recurrences spaced 1 +minute apart, then 3 recurrences spaced 5 minutes apart, then switches to an +aligned 15-minute cadence. ```ts -import { Effect, Schedule } from "effect" - -type RemoteApiError = - | { readonly _tag: "Timeout" } - | { readonly _tag: "ConnectionReset" } - | { readonly _tag: "ServiceUnavailable" } - | { readonly _tag: "BadRequest" } - | { readonly _tag: "Unauthorized" } - -declare const callRemoteApi: Effect.Effect - -const isRetryableRemoteApiError = (error: RemoteApiError): boolean => - error._tag === "Timeout" || - error._tag === "ConnectionReset" || - error._tag === "ServiceUnavailable" - -const remoteApiRetry = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(5)), - Schedule.both(Schedule.during("10 seconds")) -) +import { Schedule } from "effect" -const program = callRemoteApi.pipe( - Effect.retry({ - schedule: remoteApiRetry, - while: isRetryableRemoteApiError - }) +const incidentEscalationCadence = Schedule.spaced("1 minute").pipe( + Schedule.take(3), + Schedule.andThen(Schedule.spaced("5 minutes").pipe(Schedule.take(3))), + Schedule.andThen(Schedule.fixed("15 minutes")) ) ``` -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. - -### Retry Queue Reconnection +### Run Maintenance After A Warmup -Write the Effect code used by a worker during startup to connect to the queue. +Goal: Create a maintenance schedule that performs one warmup recurrence after +about 30 seconds, then switches to a cron schedule that recurs every day at +03:00. ```ts -import { Effect, Schedule } from "effect" - -type QueueConnection = unknown - -type QueueConnectError = - | { readonly _tag: "DnsFailure" } - | { readonly _tag: "ConnectionRefused" } - | { readonly _tag: "BrokerUnavailable" } - | { readonly _tag: "BadCredentials" } - | { readonly _tag: "UnsupportedProtocol" } - -declare const connectQueue: Effect.Effect - -const isRetryableQueueConnectError = (error: QueueConnectError): boolean => - error._tag === "DnsFailure" || - error._tag === "ConnectionRefused" || - error._tag === "BrokerUnavailable" +import { Schedule } from "effect" -const queueReconnect = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(8)), - Schedule.both(Schedule.during("1 minute")) +const maintenanceCronAfterWarmup = Schedule.duration("30 seconds").pipe( + Schedule.andThen(Schedule.cron("0 3 * * *")) ) - -const program = connectQueue.pipe( - Effect.retry({ - schedule: queueReconnect, - while: isRetryableQueueConnectError - }) -) -``` - -Caution: jitter spreads delay across callers; it does not add a retry limit. - -### Retry Cold-Start Dependencies - -Write the Effect code for a service startup check. - -```ts -import { Effect, Schedule } from "effect" - -type DependencyCheckError = - | { readonly _tag: "DnsLookup" } - | { readonly _tag: "ConnectionRefused" } - | { readonly _tag: "Timeout" } - | { readonly _tag: "BadCredentials" } - | { readonly _tag: "SchemaMismatch" } - -declare const checkDependency: Effect.Effect - -const isRetryableStartupCheckError = (error: DependencyCheckError): boolean => - error._tag === "DnsLookup" || - error._tag === "ConnectionRefused" || - error._tag === "Timeout" - -const dependencyStartup = Schedule.exponential("100 millis").pipe( - Schedule.both(Schedule.recurs(6)), - Schedule.both(Schedule.during("30 seconds")) -) - -const program = checkDependency.pipe( - Effect.retry({ - schedule: dependencyStartup, - while: isRetryableStartupCheckError - }) -) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Retry With Capped Backoff - -Write the Effect code for calling the search backend. - -```ts -import { Duration, Effect, Schedule } from "effect" - -type SearchResult = unknown - -type SearchBackendError = - | { readonly _tag: "Timeout" } - | { readonly _tag: "Overloaded" } - | { readonly _tag: "RejectedQuery" } - -declare const callSearchBackend: Effect.Effect - -const isRetryableSearchBackendError = (error: SearchBackendError): boolean => - error._tag === "Timeout" || error._tag === "Overloaded" - -const cappedBackoff = Schedule.exponential("100 millis").pipe( - Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.seconds(5)))), - Schedule.both(Schedule.recurs(5)) -) - -const program = callSearchBackend.pipe( - Effect.retry({ - schedule: cappedBackoff, - while: isRetryableSearchBackendError - }) -) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Retry HTTP Clients With Jitter - -Write the Effect code for a client request that may be called by many app instances at once. - -```ts -import { Effect, Schedule } from "effect" - -type HttpResponse = unknown - -type HttpClientError = - | { readonly _tag: "Timeout" } - | { readonly _tag: "ConnectionReset" } - | { readonly _tag: "ResponseError"; readonly status: number } - | { readonly _tag: "InvalidRequest" } - -declare const httpRequest: Effect.Effect - -const isRetryableHttpClientError = (error: HttpClientError): boolean => - error._tag === "Timeout" || - error._tag === "ConnectionReset" || - (error._tag === "ResponseError" && error.status >= 500) - -const httpRetry = Schedule.exponential("100 millis").pipe( - Schedule.jittered, - Schedule.both(Schedule.recurs(5)) -) - -const program = httpRequest.pipe( - Effect.retry({ - schedule: httpRetry, - while: isRetryableHttpClientError - }) -) -``` - -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. - -### Do Not Retry Validation Errors - -Write the Effect code for submitting the form. - -```ts -import { Effect, Schedule } from "effect" - -type SubmitReceipt = unknown - -type SubmitError = - | { readonly _tag: "Timeout" } - | { readonly _tag: "ServiceUnavailable" } - | { readonly _tag: "ValidationError"; readonly field: string } - -declare const submitForm: Effect.Effect - -const isRetryableSubmitError = (error: SubmitError): boolean => - error._tag === "Timeout" || error._tag === "ServiceUnavailable" - -const program = submitForm.pipe( - Effect.retry({ - schedule: Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3))), - while: isRetryableSubmitError - }) -) -``` - -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. - -### Retry Only 5xx Responses - -Write the Effect code for a safe resource read. - -```ts -import { Effect, Schedule } from "effect" - -type Resource = unknown - -type HttpError = - | { readonly _tag: "HttpError"; readonly status: number } - | { readonly _tag: "NetworkError" } - -declare const fetchResource: Effect.Effect - -const isServerError = (error: HttpError): boolean => error._tag === "HttpError" && error.status >= 500 - -const program = fetchResource.pipe( - Effect.retry({ - schedule: Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3))), - while: isServerError - }) -) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Retry Safe GET Reads - -Write the Effect code for a safe read path. - -```ts -import { Effect, Schedule } from "effect" - -type Resource = unknown - -type GetError = - | { readonly _tag: "Timeout"; readonly method: "GET" | "POST" } - | { readonly _tag: "ServiceUnavailable"; readonly method: "GET" | "POST" } - | { readonly _tag: "BadRequest"; readonly method: "GET" | "POST" } - -declare const getResource: Effect.Effect - -const isRetryableGetError = (error: GetError): boolean => - error.method === "GET" && - (error._tag === "Timeout" || error._tag === "ServiceUnavailable") - -const program = getResource.pipe( - Effect.retry({ - schedule: Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(3))), - while: isRetryableGetError - }) -) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Retry Short Pauses, Then Backoff - -Write the Effect code for a search request where the first failures are often very short blips. - -```ts -import { Effect, Schedule } from "effect" - -type SearchBackendError = unknown -type SearchResult = unknown - -declare const callSearchBackend: Effect.Effect - -const shortRetries = Schedule.spaced("100 millis").pipe( - Schedule.bothLeft(Schedule.recurs(2)) -) -const backoffRetries = Schedule.exponential("250 millis").pipe( - Schedule.both(Schedule.recurs(4)) -) -const policy = shortRetries.pipe(Schedule.andThen(backoffRetries)) - -const program = callSearchBackend.pipe(Effect.retry(policy)) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Retry While An Auth Token Is Stale - -Write the Effect code for refreshing an auth token. - -```ts -import { Effect, Schedule } from "effect" - -type AuthToken = unknown - -type AuthTokenError = - | { readonly _tag: "StaleToken" } - | { readonly _tag: "TokenRevoked" } - | { readonly _tag: "MalformedToken" } - -declare const refreshAuthToken: Effect.Effect - -const program = refreshAuthToken.pipe( - Effect.retry({ - schedule: Schedule.spaced("500 millis").pipe( - Schedule.both(Schedule.recurs(4)) - ), - while: (error) => error._tag === "StaleToken" - }) -) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Retry While A Circuit Breaker Allows It - -Write the Effect code for calling billing from a checkout flow. - -```ts -import { Effect, Schedule } from "effect" - -type BillingReceipt = unknown - -type BillingError = - | { readonly _tag: "TransientBillingError" } - | { readonly _tag: "RejectedPayment" } - -declare const callBilling: Effect.Effect -declare const circuitBreakerAllowsRetry: (error: BillingError) => Effect.Effect - -const program = callBilling.pipe( - Effect.retry({ - schedule: Schedule.exponential("100 millis").pipe(Schedule.both(Schedule.recurs(5))), - while: (error) => - error._tag === "TransientBillingError" - ? circuitBreakerAllowsRetry(error) - : Effect.succeed(false) - }) -) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -## Polling And State Observation - -In these recipes, non-terminal states are successful values, not failures. `Effect.repeat` runs the effect again after each success and stops when the schedule stops. A real failure from the effect still fails the whole program. - -### Poll Job Status Until Terminal - -Write the Effect code that polls `readJobStatus("job-1")` until the status is `succeeded` or `failed`, or until about 30 seconds pass. - -```ts -import { Effect, Schedule } from "effect" - -type JobStatusError = unknown - -type JobStatus = - | { readonly state: "queued"; readonly jobId: string } - | { readonly state: "running"; readonly jobId: string } - | { readonly state: "succeeded"; readonly jobId: string; readonly artifactUrl: string } - | { readonly state: "failed"; readonly jobId: string; readonly reason: string } - -declare const readJobStatus: ( - jobId: string -) => Effect.Effect - -const isTerminal = (status: JobStatus): boolean => status.state === "succeeded" || status.state === "failed" - -const pollJobStatus = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => !isTerminal(input)), - Schedule.bothLeft( - Schedule.during("30 seconds").pipe( - Schedule.setInputType() - ) - ) -) - -const program = readJobStatus("job-1").pipe( - Effect.repeat(pollJobStatus) -) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Platform Readiness - -Write the Effect code that polls readiness snapshots. - -```ts -import { Effect, Schedule } from "effect" - -type ReadinessCheckError = unknown -type ReadinessSnapshot = unknown - -declare const readPlatformReadiness: Effect.Effect -declare const isTerminalReadiness: (snapshot: ReadinessSnapshot) => boolean - -const readinessPolling = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => !isTerminalReadiness(input)), - Schedule.bothLeft(Schedule.during("30 seconds")), - Schedule.bothLeft(Schedule.recurs(30)) -) - -const program = readPlatformReadiness.pipe( - Effect.repeat(readinessPolling) -) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Rollout Status With Jitter - -Write the Effect code that polls a rollout about once per second while it is `running`. Add a little randomness so many status watchers do not poll at the same instant. Stop on `succeeded` or `failed`; if it is still running after about 2 minutes, return the latest status. - -```ts -import { Effect, Schedule } from "effect" - -type StatusReadError = unknown - -type RolloutStatus = - | { readonly state: "running" } - | { readonly state: "succeeded" } - | { readonly state: "failed"; readonly reason: string } - -declare const readRolloutStatus: ( - rolloutId: string -) => Effect.Effect - -const pollRolloutStatus = Schedule.spaced("1 second").pipe( - Schedule.jittered, - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input.state === "running"), - Schedule.bothLeft(Schedule.during("2 minutes")) -) - -const program = readRolloutStatus("rollout-42").pipe( - Effect.repeat(pollRolloutStatus) -) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll ETL Status - -Write the Effect code that polls ETL status until `succeeded`, `failed`, or `canceled`, preserving the final observed status. Check about every 5 seconds and stop waiting after about 5 minutes. - -```ts -import { Effect, Schedule } from "effect" - -type StatusReadError = unknown - -type EtlStatus = - | { readonly state: "running" } - | { readonly state: "succeeded" } - | { readonly state: "failed"; readonly reason: string } - | { readonly state: "canceled" } - -declare const readEtlStatus: ( - runId: string -) => Effect.Effect - -const isTerminalEtlStatus = (status: EtlStatus): boolean => - status.state === "succeeded" || - status.state === "failed" || - status.state === "canceled" - -const pollEtlStatusBudget = Schedule.spaced("5 seconds").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => !isTerminalEtlStatus(input)), - Schedule.bothLeft(Schedule.during("5 minutes")), - Schedule.bothLeft(Schedule.recurs(60)) -) - -const program = readEtlStatus("etl-run-7").pipe( - Effect.repeat(pollEtlStatusBudget) -) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Payment Settlement - -Write the Effect code that polls successful settlement statuses. - -```ts -import { Effect, Schedule } from "effect" - -type SettlementError = unknown - -type SettlementStatus = - | { readonly _tag: "Pending" } - | { readonly _tag: "Processing" } - | { readonly _tag: "Settled" } - | { readonly _tag: "Declined" } - -declare const fetchSettlementStatus: Effect.Effect - -const isOpen = (status: SettlementStatus) => status._tag === "Pending" || status._tag === "Processing" - -const pollOpenSettlements = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => isOpen(input)), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = fetchSettlementStatus.pipe( - Effect.repeat(pollOpenSettlements) -) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Order Fulfillment - -Write the Effect code that polls fulfillment statuses. - -```ts -import { Effect, Schedule } from "effect" - -type FulfillmentReadError = unknown - -type FulfillmentStatus = - | { readonly state: "preparing" } - | { readonly state: "shipped" } - | { readonly state: "delivered" } - | { readonly state: "canceled"; readonly reason: string } - -declare const readFulfillmentStatus: Effect.Effect - -const isTerminalFulfillment = (status: FulfillmentStatus) => status.state === "delivered" || status.state === "canceled" - -const userFacingPolling = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => !isTerminalFulfillment(input)), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = readFulfillmentStatus.pipe( - Effect.repeat(userFacingPolling) -) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll A Background Job - -Write the Effect code used by an API endpoint after it starts a background job. - -```ts -import { Effect, Schedule } from "effect" - -type JobStatusError = unknown - -type JobStatus = - | { readonly state: "queued"; readonly jobId: string } - | { readonly state: "running"; readonly jobId: string } - | { readonly state: "done"; readonly jobId: string } - | { readonly state: "failed"; readonly jobId: string; readonly reason: string } - -declare const jobId: string -declare const readJobStatus: (jobId: string) => Effect.Effect - -const pollJob = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input.state === "queued" || input.state === "running"), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = readJobStatus(jobId).pipe(Effect.repeat(pollJob)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll A Payment Until Settled - -Write the Effect code used after submitting a payment. - -```ts -import { Effect, Schedule } from "effect" - -type PaymentReadError = unknown - -type PaymentStatus = - | { readonly _tag: "Pending" } - | { readonly _tag: "Processing" } - | { readonly _tag: "Settled" } - | { readonly _tag: "Declined" } - -declare const paymentId: string -declare const readPaymentStatus: (paymentId: string) => Effect.Effect - -const pollPayment = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input._tag === "Pending" || input._tag === "Processing"), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = readPaymentStatus(paymentId).pipe(Effect.repeat(pollPayment)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll An Export Until Ready - -Write the Effect code used after requesting a report export. - -```ts -import { Effect, Schedule } from "effect" - -type ExportStatusError = unknown - -type ExportStatus = - | { readonly state: "queued" } - | { readonly state: "running" } - | { readonly state: "ready"; readonly url: string } - | { readonly state: "failed"; readonly reason: string } - -declare const exportId: string -declare const readExportStatus: (exportId: string) => Effect.Effect - -const pollExport = Schedule.spaced("2 seconds").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input.state === "queued" || input.state === "running"), - Schedule.bothLeft(Schedule.during("2 minutes")) -) - -const program = readExportStatus(exportId).pipe(Effect.repeat(pollExport)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Cloud Provisioning - -Write the Effect code used after creating cloud infrastructure. - -```ts -import { Effect, Schedule } from "effect" - -type ProvisioningReadError = unknown - -type ProvisioningStatus = - | { readonly state: "creating" } - | { readonly state: "updating" } - | { readonly state: "ready" } - | { readonly state: "failed"; readonly reason: string } - -declare const resourceId: string -declare const readProvisioningStatus: (resourceId: string) => Effect.Effect - -const pollProvisioning = Schedule.spaced("5 seconds").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input.state === "creating" || input.state === "updating"), - Schedule.bothLeft(Schedule.during("5 minutes")) -) - -const program = readProvisioningStatus(resourceId).pipe(Effect.repeat(pollProvisioning)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Until Completed - -Write the Effect code used by a client that waits briefly for completion. - -```ts -import { Effect, Schedule } from "effect" - -type StatusReadError = unknown - -type Status = "Queued" | "Running" | "Completed" - -declare const id: string -declare const readStatus: (id: string) => Effect.Effect - -const untilCompleted = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input !== "Completed"), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = readStatus(id).pipe(Effect.repeat(untilCompleted)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Repeat Until A Condition Is True - -Write the Effect code used by a command-line tool waiting for work to complete. - -```ts -import { Effect, Schedule } from "effect" - -type StatusCheckError = unknown - -type Status = - | { readonly _tag: "Working" } - | { readonly _tag: "Complete" } - -declare const checkStatus: Effect.Effect -declare const isComplete: (status: Status) => boolean - -const untilComplete = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => !isComplete(input)) -) - -const program = checkStatus.pipe(Effect.repeat(untilComplete)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Repeat While Work Remains - -Write the Effect code for draining batches from a backlog. - -```ts -import { Effect, Schedule } from "effect" - -type BatchError = unknown - -type BatchResult = { - readonly processed: number - readonly remaining: number -} - -declare const processBatch: Effect.Effect - -const whileWorkRemains = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input.remaining > 0) -) - -const program = processBatch.pipe(Effect.repeat(whileWorkRemains)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Until A Resource Exists - -Write the Effect code that waits for a just-created resource to become visible. - -```ts -import { Effect, Schedule } from "effect" - -type LookupError = unknown -type Resource = unknown - -type ResourceLookup = - | { readonly _tag: "Missing" } - | { readonly _tag: "Found"; readonly resource: Resource } - -declare const lookupResource: (id: string) => Effect.Effect -declare const id: string - -const untilResourceExists = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input._tag === "Missing"), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = lookupResource(id).pipe(Effect.repeat(untilResourceExists)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Until A Cache Entry Appears - -Write the Effect code that waits for a cache entry written by another process. - -```ts -import { Effect, Schedule } from "effect" - -type CacheReadError = unknown -type CachedValue = unknown - -type CacheLookup = - | { readonly _tag: "Miss" } - | { readonly _tag: "Hit"; readonly value: CachedValue } - -declare const readCacheEntry: (key: string) => Effect.Effect -declare const key: string - -const untilCacheHit = Schedule.spaced("500 millis").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input._tag === "Miss"), - Schedule.bothLeft(Schedule.during("10 seconds")) -) - -const program = readCacheEntry(key).pipe(Effect.repeat(untilCacheHit)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Until Replication Catches Up - -Write the Effect code used after writing to the primary database. - -```ts -import { Effect, Schedule } from "effect" - -type ReplicationReadError = unknown - -type ReplicationLag = { - readonly behindBy: number -} - -declare const readReplicationLag: Effect.Effect - -const untilCaughtUp = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input.behindBy > 0), - Schedule.bothLeft(Schedule.during("1 minute")) -) - -const program = readReplicationLag.pipe(Effect.repeat(untilCaughtUp)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll With An Interval And Deadline - -Write the Effect code for a status reader that should keep checking briefly. - -```ts -import { Effect, Schedule } from "effect" - -type Status = unknown -type StatusReadError = unknown - -declare const readStatus: Effect.Effect - -const polling = Schedule.spaced("1 second").pipe( - Schedule.passthrough, - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = readStatus.pipe(Effect.repeat(polling)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Return A Graceful Poll Timeout - -Write the Effect code for an API endpoint that waits briefly for work to finish. - -```ts -import { Effect, Schedule } from "effect" - -type Result = unknown -type StatusReadError = unknown - -type Status = - | { readonly _tag: "Running" } - | { readonly _tag: "Finished"; readonly value: Result } - -declare const readStatus: Effect.Effect - -const polling = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input._tag === "Running"), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = readStatus.pipe( - Effect.repeat(polling), - Effect.flatMap((status) => - status._tag === "Running" - ? Effect.fail({ _tag: "TimedOut", latest: status } as const) - : Effect.succeed(status) - ) -) -``` - -Explanation: exhausting the schedule is not a failure by itself. `Effect.repeat` returns the latest schedule output, so the final `flatMap` turns an unfinished latest status into a domain-specific timeout. - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Poll Fast, Then Slow - -Write the Effect code used immediately after starting a workflow. - -```ts -import { Effect, Schedule } from "effect" - -type WorkflowStatus = unknown -type WorkflowStatusError = unknown - -declare const readWorkflowStatus: Effect.Effect - -const fastThenSlow = Schedule.spaced("250 millis").pipe( - Schedule.bothLeft(Schedule.recurs(8)) -) -const slowPolling = Schedule.spaced("2 seconds") -const polling = Schedule.andThen(fastThenSlow, slowPolling).pipe( - Schedule.setInputType(), - Schedule.passthrough -) - -const program = readWorkflowStatus.pipe(Effect.repeat(polling)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Repeat Until An Effectful Predicate Stops - -Write the Effect code for a startup readiness probe. - -```ts -import { Effect, Schedule } from "effect" - -type ProbeError = unknown - -type ServiceProbeResult = - | { readonly _tag: "Starting" } - | { readonly _tag: "Ready" } - | { readonly _tag: "Failed" } - -declare const probeService: Effect.Effect -declare const recordReadinessProbe: (result: ServiceProbeResult) => Effect.Effect - -const program = probeService.pipe( - Effect.repeat({ - schedule: Schedule.spaced("1 second").pipe( - Schedule.both(Schedule.recurs(30)) - ), - until: (result) => - recordReadinessProbe(result).pipe( - Effect.map(() => result._tag === "Ready" || result._tag === "Failed") - ) - }) -) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -## Repeat And Background Work - -### Repeat Failed-Record Reprocessing - -Write the Effect code for a repair job that reprocesses failed records in passes. After the first successful pass, run at most 3 more passes, waiting about 30 seconds between passes. - -```ts -import { Effect, Schedule } from "effect" - -type ReprocessBatchError = unknown - -declare const reprocessFailedBatch: Effect.Effect - -const reprocessingCadence = Schedule.spaced("30 seconds").pipe( - Schedule.take(3) -) - -const program = reprocessFailedBatch.pipe( - Effect.repeat(reprocessingCadence) -) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -### Repeat CRM Sync - -Write the Effect code that repeats successful CRM sync passes, waiting about 5 minutes between completed passes. Return the latest successful sync summary. Failed CRM requests should fail the program; do not turn this into retry logic. - -```ts -import { Effect, Schedule } from "effect" - -type SyncError = unknown -type SyncSummary = unknown - -declare const syncCrmOnce: Effect.Effect - -const crmSyncCadence = Schedule.spaced("5 minutes").pipe( - Schedule.setInputType(), - Schedule.passthrough -) - -const program = syncCrmOnce.pipe( - Effect.repeat(crmSyncCadence) -) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Jitter Periodic Cache Refreshes - -This service runs on many instances. Write the Effect code that refreshes the cache in the background after each successful refresh. - -```ts -import { Effect, Schedule } from "effect" - -type CacheRefreshError = unknown - -declare const refreshCache: Effect.Effect - -const refreshCadence = Schedule.spaced("1 minute").pipe( - Schedule.jittered -) - -const program = refreshCache.pipe(Effect.repeat(refreshCadence)) -``` - -Caution: jitter spreads delay across callers; it does not add a retry limit. - -### Slow A Worker Loop - -Write the Effect code for a background worker loop. - -```ts -import { Effect, Schedule } from "effect" - -type WorkerError = unknown - -type WorkerIteration = - | { readonly _tag: "Processed"; readonly count: number } - | { readonly _tag: "Idle" } - -declare const workerIteration: Effect.Effect - -const workerCadence = Schedule.spaced("500 millis") - -const program = workerIteration.pipe( - Effect.repeat(workerCadence) -) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -### Drain A Queue Slowly - -Write the Effect code for a maintenance process that drains queued work. - -```ts -import { Effect, Schedule } from "effect" - -type DrainSummary = unknown -type QueueDrainError = unknown - -declare const drainQueueOnce: Effect.Effect - -const drainCadence = Schedule.spaced("1 second") -const program = drainQueueOnce.pipe(Effect.repeat(drainCadence)) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -### Observe Repeat Counts - -Write the Effect code for a metrics flush loop that stops after a small number of follow-up flushes. - -```ts -import { Effect, Schedule } from "effect" - -type MetricsFlushError = unknown - -declare const flushMetricsBatch: Effect.Effect - -declare const Metrics: { - readonly recordFlushAttempt: (attempt: number) => Effect.Effect -} - -const policy = Schedule.spaced("10 seconds").pipe( - Schedule.bothRight(Schedule.recurs(6)), - Schedule.tapOutput((count) => Metrics.recordFlushAttempt(count)) -) - -const program = flushMetricsBatch.pipe(Effect.repeat(policy)) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Warm Up Polling, Then Steady Polling - -Write the Effect code used right after enabling a new indexer. - -```ts -import { Effect, Schedule } from "effect" - -type IndexerLag = unknown -type IndexerLagError = unknown - -declare const readIndexerLag: Effect.Effect - -const warmup = Schedule.spaced("250 millis").pipe( - Schedule.bothLeft(Schedule.recurs(8)) -) -const steady = Schedule.spaced("5 seconds") -const policy = warmup.pipe( - Schedule.andThen(steady), - Schedule.setInputType(), - Schedule.passthrough -) - -const program = readIndexerLag.pipe(Effect.repeat(policy)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -### Burst Heartbeats, Then Fixed Heartbeats - -Write the Effect code used when a process first joins a cluster. - -```ts -import { Effect, Schedule } from "effect" - -type HeartbeatError = unknown - -declare const sendHeartbeat: Effect.Effect - -const burst = Schedule.spaced("100 millis").pipe( - Schedule.bothLeft(Schedule.recurs(3)) -) -const steady = Schedule.fixed("30 seconds") -const policy = burst.pipe(Schedule.andThen(steady)) - -const program = sendHeartbeat.pipe(Effect.repeat(policy)) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Repeat A Fixed Heartbeat - -Write the Effect code for a long-running process heartbeat. - -```ts -import { Effect, Schedule } from "effect" - -type HeartbeatError = unknown - -declare const sendHeartbeat: Effect.Effect - -const heartbeatCadence = Schedule.fixed("30 seconds") -const program = sendHeartbeat.pipe(Effect.repeat(heartbeatCadence)) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -### Repeat On Metric Flush Windows - -Write the Effect code for flushing metrics on wall-clock minute windows. - -```ts -import { Effect, Schedule } from "effect" - -type MetricsFlushError = unknown - -declare const flushMetrics: Effect.Effect - -const flushWindow = Schedule.windowed("1 minute") -const program = flushMetrics.pipe(Effect.repeat(flushWindow)) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -### Repeat A Nightly Cron Job - -Write the Effect code for a database maintenance job. - -```ts -import { Effect, Schedule } from "effect" - -type CompactionError = unknown - -declare const compactDatabase: Effect.Effect - -const nightly = Schedule.cron("0 2 * * *") -const program = compactDatabase.pipe(Effect.repeat(nightly)) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -### Run One Follow-Up Cache Warm - -Write the Effect code for a deploy hook that warms the cache once, then performs one follow-up warm after 10 seconds. - -```ts -import { Effect, Schedule } from "effect" - -type CacheWarmError = unknown - -declare const warmCache: Effect.Effect - -const oneMoreRun = Schedule.duration("10 seconds") -const program = warmCache.pipe(Effect.repeat(oneMoreRun)) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -### Repeat Until Terminal With Options - -Write the Effect code used after starting a file transfer. - -```ts -import { Effect, Schedule } from "effect" - -type TransferStatusError = unknown - -type TransferStatus = - | { readonly _tag: "Pending" } - | { readonly _tag: "InProgress" } - | { readonly _tag: "Completed" } - | { readonly _tag: "Failed" } - -declare const readTransferStatus: Effect.Effect - -const program = readTransferStatus.pipe( - Effect.repeat({ - schedule: Schedule.spaced("1 second"), - until: (status) => status._tag === "Completed" || status._tag === "Failed" - }) -) -``` - -Caution: `Effect.repeat` observes successful values and stops on the first failure. - -## Observability - -### Log Each Retry Attempt - -Write the Effect code for fetching inventory from a provider. - -```ts -import { Console, Effect, Schedule } from "effect" - -type InventorySnapshot = unknown - -type InventoryError = - | { readonly _tag: "TransientInventoryError" } - | { readonly _tag: "InvalidSku" } - -declare const fetchInventory: Effect.Effect - -const retryInventoryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.both(Schedule.recurs(5)), - Schedule.tapInput((error: InventoryError) => Console.log(`retry input: ${error._tag}`)), - Schedule.tapOutput((output) => Console.log(`retry scheduled: ${String(output)}`)) -) - -const program = fetchInventory.pipe( - Effect.retry({ - schedule: retryInventoryPolicy, - while: (error) => error._tag === "TransientInventoryError" - }) -) -``` - -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. - -### Log Retry Counts With `bothRight` - -Write the Effect code for uploading one chunk. - -```ts -import { Console, Effect, Schedule } from "effect" - -type UploadError = - | { readonly _tag: "TransientUploadError" } - | { readonly _tag: "InvalidUpload" } - -declare const uploadChunk: Effect.Effect - -const policy = Schedule.exponential("100 millis").pipe( - Schedule.bothRight(Schedule.recurs(4)), - Schedule.tapOutput((count) => Console.log(`retry attempt ${count}`)) -) - -const program = uploadChunk.pipe( - Effect.retry({ - schedule: policy, - while: (error) => error._tag === "TransientUploadError" - }) -) -``` - -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. - -### Observe Retry Input Errors - -Write the Effect code for an inventory lookup that records retry diagnostics. - -```ts -import { Console, Effect, Schedule } from "effect" - -type InventorySnapshot = unknown - -type InventoryError = - | { readonly _tag: "TransientInventoryError" } - | { readonly _tag: "InvalidSku" } - -declare const callInventory: Effect.Effect - -const policy = Schedule.exponential("100 millis").pipe( - Schedule.tapInput((error: InventoryError) => Console.log(`retrying after ${error._tag}`)), - Schedule.both(Schedule.recurs(4)) -) - -const program = callInventory.pipe( - Effect.retry({ - schedule: policy, - while: (error) => error._tag === "TransientInventoryError" - }) -) -``` - -Caution: classify permanent failures before applying retry; backoff does not make invalid input transient. - -### Observe Retry Output Delays - -Write the Effect code for webhook delivery. - -```ts -import { Duration, Effect, Schedule } from "effect" - -type WebhookError = unknown - -declare const sendWebhook: Effect.Effect - -declare const Metrics: { - readonly recordRetryDelay: (delay: Duration.Duration) => Effect.Effect -} - -const policy = Schedule.exponential("100 millis").pipe( - Schedule.tapOutput((delay) => Metrics.recordRetryDelay(delay)), - Schedule.both(Schedule.recurs(5)) -) - -const program = sendWebhook.pipe(Effect.retry(policy)) -``` - -Caution: `recurs(n)` counts follow-up recurrences after the first execution. - -### Observe Poll Output Statuses - -Write the Effect code for a deployment status watcher. - -```ts -import { Console, Effect, Schedule } from "effect" - -type DeploymentStatusError = unknown - -type DeploymentStatus = - | { readonly _tag: "Queued" } - | { readonly _tag: "Running" } - | { readonly _tag: "Complete" } - | { readonly _tag: "Failed" } - -declare const readDeploymentStatus: Effect.Effect - -const policy = Schedule.spaced("2 seconds").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.tapOutput((status) => Console.log(`deployment status: ${status._tag}`)), - Schedule.while(({ input }) => input._tag === "Queued" || input._tag === "Running") -) - -const program = readDeploymentStatus.pipe(Effect.repeat(policy)) -``` - -Caution: raw `Effect.repeat(schedule)` returns the schedule output; `Schedule.passthrough` preserves the latest successful value. - -## Runnable Scenario Checks - -These examples keep the recipe shape, add small deterministic operations, and use `TestClock` where time must advance. - -### Run A Timeout Retry - -Use this to check that a retryable timeout is retried and the first success is returned. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type HttpTimeout = { readonly _tag: "HttpTimeout" } -type HttpStatusError = { readonly _tag: "HttpStatusError"; readonly status: number } -type DecodeError = { readonly _tag: "DecodeError" } -type User = { readonly id: string } - -const getUser = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`getUser attempt ${attempt}`) - - if (attempt < 3) { - return yield* Effect.fail({ _tag: "HttpTimeout" } as HttpTimeout) - } - - return { id: "user-123" } satisfies User -}) - -const isHttpTimeout = ( - error: HttpTimeout | HttpStatusError | DecodeError -): error is HttpTimeout => error._tag === "HttpTimeout" - -const retryGetUserTimeouts = Schedule.exponential("10 millis").pipe( - Schedule.both(Schedule.recurs(3)) -) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* getUser(attempts).pipe( - Effect.retry({ - schedule: retryGetUserTimeouts, - while: isHttpTimeout - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("30 millis") - const user = yield* Fiber.join(fiber) - yield* Console.log(`loaded ${user.id}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// getUser attempt 1 -// getUser attempt 2 -// getUser attempt 3 -// loaded user-123 -``` - -### Stop On Permanent Failure - -Use this to check that permanent errors do not spend the retry budget. - -```ts runnable deterministic -import { Console, Effect, Schedule } from "effect" - -type SubmitError = - | { readonly _tag: "Timeout" } - | { readonly _tag: "ServiceUnavailable" } - | { readonly _tag: "ValidationError"; readonly field: string } - -const submitForm: Effect.Effect = Effect.gen(function*() { - yield* Console.log("submit attempt") - return yield* Effect.fail({ _tag: "ValidationError", field: "email" } as const) -}) - -const isRetryableSubmitError = (error: SubmitError): boolean => - error._tag === "Timeout" || error._tag === "ServiceUnavailable" - -const program = submitForm.pipe( - Effect.retry({ - schedule: Schedule.recurs(3), - while: isRetryableSubmitError - }), - Effect.exit, - Effect.flatMap((exit) => Console.log(`failed immediately: ${exit._tag === "Failure"}`)) -) - -Effect.runPromise(program) -// Output: -// submit attempt -// failed immediately: true -``` - -### Respect Retry-After - -Use this to check that a rate-limit response can stretch the selected retry delay. - -```ts runnable deterministic -import { Console, Duration, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type HttpError = - | { readonly _tag: "RateLimited"; readonly retryAfter: Duration.Duration | undefined } - | { readonly _tag: "Unauthorized" | "Forbidden" | "BadRequest" | "Unavailable" } - -const callApi = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`api attempt ${attempt}`) - - if (attempt === 1) { - return yield* Effect.fail({ - _tag: "RateLimited", - retryAfter: Duration.seconds(1) - } as HttpError) - } - - return "ok" -}) - -const rateLimitPolicy = Schedule.exponential("10 millis").pipe( - Schedule.both(Schedule.identity()), - Schedule.modifyDelay(([_, error], delay) => - Effect.succeed( - error._tag === "RateLimited" && error.retryAfter !== undefined - ? Duration.max(delay, error.retryAfter) - : delay - ) - ), - Schedule.both(Schedule.recurs(2)), - Schedule.while(({ input }) => input._tag === "RateLimited") -) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* callApi(attempts).pipe( - Effect.retry(rateLimitPolicy), - Effect.forkScoped - ) - - yield* TestClock.adjust("999 millis") - yield* Console.log(`before retry: ${yield* Ref.get(attempts)}`) - yield* TestClock.adjust("1 millis") - const result = yield* Fiber.join(fiber) - yield* Console.log(`result: ${result}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// api attempt 1 -// before retry: 1 -// api attempt 2 -// result: ok -``` - -### Reuse An Idempotency Key - -Use this to check that a retried write keeps the same logical operation identity. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type Order = { readonly id: string } -type PostOrderError = { readonly _tag: "Timeout" | "InvalidRequest" } - -const idempotencyKey = "order-key-123" - -const postOrder = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`order attempt ${attempt} key=${idempotencyKey}`) - - if (attempt < 2) { - return yield* Effect.fail({ _tag: "Timeout" } as PostOrderError) - } - - return { id: "order-1" } satisfies Order -}) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* postOrder(attempts).pipe( - Effect.retry({ - schedule: Schedule.spaced("10 millis").pipe(Schedule.both(Schedule.recurs(4))), - while: (error) => error._tag === "Timeout" - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("10 millis") - const order = yield* Fiber.join(fiber) - yield* Console.log(`created ${order.id}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// order attempt 1 key=order-key-123 -// order attempt 2 key=order-key-123 -// created order-1 -``` - -### Cap Reconnect Backoff - -Use this to check that a reconnect policy retries until the socket opens. - -```ts runnable deterministic -import { Console, Duration, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type LiveSocket = { readonly connected: true } -type WebSocketOpenError = { readonly _tag: "ConnectTimeout" | "BadHandshake" } - -const openLiveSocket = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`socket attempt ${attempt}`) - - if (attempt < 3) { - return yield* Effect.fail({ _tag: "ConnectTimeout" } as WebSocketOpenError) - } - - return { connected: true } satisfies LiveSocket -}) - -const websocketReconnectPolicy = Schedule.exponential("10 millis").pipe( - Schedule.modifyDelay((_, delay) => Effect.succeed(Duration.min(delay, Duration.seconds(5)))), - Schedule.both(Schedule.recurs(8)) -) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* openLiveSocket(attempts).pipe( - Effect.retry({ - schedule: websocketReconnectPolicy, - while: (error) => error._tag === "ConnectTimeout" - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("30 millis") - const socket = yield* Fiber.join(fiber) - yield* Console.log(`connected: ${socket.connected}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// socket attempt 1 -// socket attempt 2 -// socket attempt 3 -// connected: true -``` - -### Ask A Circuit Breaker Before Retrying - -Use this to check an effectful `while` predicate. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type BillingError = - | { readonly _tag: "TransientBillingError" } - | { readonly _tag: "RejectedPayment" } - -const callBilling = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`billing attempt ${attempt}`) - - if (attempt < 2) { - return yield* Effect.fail({ _tag: "TransientBillingError" } as BillingError) - } - - return "receipt" -}) - -const circuitBreakerAllowsRetry = (error: BillingError): Effect.Effect => - Console.log(`breaker saw ${error._tag}`).pipe(Effect.as(true)) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* callBilling(attempts).pipe( - Effect.retry({ - schedule: Schedule.spaced("10 millis").pipe(Schedule.both(Schedule.recurs(5))), - while: (error) => - error._tag === "TransientBillingError" - ? circuitBreakerAllowsRetry(error) - : Effect.succeed(false) - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("10 millis") - const receipt = yield* Fiber.join(fiber) - yield* Console.log(`result: ${receipt}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// billing attempt 1 -// breaker saw TransientBillingError -// billing attempt 2 -// result: receipt -``` - -### Poll A Job Until Terminal - -Use this to check that successful non-terminal states drive `Effect.repeat`. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type JobStatus = - | { readonly state: "queued"; readonly jobId: string } - | { readonly state: "running"; readonly jobId: string } - | { readonly state: "succeeded"; readonly jobId: string; readonly artifactUrl: string } - | { readonly state: "failed"; readonly jobId: string; readonly reason: string } - -const readJobStatus = Effect.fnUntraced(function*(reads: Ref.Ref) { - const read = yield* Ref.updateAndGet(reads, (n) => n + 1) - const status: JobStatus = read < 3 - ? { state: "running", jobId: "job-1" } - : { state: "succeeded", jobId: "job-1", artifactUrl: "/artifact" } - yield* Console.log(`job ${status.state}`) - return status -}) - -const isTerminal = (status: JobStatus): boolean => status.state === "succeeded" || status.state === "failed" - -const pollJobStatus = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => !isTerminal(input)), - Schedule.bothLeft(Schedule.during("30 seconds").pipe(Schedule.setInputType())) -) - -const program = Effect.gen(function*() { - const reads = yield* Ref.make(0) - const fiber = yield* readJobStatus(reads).pipe( - Effect.repeat(pollJobStatus), - Effect.forkScoped - ) - - yield* TestClock.adjust("2 seconds") - const status = yield* Fiber.join(fiber) - yield* Console.log(`final ${status.state}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// job running -// job running -// job succeeded -// final succeeded -``` - -### Poll Payment Settlement - -Use this to check that pending payment states are successes, not retry failures. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type SettlementStatus = - | { readonly _tag: "Pending" } - | { readonly _tag: "Processing" } - | { readonly _tag: "Settled" } - | { readonly _tag: "Declined" } - -const fetchSettlementStatus = Effect.fnUntraced(function*(reads: Ref.Ref) { - const read = yield* Ref.updateAndGet(reads, (n) => n + 1) - const status: SettlementStatus = read === 1 ? { _tag: "Pending" } : { _tag: "Settled" } - yield* Console.log(`settlement ${status._tag}`) - return status -}) - -const isOpen = (status: SettlementStatus) => status._tag === "Pending" || status._tag === "Processing" - -const pollOpenSettlements = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => isOpen(input)), - Schedule.bothLeft(Schedule.during("30 seconds")) -) - -const program = Effect.gen(function*() { - const reads = yield* Ref.make(0) - const fiber = yield* fetchSettlementStatus(reads).pipe( - Effect.repeat(pollOpenSettlements), - Effect.forkScoped - ) - - yield* TestClock.adjust("1 second") - const status = yield* Fiber.join(fiber) - yield* Console.log(`final ${status._tag}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// settlement Pending -// settlement Settled -// final Settled -``` - -### Poll Until A Cache Entry Appears - -Use this to check a successful miss-then-hit polling loop. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type CachedValue = string -type CacheLookup = - | { readonly _tag: "Miss" } - | { readonly _tag: "Hit"; readonly value: CachedValue } - -const readCacheEntry = Effect.fnUntraced(function*(reads: Ref.Ref) { - const read = yield* Ref.updateAndGet(reads, (n) => n + 1) - const lookup: CacheLookup = read < 3 ? { _tag: "Miss" } : { _tag: "Hit", value: "cached" } - yield* Console.log(`cache ${lookup._tag}`) - return lookup -}) - -const untilCacheHit = Schedule.spaced("500 millis").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input._tag === "Miss"), - Schedule.bothLeft(Schedule.during("10 seconds")) -) - -const program = Effect.gen(function*() { - const reads = yield* Ref.make(0) - const fiber = yield* readCacheEntry(reads).pipe( - Effect.repeat(untilCacheHit), - Effect.forkScoped - ) - - yield* TestClock.adjust("1 second") - const lookup = yield* Fiber.join(fiber) - yield* Console.log(`final ${lookup._tag}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// cache Miss -// cache Miss -// cache Hit -// final Hit -``` - -### Return A Timeout Result From Polling - -Use this to check that an exhausted polling budget is interpreted after `repeat`. - -```ts runnable deterministic -import { Console, Effect, Fiber, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type Status = - | { readonly _tag: "Running" } - | { readonly _tag: "Finished"; readonly value: string } - -const readStatus: Effect.Effect = Console.log("status Running").pipe( - Effect.as({ _tag: "Running" } as Status) -) - -const polling = Schedule.spaced("1 second").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.while(({ input }) => input._tag === "Running"), - Schedule.bothLeft(Schedule.recurs(1)) -) - -const program = Effect.gen(function*() { - const fiber = yield* readStatus.pipe( - Effect.repeat(polling), - Effect.flatMap((status) => - status._tag === "Running" - ? Effect.fail({ _tag: "TimedOut", latest: status } as const) - : Effect.succeed(status) - ), - Effect.exit, - Effect.forkScoped - ) - - yield* TestClock.adjust("1 second") - const exit = yield* Fiber.join(fiber) - yield* Console.log(`timed out: ${exit._tag === "Failure"}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// status Running -// status Running -// timed out: true -``` - -### Poll Fast, Then Slow - -Use this to check phased polling with `andThen`. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type WorkflowStatus = { readonly step: number } - -const readWorkflowStatus = Effect.fnUntraced(function*(reads: Ref.Ref) { - const step = yield* Ref.updateAndGet(reads, (n) => n + 1) - yield* Console.log(`workflow ${step}`) - return { step } satisfies WorkflowStatus -}) - -const fastThenSlow = Schedule.spaced("250 millis").pipe( - Schedule.bothLeft(Schedule.recurs(2)) -) -const slowPolling = Schedule.spaced("2 seconds") -const polling = fastThenSlow.pipe( - Schedule.andThen(slowPolling), - Schedule.setInputType(), - Schedule.passthrough, - Schedule.bothLeft(Schedule.recurs(3).pipe(Schedule.setInputType())) -) - -const program = Effect.gen(function*() { - const reads = yield* Ref.make(0) - const fiber = yield* readWorkflowStatus(reads).pipe( - Effect.repeat(polling), - Effect.forkScoped - ) - - yield* TestClock.adjust("3 seconds") - const status = yield* Fiber.join(fiber) - yield* Console.log(`final step ${status.step}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// workflow 1 -// workflow 2 -// workflow 3 -// workflow 4 -// final step 4 -``` - -### Repeat A Background Sync - -Use this to check that `passthrough` returns the latest successful sync summary. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type SyncSummary = { readonly pass: number } - -const syncCrmOnce = Effect.fnUntraced(function*(passes: Ref.Ref) { - const pass = yield* Ref.updateAndGet(passes, (n) => n + 1) - yield* Console.log(`sync ${pass}`) - return { pass } satisfies SyncSummary -}) - -const crmSyncCadence = Schedule.spaced("5 minutes").pipe( - Schedule.setInputType(), - Schedule.passthrough, - Schedule.bothLeft(Schedule.recurs(2).pipe(Schedule.setInputType())) -) - -const program = Effect.gen(function*() { - const passes = yield* Ref.make(0) - const fiber = yield* syncCrmOnce(passes).pipe( - Effect.repeat(crmSyncCadence), - Effect.forkScoped - ) - - yield* TestClock.adjust("10 minutes") - const summary = yield* Fiber.join(fiber) - yield* Console.log(`latest ${summary.pass}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// sync 1 -// sync 2 -// sync 3 -// latest 3 -``` - -### Repeat A Repair Pass A Few Times - -Use this to check `Schedule.take` on a spaced repeat cadence. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -const reprocessFailedBatch = Effect.fnUntraced(function*(passes: Ref.Ref) { - const pass = yield* Ref.updateAndGet(passes, (n) => n + 1) - yield* Console.log(`repair pass ${pass}`) -}) - -const reprocessingCadence = Schedule.spaced("30 seconds").pipe( - Schedule.take(3) -) - -const program = Effect.gen(function*() { - const passes = yield* Ref.make(0) - const fiber = yield* reprocessFailedBatch(passes).pipe( - Effect.repeat(reprocessingCadence), - Effect.forkScoped - ) - - yield* TestClock.adjust("90 seconds") - yield* Fiber.join(fiber) - yield* Console.log(`passes ${yield* Ref.get(passes)}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// repair pass 1 -// repair pass 2 -// repair pass 3 -// repair pass 4 -// passes 4 -``` - -### Slow A Worker Loop - -Use this to check that successful worker iterations leave a gap. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type WorkerIteration = - | { readonly _tag: "Processed"; readonly count: number } - | { readonly _tag: "Idle" } - -const workerIteration = Effect.fnUntraced(function*(iterations: Ref.Ref) { - const iteration = yield* Ref.updateAndGet(iterations, (n) => n + 1) - const result: WorkerIteration = iteration === 1 ? { _tag: "Processed", count: 1 } : { _tag: "Idle" } - yield* Console.log(`worker ${result._tag}`) - return result -}) - -const workerCadence = Schedule.spaced("500 millis").pipe( - Schedule.take(2) -) - -const program = Effect.gen(function*() { - const iterations = yield* Ref.make(0) - const fiber = yield* workerIteration(iterations).pipe( - Effect.repeat(workerCadence), - Effect.forkScoped - ) - - yield* TestClock.adjust("1 second") - yield* Fiber.join(fiber) - yield* Console.log(`iterations ${yield* Ref.get(iterations)}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// worker Processed -// worker Idle -// worker Idle -// iterations 3 -``` - -### Run One Follow-Up - -Use this to check `Schedule.duration` for exactly one additional run. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -const warmCache = Effect.fnUntraced(function*(runs: Ref.Ref) { - const run = yield* Ref.updateAndGet(runs, (n) => n + 1) - yield* Console.log(`warm ${run}`) -}) - -const oneMoreRun = Schedule.duration("10 seconds") - -const program = Effect.gen(function*() { - const runs = yield* Ref.make(0) - const fiber = yield* warmCache(runs).pipe( - Effect.repeat(oneMoreRun), - Effect.forkScoped - ) - - yield* TestClock.adjust("10 seconds") - yield* Fiber.join(fiber) - yield* Console.log(`runs ${yield* Ref.get(runs)}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// warm 1 -// warm 2 -// runs 2 -``` - -### Repeat Until A Terminal Value With Options - -Use this to check the options-form `until` predicate. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type TransferStatus = - | { readonly _tag: "Pending" } - | { readonly _tag: "InProgress" } - | { readonly _tag: "Completed" } - | { readonly _tag: "Failed" } - -const readTransferStatus = Effect.fnUntraced(function*(reads: Ref.Ref) { - const read = yield* Ref.updateAndGet(reads, (n) => n + 1) - const status: TransferStatus = read < 2 - ? { _tag: "InProgress" } - : read === 2 - ? { _tag: "Completed" } - : { _tag: "Failed" } - yield* Console.log(`transfer ${status._tag}`) - return status -}) - -const program = Effect.gen(function*() { - const reads = yield* Ref.make(0) - const fiber = yield* readTransferStatus(reads).pipe( - Effect.repeat({ - schedule: Schedule.spaced("1 second"), - until: (status) => status._tag === "Completed" || status._tag === "Failed" - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("1 second") - const status = yield* Fiber.join(fiber) - yield* Console.log(`final ${status._tag}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// transfer InProgress -// transfer Completed -// final Completed -``` - -### Repeat Until An Effectful Predicate Stops - -Use this to check an effectful `until` predicate that records every successful result. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type ServiceProbeResult = - | { readonly _tag: "Starting" } - | { readonly _tag: "Ready" } - | { readonly _tag: "Failed" } - -const probeService = Effect.fnUntraced(function*(reads: Ref.Ref) { - const read = yield* Ref.updateAndGet(reads, (n) => n + 1) - const result: ServiceProbeResult = read < 2 - ? { _tag: "Starting" } - : read === 2 - ? { _tag: "Ready" } - : { _tag: "Failed" } - yield* Console.log(`probe ${result._tag}`) - return result -}) - -const recordReadinessProbe = (result: ServiceProbeResult): Effect.Effect => Console.log(`recorded ${result._tag}`) - -const program = Effect.gen(function*() { - const reads = yield* Ref.make(0) - const fiber = yield* probeService(reads).pipe( - Effect.repeat({ - schedule: Schedule.spaced("1 second").pipe( - Schedule.both(Schedule.recurs(30)) - ), - until: (result) => - recordReadinessProbe(result).pipe( - Effect.map(() => result._tag === "Ready" || result._tag === "Failed") - ) - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("1 second") - const result = yield* Fiber.join(fiber) - yield* Console.log(`final ${result._tag}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// probe Starting -// recorded Starting -// probe Ready -// recorded Ready -// final Ready -``` - -### Observe Retry Input And Output - -Use this to check schedule diagnostics without changing retry behavior. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type InventoryError = - | { readonly _tag: "TransientInventoryError" } - | { readonly _tag: "InvalidSku" } - -const fetchInventory = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`inventory attempt ${attempt}`) - - if (attempt < 2) { - return yield* Effect.fail({ _tag: "TransientInventoryError" } as InventoryError) - } - - return ["sku-1"] -}) - -const retryInventoryPolicy = Schedule.exponential("10 millis").pipe( - Schedule.both(Schedule.recurs(5)), - Schedule.tapInput((error: InventoryError) => Console.log(`retry input: ${error._tag}`)), - Schedule.tapOutput(([_, count]) => Console.log(`retry scheduled: ${count}`)) -) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* fetchInventory(attempts).pipe( - Effect.retry({ - schedule: retryInventoryPolicy, - while: (error) => error._tag === "TransientInventoryError" - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("10 millis") - const items = yield* Fiber.join(fiber) - yield* Console.log(`items ${items.length}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// inventory attempt 1 -// retry input: TransientInventoryError -// retry scheduled: 0 -// inventory attempt 2 -// items 1 -``` - -### Log Retry Counts With `bothRight` - -Use this to check that `bothRight` keeps the recurrence count as output. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -type UploadError = - | { readonly _tag: "TransientUploadError" } - | { readonly _tag: "InvalidUpload" } - -const uploadChunk = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`upload attempt ${attempt}`) - - if (attempt < 3) { - return yield* Effect.fail({ _tag: "TransientUploadError" } as UploadError) - } -}) - -const policy = Schedule.exponential("10 millis").pipe( - Schedule.bothRight(Schedule.recurs(4)), - Schedule.tapOutput((count) => Console.log(`retry count ${count}`)) -) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* uploadChunk(attempts).pipe( - Effect.retry({ - schedule: policy, - while: (error) => error._tag === "TransientUploadError" - }), - Effect.forkScoped - ) - - yield* TestClock.adjust("30 millis") - yield* Fiber.join(fiber) - yield* Console.log(`done after ${yield* Ref.get(attempts)}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// upload attempt 1 -// retry count 0 -// upload attempt 2 -// retry count 1 -// upload attempt 3 -// done after 3 -``` - -## Timing Checks - -Use `TestClock` when the timing behavior itself is the recipe contract. Fork the repeated or retried program, -advance virtual time, then check the result. - -The examples fork the program because the program is waiting for time to pass. While the fiber is suspended, `TestClock.adjust` moves virtual time forward. `Fiber.join` then waits for the forked program to finish after the expected retries or repeats have run. - -### Assert Retry Delay - -Use when a retry policy must not run before its configured delay. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -const ensure = (condition: boolean, message: string) => condition ? Effect.void : Effect.fail(new Error(message)) - -const retryPolicy = Schedule.spaced("100 millis").pipe( - Schedule.both(Schedule.recurs(2)) -) - -const operation = Effect.fnUntraced(function*(attempts: Ref.Ref) { - const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) - yield* Console.log(`attempt ${attempt}`) - - if (attempt < 3) { - return yield* Effect.fail("transient" as const) - } - - return "ok" as const -}) - -const program = Effect.gen(function*() { - const attempts = yield* Ref.make(0) - const fiber = yield* operation(attempts).pipe( - Effect.retry(retryPolicy), - Effect.forkScoped - ) - - yield* Effect.yieldNow - yield* TestClock.adjust("99 millis") - yield* ensure((yield* Ref.get(attempts)) === 1, "retry ran too early") - - yield* TestClock.adjust("1 millis") - yield* ensure((yield* Ref.get(attempts)) === 2, "first retry did not run") - - yield* TestClock.adjust("100 millis") - const result = yield* Fiber.join(fiber) - yield* ensure(result === "ok", "retry did not succeed") - yield* Console.log(`result: ${result}`) -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// attempt 1 -// attempt 2 -// attempt 3 -// result: ok -``` - -### Assert Fixed Cadence - -Use when the repeat policy should stay on aligned intervals instead of waiting after each run completes. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -const ensure = (condition: boolean, message: string) => condition ? Effect.void : Effect.fail(new Error(message)) - -const heartbeatCadence = Schedule.fixed("1 second").pipe( - Schedule.take(2) -) - -const heartbeat = Effect.fnUntraced(function*(ticks: Ref.Ref) { - const tick = yield* Ref.updateAndGet(ticks, (n) => n + 1) - yield* Console.log(`heartbeat ${tick}`) -}) - -const program = Effect.gen(function*() { - const ticks = yield* Ref.make(0) - const fiber = yield* heartbeat(ticks).pipe( - Effect.repeat(heartbeatCadence), - Effect.forkScoped - ) - - yield* Effect.yieldNow - yield* TestClock.adjust("1 second") - yield* TestClock.adjust("1 second") - yield* Fiber.join(fiber) - yield* ensure((yield* Ref.get(ticks)) === 3, "expected one initial run and two fixed recurrences") -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// heartbeat 1 -// heartbeat 2 -// heartbeat 3 -``` - -### Assert Windowed Cadence - -Use when the repeat policy should recur on wall-clock windows. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -const ensure = (condition: boolean, message: string) => condition ? Effect.void : Effect.fail(new Error(message)) - -const flushWindow = Schedule.windowed("1 minute").pipe( - Schedule.take(2) -) - -const flushMetrics = Effect.fnUntraced(function*(flushes: Ref.Ref) { - const flush = yield* Ref.updateAndGet(flushes, (n) => n + 1) - yield* Console.log(`flush ${flush}`) -}) - -const program = Effect.gen(function*() { - yield* TestClock.setTime(0) - const flushes = yield* Ref.make(0) - const fiber = yield* flushMetrics(flushes).pipe( - Effect.repeat(flushWindow), - Effect.forkScoped - ) - - yield* Effect.yieldNow - yield* TestClock.adjust("1 minute") - yield* TestClock.adjust("1 minute") - yield* Fiber.join(fiber) - yield* ensure((yield* Ref.get(flushes)) === 3, "expected one initial flush and two windowed recurrences") -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// flush 1 -// flush 2 -// flush 3 -``` - -### Assert Cron Cadence - -Use when the repeat policy should recur at times selected by a cron expression. - -```ts runnable deterministic -import { Console, Effect, Fiber, Ref, Schedule } from "effect" -import { TestClock } from "effect/testing" - -const ensure = (condition: boolean, message: string) => condition ? Effect.void : Effect.fail(new Error(message)) - -const nightly = Schedule.cron("0 2 * * *").pipe( - Schedule.take(1) -) - -const compactDatabase = Effect.fnUntraced(function*(runs: Ref.Ref) { - const run = yield* Ref.updateAndGet(runs, (n) => n + 1) - yield* Console.log(`compaction ${run}`) -}) - -const program = Effect.gen(function*() { - yield* TestClock.setTime(new Date(2024, 0, 1, 1, 59, 59).getTime()) - const runs = yield* Ref.make(0) - const fiber = yield* compactDatabase(runs).pipe( - Effect.repeat(nightly), - Effect.forkScoped - ) - - yield* Effect.yieldNow - yield* TestClock.adjust("1 second") - yield* Fiber.join(fiber) - yield* ensure((yield* Ref.get(runs)) === 2, "expected initial run and one cron recurrence") -}) - -Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()), Effect.scoped)) -// Output: -// compaction 1 -// compaction 2 ``` diff --git a/.context/effect/migration/schema.md b/.context/effect/migration/schema.md index bab73d366..98c2f046b 100644 --- a/.context/effect/migration/schema.md +++ b/.context/effect/migration/schema.md @@ -100,6 +100,40 @@ Note: `positive`, `negative`, `nonNegative`, `nonPositive` have been removed in ## Detailed migrations +### Redacted + +**Migration: rename with behavior distinction** + +In v3, `Schema.Redacted(value)` decoded the raw encoded value and wrapped the decoded value in `Redacted`. + +In v4, that behavior is named `Schema.RedactedFromValue(value)`. + +v3 + +```ts +import { Schema } from "effect" + +const schema = Schema.Redacted(Schema.String) +const decode = Schema.decodeSync(schema) + +decode("secret") +``` + +v4 + +```ts +import { Redacted, Schema } from "effect" + +const schema = Schema.RedactedFromValue(Schema.String) +const decode = Schema.decodeSync(schema) + +const redacted = decode("secret") +console.log(Redacted.value(redacted)) +// secret +``` + +`Schema.Redacted(value)` in v4 is the replacement for v3 `Schema.RedactedFromSelf(value)`: it expects the input to already be a `Redacted` value, so both `Type` and `Encoded` are `Redacted<...>`. + ### asserts signature **Migration: semi-auto** diff --git a/.context/effect/package.json b/.context/effect/package.json index 1d018d49e..d7cd5b113 100644 --- a/.context/effect/package.json +++ b/.context/effect/package.json @@ -8,13 +8,15 @@ "codemod": "node scripts/codemod.mjs", "build": "tspc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build", "build:tsgo": "tsgo -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build:tsgo", + "bundle-analyze": "bash scripts/bundle-analyze.sh", "bundle-compare": "bash scripts/bundle-compare.sh", + "bundle-compare-selected": "bash scripts/bundle-compare-selected.sh", "circular": "node scripts/circular.mjs", "test": "vitest", "coverage": "vitest --coverage", "check": "tspc -b tsconfig.json", "check:tsgo": "tsgo -b tsconfig.json", - "check:cookbook:schedule": "node scripts/check-cookbook-schedule.ts cookbooks/schedule.md", + "typeperf": "pnpm --dir packages/effect exec node typeperf/run.mjs", "check-recursive": "pnpm --recursive --filter \"./packages/**/*\" exec tspc -b tsconfig.json", "jsdocs": "effect-jsdocs", "lint": "pnpm jsdocs && oxlint -f unix && dprint check", diff --git a/.context/effect/packages/ai/anthropic/CHANGELOG.md b/.context/effect/packages/ai/anthropic/CHANGELOG.md index 427af7a9f..5c43c2385 100644 --- a/.context/effect/packages/ai/anthropic/CHANGELOG.md +++ b/.context/effect/packages/ai/anthropic/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/ai-anthropic +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- [#2450](https://github.com/Effect-TS/effect-smol/pull/2450) [`7b8a549`](https://github.com/Effect-TS/effect-smol/commit/7b8a5493950aa6f9e20024cd7e588838722218f0) Thanks @DomiR! - Fix non-streaming Anthropic responses throwing when a tool call carries `caller` metadata. The mapper emitted `caller.toolId: undefined`, but `ProviderMetadata` is `Record(String, NullOr(Json))` and `undefined` is not a valid Json value, so decoding the model's own response threw `Expected JSON value`. Emit `null` instead, matching the streaming mappers. + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/ai/anthropic/package.json b/.context/effect/packages/ai/anthropic/package.json index 307e48843..4c91ff458 100644 --- a/.context/effect/packages/ai/anthropic/package.json +++ b/.context/effect/packages/ai/anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-anthropic", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "An Anthropic provider integration for Effect AI SDK", diff --git a/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts index e9da3df7d..516fae12a 100644 --- a/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts +++ b/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -1620,7 +1620,7 @@ const makeResponse = Effect.fnUntraced( const callerInfo = Predicate.isNotNullish(caller) ? { type: caller.type, - toolId: "tool_id" in caller ? caller.tool_id : undefined + toolId: "tool_id" in caller ? caller.tool_id : null } : undefined @@ -3028,13 +3028,13 @@ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError }) }) -const tryCodecTransform = (schema: S, method: string) => +const tryCodecTransform = (schema: S, method: string) => Effect.try({ try: () => toCodecAnthropic(schema), catch: (error) => unsupportedSchemaError(error, method) }) -const tryJsonSchema = (schema: S, method: string) => +const tryJsonSchema = (schema: S, method: string) => Effect.try({ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecAnthropic }), catch: (error) => unsupportedSchemaError(error, method) diff --git a/.context/effect/packages/ai/anthropic/src/Generated.ts b/.context/effect/packages/ai/anthropic/src/Generated.ts index 1d2ebcb0e..7a7a9470c 100644 --- a/.context/effect/packages/ai/anthropic/src/Generated.ts +++ b/.context/effect/packages/ai/anthropic/src/Generated.ts @@ -11088,10 +11088,10 @@ export const make = ( Stream.unwrap ) const decodeSuccess = - (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => HttpClientResponse.schemaBodyJson(schema)(response) const decodeError = - (tag: Tag, schema: Schema) => + (tag: Tag, schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap( HttpClientResponse.schemaBodyJson(schema)(response), diff --git a/.context/effect/packages/ai/openai-compat/CHANGELOG.md b/.context/effect/packages/ai/openai-compat/CHANGELOG.md index f6b15ecb6..d99c6ff05 100644 --- a/.context/effect/packages/ai/openai-compat/CHANGELOG.md +++ b/.context/effect/packages/ai/openai-compat/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/ai-openai-compat +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/ai/openai-compat/package.json b/.context/effect/packages/ai/openai-compat/package.json index df2d43d49..b03d0d377 100644 --- a/.context/effect/packages/ai/openai-compat/package.json +++ b/.context/effect/packages/ai/openai-compat/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai-compat", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "An OpenAI compat integration for Effect", diff --git a/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts b/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts index ab309e98f..acce3af13 100644 --- a/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts +++ b/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts @@ -1394,7 +1394,7 @@ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError }) }) -const tryJsonSchema = (schema: S, method: string) => +const tryJsonSchema = (schema: S, method: string) => Effect.try({ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }), catch: (error) => unsupportedSchemaError(error, method) diff --git a/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts b/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts index da7855d88..a67365d98 100644 --- a/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts +++ b/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts @@ -1,11 +1,5 @@ import type * as Response from "effect/unstable/ai/Response" -/** @internal */ -export const ProviderOptionsKey = "@effect/ai-openai-compat/OpenAiLanguageModel/ProviderOptions" - -/** @internal */ -export const ProviderMetadataKey = "@effect/ai-openai-compat/OpenAiLanguageModel/ProviderMetadata" - const finishReasonMap: Record = { content_filter: "content-filter", function_call: "tool-calls", @@ -14,9 +8,6 @@ const finishReasonMap: Record = { tool_calls: "tool-calls" } -/** @internal */ -export const escapeJSONDelta = (delta: string): string => JSON.stringify(delta).slice(1, -1) - /** @internal */ export const resolveFinishReason = ( finishReason: string | null | undefined, diff --git a/.context/effect/packages/ai/openai/CHANGELOG.md b/.context/effect/packages/ai/openai/CHANGELOG.md index 42ef2dcf3..7af6506cc 100644 --- a/.context/effect/packages/ai/openai/CHANGELOG.md +++ b/.context/effect/packages/ai/openai/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/ai-openai +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/ai/openai/package.json b/.context/effect/packages/ai/openai/package.json index 6346f6506..156077656 100644 --- a/.context/effect/packages/ai/openai/package.json +++ b/.context/effect/packages/ai/openai/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "An OpenAI provider integration for Effect AI SDK", diff --git a/.context/effect/packages/ai/openai/src/Generated.ts b/.context/effect/packages/ai/openai/src/Generated.ts index 9e25b668e..d7189bf5d 100644 --- a/.context/effect/packages/ai/openai/src/Generated.ts +++ b/.context/effect/packages/ai/openai/src/Generated.ts @@ -30435,7 +30435,7 @@ export const make = ( Type, DecodingServices >( - schema: Schema.Decoder + schema: Schema.ConstraintDecoder ) => ( request: HttpClientRequest.HttpClientRequest @@ -30458,10 +30458,10 @@ export const make = ( Stream.unwrap ) const decodeSuccess = - (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => HttpClientResponse.schemaBodyJson(schema)(response) const decodeError = - (tag: Tag, schema: Schema) => + (tag: Tag, schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap( HttpClientResponse.schemaBodyJson(schema)(response), diff --git a/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts b/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts index f5903263e..7930d3d59 100644 --- a/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts +++ b/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -2890,13 +2890,13 @@ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError }) }) -const tryCodecTransform = (schema: S, method: string) => +const tryCodecTransform = (schema: S, method: string) => Effect.try({ try: () => toCodecOpenAI(schema), catch: (error) => unsupportedSchemaError(error, method) }) -const tryJsonSchema = (schema: S, method: string) => +const tryJsonSchema = (schema: S, method: string) => Effect.try({ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }), catch: (error) => unsupportedSchemaError(error, method) diff --git a/.context/effect/packages/ai/openai/src/internal/utilities.ts b/.context/effect/packages/ai/openai/src/internal/utilities.ts index fbb32dd08..1aca7dae4 100644 --- a/.context/effect/packages/ai/openai/src/internal/utilities.ts +++ b/.context/effect/packages/ai/openai/src/internal/utilities.ts @@ -1,11 +1,5 @@ import type * as Response from "effect/unstable/ai/Response" -/** @internal */ -export const ProviderOptionsKey = "@effect/ai-openai/OpenAiLanguageModel/ProviderOptions" - -/** @internal */ -export const ProviderMetadataKey = "@effect/ai-openai/OpenAiLanguageModel/ProviderMetadata" - const finishReasonMap: Record = { content_filter: "content-filter", function_call: "tool-calls", diff --git a/.context/effect/packages/ai/openrouter/CHANGELOG.md b/.context/effect/packages/ai/openrouter/CHANGELOG.md index 3a700a748..8e05c1f91 100644 --- a/.context/effect/packages/ai/openrouter/CHANGELOG.md +++ b/.context/effect/packages/ai/openrouter/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/ai-openrouter +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/ai/openrouter/package.json b/.context/effect/packages/ai/openrouter/package.json index 8ea6097bd..20cb353c1 100644 --- a/.context/effect/packages/ai/openrouter/package.json +++ b/.context/effect/packages/ai/openrouter/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openrouter", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "An OpenRouter provider integration for Effect AI SDK", diff --git a/.context/effect/packages/ai/openrouter/src/Generated.ts b/.context/effect/packages/ai/openrouter/src/Generated.ts index 8cdb32d42..c015b4429 100644 --- a/.context/effect/packages/ai/openrouter/src/Generated.ts +++ b/.context/effect/packages/ai/openrouter/src/Generated.ts @@ -8923,7 +8923,7 @@ export const make = ( Type, DecodingServices >( - schema: Schema.Decoder + schema: Schema.ConstraintDecoder ) => ( request: HttpClientRequest.HttpClientRequest @@ -8939,10 +8939,10 @@ export const make = ( Stream.pipeThroughChannel(Sse.decodeDataSchema(schema)) ) const decodeSuccess = - (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => HttpClientResponse.schemaBodyJson(schema)(response) const decodeError = - (tag: Tag, schema: Schema) => + (tag: Tag, schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap( HttpClientResponse.schemaBodyJson(schema)(response), diff --git a/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts index 94c6aebc4..f937ca4e7 100644 --- a/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts +++ b/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -1766,7 +1766,7 @@ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError }) }) -const tryJsonSchema = ( +const tryJsonSchema = ( schema: S, method: string, transformer: LanguageModel.CodecTransformer diff --git a/.context/effect/packages/atom/react/CHANGELOG.md b/.context/effect/packages/atom/react/CHANGELOG.md index 58e887810..e3d3ecaed 100644 --- a/.context/effect/packages/atom/react/CHANGELOG.md +++ b/.context/effect/packages/atom/react/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/atom-react +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/atom/react/package.json b/.context/effect/packages/atom/react/package.json index b0468c956..9a4fc7228 100644 --- a/.context/effect/packages/atom/react/package.json +++ b/.context/effect/packages/atom/react/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-react", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "React bindings for the Effect Atom modules", diff --git a/.context/effect/packages/atom/solid/CHANGELOG.md b/.context/effect/packages/atom/solid/CHANGELOG.md index 6bf9a3bd9..4547bf3f8 100644 --- a/.context/effect/packages/atom/solid/CHANGELOG.md +++ b/.context/effect/packages/atom/solid/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/atom-solid +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/atom/solid/package.json b/.context/effect/packages/atom/solid/package.json index e163afbd7..3399c9ec1 100644 --- a/.context/effect/packages/atom/solid/package.json +++ b/.context/effect/packages/atom/solid/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-solid", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "SolidJS bindings for the Effect Atom modules", diff --git a/.context/effect/packages/atom/vue/CHANGELOG.md b/.context/effect/packages/atom/vue/CHANGELOG.md index f00e1adb2..f87de75e7 100644 --- a/.context/effect/packages/atom/vue/CHANGELOG.md +++ b/.context/effect/packages/atom/vue/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/atom-vue +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/atom/vue/package.json b/.context/effect/packages/atom/vue/package.json index f3a074bfd..2245b8f3f 100644 --- a/.context/effect/packages/atom/vue/package.json +++ b/.context/effect/packages/atom/vue/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-vue", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "Vue bindings for the Effect Atom modules", diff --git a/.context/effect/packages/effect/CHANGELOG.md b/.context/effect/packages/effect/CHANGELOG.md index cff7e78d5..cfe741023 100644 --- a/.context/effect/packages/effect/CHANGELOG.md +++ b/.context/effect/packages/effect/CHANGELOG.md @@ -1,5 +1,115 @@ # effect +## 4.0.0-beta.93 + +### Patch Changes + +- [#2512](https://github.com/Effect-TS/effect-smol/pull/2512) [`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677) Thanks @gcanti! - Preserve content schema identifiers when emitting JSON Schema for `Schema.fromJsonString`. + + This keeps user-defined identifiers attached to the decoded JSON payload while giving the generated JSON string wrapper its own derived name, avoiding client codegen outputs where the payload type is renamed behind the transport wrapper. + +- [#2492](https://github.com/Effect-TS/effect-smol/pull/2492) [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3) Thanks @maxprilutskiy! - Map HttpApi json defects to SchemaError + +- [#2519](https://github.com/Effect-TS/effect-smol/pull/2519) [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc) Thanks @tim-smart! - Fix structural equality for request-style values when structural hashes collide. + +- [#2507](https://github.com/Effect-TS/effect-smol/pull/2507) [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e) Thanks @tim-smart! - ensure handler errors don't cause httpapi security middleware to fallback + +- [#2518](https://github.com/Effect-TS/effect-smol/pull/2518) [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f) Thanks @tim-smart! - Move `UrlParams.makeUrl` to `Url.make` and return `Url.UrlError` for URL construction failures. + +- [#2505](https://github.com/Effect-TS/effect-smol/pull/2505) [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c) Thanks @KhraksMamtsov! - accept UrlParams.Input in some UrlParams apis + +## 4.0.0-beta.92 + +### Patch Changes + +- [#2501](https://github.com/Effect-TS/effect-smol/pull/2501) [`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9) Thanks @gcanti! - Fix excess property handling in schema-backed class constructors, closes [#2499](https://github.com/Effect-TS/effect-smol/issues/2499). + +## 4.0.0-beta.91 + +### Patch Changes + +- [#2498](https://github.com/Effect-TS/effect-smol/pull/2498) [`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075) Thanks @gcanti! - Fix `Schedule.andThenResult` to emit `self` outputs as `Failure` and `other` outputs as `Success`, closes [#2497](https://github.com/Effect-TS/effect-smol/issues/2497). + +- [#2488](https://github.com/Effect-TS/effect-smol/pull/2488) [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150) Thanks @fubhy! - Fix `String.camelCase` and `String.pascalCase` handling of numeric word segments, and add `String.configCase` for configuration key casing. + +- [#2485](https://github.com/Effect-TS/effect-smol/pull/2485) [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6) Thanks @tim-smart! - fix RequestResolver interruption + +## 4.0.0-beta.90 + +### Patch Changes + +- [#2483](https://github.com/Effect-TS/effect-smol/pull/2483) [`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5) Thanks @tim-smart! - Fix `Config.schema` so missing array values are treated as missing data, allowing `Config.withDefault` to apply. + +## 4.0.0-beta.89 + +### Patch Changes + +- [#2475](https://github.com/Effect-TS/effect-smol/pull/2475) [`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad) Thanks @tim-smart! - Update `Schema.Void` to model ignored `void` return values. + + Runtime parsing now accepts any present value and discards it as `undefined`. + This matches TypeScript `void` return values, where callers do not observe the + returned value. Use `Schema.Undefined` when the input must be exactly + `undefined`. + +- [#2479](https://github.com/Effect-TS/effect-smol/pull/2479) [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669) Thanks @tim-smart! - Add custom error callbacks to Effect.fromOption. + +- [#2480](https://github.com/Effect-TS/effect-smol/pull/2480) [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6) Thanks @tim-smart! - render causes in OtlpTracer exception events + +## 4.0.0-beta.88 + +### Patch Changes + +- [#2472](https://github.com/Effect-TS/effect-smol/pull/2472) [`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b) Thanks @tim-smart! - Add adaptive consume and feedback operations to the unstable persistent RateLimiterStore API, including in-memory and Redis-backed bounded cooldown, learning, learned pacing, and expiry behavior for 429 Retry-After feedback. + +- [#2457](https://github.com/Effect-TS/effect-smol/pull/2457) [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4) Thanks @P0lip! - Localize missing rpc method errors to the provided request id + +- [#2428](https://github.com/Effect-TS/effect-smol/pull/2428) [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da) Thanks @MrGovindan! - Add `isOpen` to `Latch` to allow querying the latch's open state + +## 4.0.0-beta.87 + +### Patch Changes + +- [#2468](https://github.com/Effect-TS/effect-smol/pull/2468) [`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956) Thanks @gcanti! - Expose the original input schema on `Schema.toType`, `Schema.toEncoded`, `Schema.toCodecJson`, and `Schema.toCodecStringTree` results via the `schema` property. This aligns these schema wrappers with other wrappers that retain their source schema for type-level and runtime introspection. + +- [#2466](https://github.com/Effect-TS/effect-smol/pull/2466) [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed) Thanks @gcanti! - Use `URL.canParse` to validate URL string schema decoding before constructing a `URL`. This avoids relying on thrown exceptions for routine validation while preserving the same invalid URL issue and successful decode output. + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- [#2455](https://github.com/Effect-TS/effect-smol/pull/2455) [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e) Thanks @fubhy! - Fix `Cron.next` skipping earlier matching days when the upcoming day-of-month does not exist in the current month. + +- [#2454](https://github.com/Effect-TS/effect-smol/pull/2454) [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab) Thanks @StarpTech! - Exclude response metadata from HTTP server span failures after response headers have been sent. + +- [#2449](https://github.com/Effect-TS/effect-smol/pull/2449) [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc) Thanks @gcanti! - Fix Schema handling of encoded-side checks for container ASTs. + + Checks added after `flip` are now preserved as `encodingChecks` across + `Declaration`, `Arrays`, `Objects`, and `Union`, even when rebuilding the AST + does not change child nodes. `toType` now projects those checks consistently, + and parsing applies encoded-side checks to the local encoded value when an + encoding chain is present without allowing encoded-side `parseOptions` + annotations to affect the current parser side. + +- [#2446](https://github.com/Effect-TS/effect-smol/pull/2446) [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9) Thanks @IMax153! - Allow schemas provided to CLI flags / arguments to utilize the environment required by the CLI + +- [#2452](https://github.com/Effect-TS/effect-smol/pull/2452) [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12) Thanks @gcanti! - Remove the `keepDeclarations` option from `Schema.toCodecStringTree`. + +- [#2461](https://github.com/Effect-TS/effect-smol/pull/2461) [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46) Thanks @tim-smart! - Fail RpcClient HTTP requests with a defect when the response stream closes before the request receives a terminal response. + +- [#2442](https://github.com/Effect-TS/effect-smol/pull/2442) [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9) Thanks @gcanti! - Improve Schema type-level performance by lazily computing schema views, + specializing common struct projections, and using lighter schema constraints at + API boundaries that do not need the full schema protocol. + + This also adds the Schema type-performance benchmark suite, introduces + `Schema.toCodecArrayFromSingle`, preserves canonical StringTree array codecs, + renames the arbitrary-generation annotation constraint for clarity, and updates + affected codec, parser, channel, SQL, HTTP API, persistence, RPC, AI, OpenAPI, + and workflow typings to match the refined Schema surface. + +- [#2464](https://github.com/Effect-TS/effect-smol/pull/2464) [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e) Thanks @tim-smart! - do not use performance.timeOrigin and calculate origins lazily + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/effect/SCHEMA.md b/.context/effect/packages/effect/SCHEMA.md index acbfa4565..041a31d41 100644 --- a/.context/effect/packages/effect/SCHEMA.md +++ b/.context/effect/packages/effect/SCHEMA.md @@ -4646,7 +4646,7 @@ console.log(String(Schema.decodeUnknownExit(schema)(formData))) // Success({"a":"1","b":{"c":"2","d":"3"}}) ``` -If you want to decode values that are not strings, use `Schema.toCodecStringTree` with the `keepDeclarations: true` option. This serializer preserves values such as numbers and `Blob` objects when compatible with the schema. +If you want to decode string fields into non-string primitive values, use `Schema.toCodecStringTree`. **Example** (Parsing non-string values) @@ -4657,8 +4657,7 @@ const schema = Schema.fromFormData( Schema.toCodecStringTree( Schema.Struct({ a: Schema.Int - }), - { keepDeclarations: true } + }) ) ) @@ -4720,7 +4719,7 @@ console.log(String(Schema.decodeUnknownExit(schema)(urlSearchParams))) // Success({"a":"1","b":{"c":"2","d":"3"}}) ``` -If you want to decode values that are not strings, use `Schema.toCodecStringTree` with the `keepDeclarations: true` option. This serializer preserves values such as numbers or declarations when compatible with the schema. +If you want to decode values that are not strings, use `Schema.toCodecStringTree`. This serializer preserves values such as numbers when compatible with the schema. **Example** (Parsing non-string values) @@ -4731,8 +4730,7 @@ const schema = Schema.fromURLSearchParams( Schema.toCodecStringTree( Schema.Struct({ a: Schema.Int - }), - { keepDeclarations: true } + }) ) ) @@ -4995,31 +4993,6 @@ console.log(stringTree) // [ '1', '2' ] ``` -#### keepDeclarations: true - -The `keepDeclarations: true` option behaves like the StringTree codec, but it does **not** convert declarations without a `toCodecJson` annotation to `undefined`. Instead, it keeps them as they are. - -This is usefult for example when you encode a schema to a `FormData` format and you want to preserve `Blob` values. - -```ts -import { Schema } from "effect" - -const schema = Schema.Struct({ - a: Schema.instanceOf(URL), - b: Schema.Number -}) - -const stringTree = Schema.toCodecStringTree(schema, { keepDeclarations: true }) - -console.log( - Schema.encodeUnknownSync(stringTree)({ - a: new URL("https://effect.website"), - b: 1 - }) -) -// { a: URL("https://effect.website"), b: '1' } -``` - ### ISO Canonical Codec The ISO canonical codec (`toCodecIso`) converts schemas to their `Iso` representation. This is useful when you want to build isomorphic transformations or optics. diff --git a/.context/effect/packages/effect/package.json b/.context/effect/packages/effect/package.json index b004ea038..2862070e5 100644 --- a/.context/effect/packages/effect/package.json +++ b/.context/effect/packages/effect/package.json @@ -1,7 +1,7 @@ { "name": "effect", "type": "module", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "license": "MIT", "description": "The missing standard library for TypeScript, for writing production-grade software.", "homepage": "https://effect.website", diff --git a/.context/effect/packages/effect/src/Cause.ts b/.context/effect/packages/effect/src/Cause.ts index 5cc96c473..2d0fda116 100644 --- a/.context/effect/packages/effect/src/Cause.ts +++ b/.context/effect/packages/effect/src/Cause.ts @@ -1108,7 +1108,9 @@ export const filterInterruptors: (self: Cause) => Result.Result(self: Cause) => Array = effect.causePrettyErrors +export const prettyErrors: (self: Cause, options?: { + readonly includeCauseInStack?: boolean | undefined +}) => Array = effect.causePrettyErrors /** * Formats a `Cause` as a human-readable string for logging or debugging. diff --git a/.context/effect/packages/effect/src/ChannelSchema.ts b/.context/effect/packages/effect/src/ChannelSchema.ts index d2ab62c57..fb2b4864c 100644 --- a/.context/effect/packages/effect/src/ChannelSchema.ts +++ b/.context/effect/packages/effect/src/ChannelSchema.ts @@ -33,7 +33,7 @@ import * as Schema from "./Schema.ts" * @category constructors * @since 4.0.0 */ -export const encode = ( +export const encode = ( schema: S ) => (): Channel.Channel< @@ -63,7 +63,7 @@ export const encode = ( * @category constructors * @since 4.0.0 */ -export const encodeUnknown: ( +export const encodeUnknown: ( schema: S ) => () => Channel.Channel< Arr.NonEmptyReadonlyArray, @@ -95,7 +95,7 @@ export const encodeUnknown: ( * @category constructors * @since 4.0.0 */ -export const decode = ( +export const decode = ( schema: S ) => (): Channel.Channel< @@ -130,7 +130,7 @@ export const decode = ( * @category constructors * @since 4.0.0 */ -export const decodeUnknown: ( +export const decodeUnknown: ( schema: S ) => () => Channel.Channel< Arr.NonEmptyReadonlyArray, @@ -166,7 +166,7 @@ export const decodeUnknown: ( * @since 4.0.0 */ export const duplex: { - (options: { + (options: { readonly inputSchema: In readonly outputSchema: Out }): ( @@ -188,7 +188,7 @@ export const duplex: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > - ( + ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, OutErr, @@ -211,7 +211,7 @@ export const duplex: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > -} = dual(2, ( +} = dual(2, ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, OutErr, @@ -260,7 +260,7 @@ export const duplex: { * @since 4.0.0 */ export const duplexUnknown: { - (options: { + (options: { readonly inputSchema: In readonly outputSchema: Out }): ( @@ -282,7 +282,7 @@ export const duplexUnknown: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > - ( + ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, OutErr, diff --git a/.context/effect/packages/effect/src/Config.ts b/.context/effect/packages/effect/src/Config.ts index 221745f2b..e22371cfb 100644 --- a/.context/effect/packages/effect/src/Config.ts +++ b/.context/effect/packages/effect/src/Config.ts @@ -531,6 +531,8 @@ const recur: ( function*(ast, provider, path) { switch (ast._tag) { case "Objects": { + const stat = yield* provider.load(path) + if (stat === undefined && path.length > 0) return undefined const out: Record = {} for (const ps of ast.propertySignatures) { const name = ps.name @@ -540,7 +542,6 @@ const recur: ( } } if (ast.indexSignatures.length > 0) { - const stat = yield* provider.load(path) if (stat && stat._tag === "Record") { for (const is of ast.indexSignatures) { const matches = SchemaParser._is(is.parameter) @@ -557,10 +558,18 @@ const recur: ( } case "Arrays": { const stat = yield* provider.load(path) - if (stat && stat._tag === "Value") return stat.value + if (stat === undefined) return undefined + if (stat && stat._tag === "Value") return stat.value === "" ? [] : stat.value.split(",") + if (stat && stat._tag === "Array" && stat.value !== undefined) { + return stat.value === "" ? [] : stat.value.split(",") + } const out: Array = [] - for (let i = 0; i < ast.elements.length; i++) { - out.push(yield* recur(ast.elements[i], provider, [...path, i])) + const length = stat && stat._tag === "Array" ? stat.length : ast.elements.length + for (let i = 0; i < length; i++) { + const element = ast.elements[i] ?? ast.rest[0] + if (element !== undefined) { + out.push(yield* recur(element, provider, [...path, i])) + } } return out } @@ -760,7 +769,7 @@ export const LogLevel = Schema.Literals(LogLevel_.values) * @category schemas * @since 4.0.0 */ -export const Record = (key: K, value: V, options?: { +export const Record = (key: K, value: V, options?: { readonly separator?: string | undefined readonly keyValueSeparator?: string | undefined }) => { @@ -780,7 +789,7 @@ export const Record = (key: K * @category schemas * @since 4.0.0 */ -const ArrayConfig = (value: V, options?: { +const ArrayConfig = (value: V, options?: { readonly separator?: string | undefined }) => { const array = Schema.Array(value) diff --git a/.context/effect/packages/effect/src/ConfigProvider.ts b/.context/effect/packages/effect/src/ConfigProvider.ts index ec718c946..bcdb08244 100644 --- a/.context/effect/packages/effect/src/ConfigProvider.ts +++ b/.context/effect/packages/effect/src/ConfigProvider.ts @@ -511,8 +511,9 @@ export const mapInput: { * * **Details** * - * Numeric segments are left unchanged. This is a specialization of - * {@link mapInput}. + * Numeric segments are left unchanged. String segments use `String.configCase` + * so numeric word groups such as `v2` are preserved for environment variable + * names. This is a specialization of {@link mapInput}. * * **Example** (Resolving camelCase keys to env vars) * @@ -532,7 +533,7 @@ export const mapInput: { * @since 2.0.0 */ export const constantCase: (self: ConfigProvider) => ConfigProvider = mapInput((path) => - path.map((seg) => typeof seg === "number" ? seg : Str.constantCase(seg)) + path.map((seg) => typeof seg === "number" ? seg : Str.configCase(seg)) ) /** diff --git a/.context/effect/packages/effect/src/Cron.ts b/.context/effect/packages/effect/src/Cron.ts index 2b631d2b8..4c6b6dc6f 100644 --- a/.context/effect/packages/effect/src/Cron.ts +++ b/.context/effect/packages/effect/src/Cron.ts @@ -868,6 +868,10 @@ const stepCron = (cron: Cron, now: DateTime.DateTime.Input | undefined, directio } else { b = daysInMonth(current) - currentDay + boundary.day } + } else if (!reverse && nextDay > daysInMonth(current)) { + // The next matching day does not exist in the current month. Setting it + // directly would overflow and skip earlier matching days next month. + b = daysInMonth(current) - currentDay + boundary.day } else { b = nextDay - currentDay } diff --git a/.context/effect/packages/effect/src/Effect.ts b/.context/effect/packages/effect/src/Effect.ts index 4ec77e2f4..f15b5f49f 100644 --- a/.context/effect/packages/effect/src/Effect.ts +++ b/.context/effect/packages/effect/src/Effect.ts @@ -1791,7 +1791,9 @@ export const fromResult: (result: Result.Result) => Effect = i * **Details** * * `Option.some` becomes a successful effect with the contained value, while - * `Option.none` becomes a failed effect with `NoSuchElementError`. + * `Option.none` becomes a failed effect. By default the failure is a + * `NoSuchElementError`, but you can provide an `onNone` callback to customize + * the error value. * * **Example** (Converting an Option into an Effect) * @@ -1803,6 +1805,7 @@ export const fromResult: (result: Result.Result) => Effect = i * * const effect1 = Effect.fromOption(some) * const effect2 = Effect.fromOption(none) + * const effect3 = Effect.fromOption(none, () => new Error("missing")) * * Effect.runPromise(effect1).then(console.log) // 42 * Effect.runPromiseExit(effect2).then(console.log) @@ -1812,9 +1815,12 @@ export const fromResult: (result: Result.Result) => Effect = i * @category converting * @since 4.0.0 */ -export const fromOption: ( - option: Option -) => Effect = internal.fromOption +export const fromOption: | LazyArg, E = Cause.NoSuchElementError>( + arg: Arg, + ...rest: [Arg] extends [Option] ? [onNone?: LazyArg] : [] +) => [Arg] extends [Option] ? Effect + : [Arg] extends [LazyArg] ? (option: Option) => Effect + : never = internal.fromOption /** * Converts an `Option` of an `Effect` into an `Effect` of an `Option`. diff --git a/.context/effect/packages/effect/src/Latch.ts b/.context/effect/packages/effect/src/Latch.ts index e374e8100..418c78e44 100644 --- a/.context/effect/packages/effect/src/Latch.ts +++ b/.context/effect/packages/effect/src/Latch.ts @@ -113,6 +113,15 @@ export interface Latch { * Use to gate an effect behind the latch signal. */ whenOpen(self: Effect.Effect): Effect.Effect + + /** + * Checks whether the latch is currently open or closed. + * + * **When to use** + * + * Use to check the state of the latch without suspending or changing its state. + */ + isOpen(this: Latch): boolean } /** @@ -371,3 +380,15 @@ export const whenOpen: { const [self, effect] = args return self.whenOpen(effect) }) as any + +/** + * Checks whether the latch is currently open or closed. + * + * **When to use** + * + * Use to check the state of the latch without suspending or changing its state. + * + * @category getters + * @since 4.0.0 + */ +export const isOpen = (self: Latch): boolean => self.isOpen() diff --git a/.context/effect/packages/effect/src/Schedule.ts b/.context/effect/packages/effect/src/Schedule.ts index 8e62d3afa..75d75cc9b 100644 --- a/.context/effect/packages/effect/src/Schedule.ts +++ b/.context/effect/packages/effect/src/Schedule.ts @@ -743,31 +743,31 @@ export const andThenResult: { } = dual(2, ( self: Schedule, other: Schedule -): Schedule, Input & Input2, Error | Error2, Env | Env2> => +): Schedule, Input & Input2, Error | Error2, Env | Env2> => fromStep(effect.sync(() => { let currentSide = 0 let currentStep: | undefined | ((now: number, input: Input & Input2) => Pull.Pull< - [Result.Result, Duration.Duration], + [Result.Result, Duration.Duration], Error | Error2, - Result.Result, + Result.Result, Env | Env2 >) - const left = map(self, Result.succeed) - const right = map(other, Result.fail) + const left = map(self, Result.fail) + const right = map(other, Result.succeed) return function recur( now, input ): Pull.Pull< - [Result.Result, Duration.Duration], + [Result.Result, Duration.Duration], Error | Error2, - Result.Result, + Result.Result, Env | Env2 > { if (currentStep) return currentStep(now, input) return toStep< - Result.Result, + Result.Result, Input & Input2, Error | Error2, Env | Env2 diff --git a/.context/effect/packages/effect/src/Schema.ts b/.context/effect/packages/effect/src/Schema.ts index b51fe0e4b..d09bb3d15 100644 --- a/.context/effect/packages/effect/src/Schema.ts +++ b/.context/effect/packages/effect/src/Schema.ts @@ -122,23 +122,23 @@ export interface MakeOptions { * Whether to disable validation for the schema. */ readonly disableChecks?: boolean | undefined + + /** @internal */ + readonly "~payload"?: { + readonly token: unknown + readonly value: unknown + } } /** * The fully-parameterized base interface for all schemas. Exposes all 14 type - * parameters controlling type inference, mutability, optionality, services, - * and transformation behavior. + * parameters controlling type inference, mutability, optionality, services, and + * transformation behavior. * * **When to use** * - * Use when you are writing advanced generic schema utilities or performing schema - * introspection. - * - In user code, prefer {@link Schema}, {@link Codec}, {@link Decoder}, or - * {@link Encoder} instead. - * - * @see {@link Top} — the existential "any schema" type (erased type params) - * @see {@link Schema} — tracks only the decoded Type - * @see {@link Codec} — tracks Type + Encoded + * Use when you are writing advanced generic schema utilities or performing + * schema introspection. * * @category models * @since 4.0.0 @@ -152,7 +152,7 @@ export interface Bottom< out Rebuild extends Top, out TypeMakeIn = T, out Iso = T, - in out TypeParameters extends ReadonlyArray = readonly [], + in out TypeParameters extends ReadonlyArray = readonly [], out TypeMake = TypeMakeIn, out TypeMutability extends Mutability = "readonly", out TypeOptionality extends Optionality = "required", @@ -248,13 +248,67 @@ export interface Bottom< makeEffect(input: this["~type.make.in"], options?: MakeOptions): Effect.Effect } +/** + * Lazy `Bottom` variant for schema implementations that compute their public + * views on demand. + * + * **When to use** + * + * Use as an implementation base for schema interfaces that must expose + * `Bottom` behavior without forcing TypeScript to eagerly evaluate expensive + * `Type`, `Encoded`, or service views. + * + * **Details** + * + * The laziness is purely type-level; runtime behavior is unchanged. + * `BottomLazy` keeps the structural operations inherited from `Bottom`, but + * erases the expensive schema views to `unknown`. Concrete schema interfaces can + * then redeclare the precise views they expose. This keeps wide schemas such as + * `Struct` and `Union` cheaper when generic code reads a single view, while + * preserving their exact public types. + * + * @see {@link Bottom} for the fully parameterized schema interface when every + * view must be supplied directly. + * + * @category utility types + * @since 4.0.0 + */ +export interface BottomLazy< + out Ast extends SchemaAST.AST, + out Rebuild extends Top, + in out TypeParameters extends ReadonlyArray = readonly [], + out TypeMutability extends Mutability = "readonly", + out TypeOptionality extends Optionality = "required", + out TypeConstructorDefault extends ConstructorDefault = "no-default", + out EncodedMutability extends Mutability = "readonly", + out EncodedOptionality extends Optionality = "required" +> extends + Bottom< + unknown, + unknown, + unknown, + unknown, + Ast, + Rebuild, + unknown, + unknown, + TypeParameters, + unknown, + TypeMutability, + TypeOptionality, + TypeConstructorDefault, + EncodedMutability, + EncodedOptionality + > +{} + /** * Type-level representation returned by {@link declareConstructor}. * * @category constructors * @since 4.0.0 */ -export interface declareConstructor, Iso = T> extends +export interface declareConstructor, Iso = T> extends Bottom< T, E, @@ -301,7 +355,7 @@ export interface declareConstructor => * typeof u === "object" && u !== null && "value" in u * - * const Box = (item: A) => + * const Box = (item: A) => * Schema.declareConstructor, Box>()( * [item], * ([itemCodec]) => @@ -323,7 +377,7 @@ export interface declareConstructor() { - return >( + return >( typeParameters: TypeParameters, run: ( typeParameters: { @@ -566,8 +620,8 @@ export function annotateKey(annotations: Annotations.Key` — when you only care about the decoded type * - {@link Codec}`` — when you need the encoded type and service requirements - * - {@link Decoder}`` — for decode-only APIs - * - {@link Encoder}`` — for encode-only APIs + * - {@link ConstraintDecoder}`` — for decode-only APIs + * - {@link ConstraintEncoder}`` — for encode-only APIs * * @category models * @since 4.0.0 @@ -592,6 +646,127 @@ export interface Top extends > {} +/** + * Lightweight structural constraint for APIs that accept schema values but only + * read their data and type-level views. + * + * **When to use** + * + * Use when you need to constrain a generic value to be a schema, but the API + * only reads properties such as `ast`, `Type`, `Encoded`, service + * requirements, constructor input views, or modifier flags. + * + * **Details** + * + * `Constraint` keeps the schema type identifier and the property surface needed + * by schema constructors, while avoiding the full `Bottom` protocol. Use + * {@link Top} when an API calls schema methods such as `annotate`, `check`, + * `rebuild`, `make`, or `makeEffect`. + * + * @see {@link Top} for the complete schema protocol. + * + * @category models + * @since 4.0.0 + */ +export interface Constraint { + readonly [TypeId]: typeof TypeId + readonly "ast": SchemaAST.AST + + readonly "Type": unknown + readonly "Encoded": unknown + readonly "DecodingServices": unknown + readonly "EncodingServices": unknown + + readonly "~type.parameters": any + readonly "~type.make.in": unknown + readonly "~type.make": unknown + readonly "Iso": unknown + + readonly "~type.optionality": Optionality + readonly "~type.mutability": Mutability + readonly "~type.constructor.default": ConstructorDefault + readonly "~encoded.optionality": Optionality + readonly "~encoded.mutability": Mutability +} + +/** + * Lightweight structural constraint for APIs that need codec type views but do + * not need the full schema protocol. + * + * **When to use** + * + * Use when you need to preserve decoded type, encoded type, and service + * requirements for a schema value, but the API does not call schema methods + * such as `annotate`, `check`, `rebuild`, `make`, or `makeEffect`. + * + * @see {@link Constraint} for the generic lightweight schema constraint. + * @see {@link Codec} for the full schema protocol with codec type views. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintCodec extends Constraint { + readonly "Type": T + readonly "Encoded": E + readonly "DecodingServices": RD + readonly "EncodingServices": RE +} + +/** + * Lightweight structural constraint for APIs that need decoder type views but + * do not need the full schema protocol. + * + * **When to use** + * + * Use when you need to preserve a schema's decoded type and decoding services, + * but the API does not constrain the encoded type, encoding services, or call + * schema methods such as `annotate`, `check`, `rebuild`, `make`, or + * `makeEffect`. + * + * @see {@link ConstraintCodec} for APIs that need both decoded and encoded codec views. + * @see {@link Codec} for the full schema protocol with codec type views. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintDecoder extends ConstraintCodec {} + +/** + * Lightweight structural constraint for APIs that need encoder type views but + * do not need the full schema protocol. + * + * **When to use** + * + * Use when you need to preserve a schema's encoded type and encoding services, + * but the API does not constrain the decoded type, decoding services, or call + * schema methods such as `annotate`, `check`, `rebuild`, `make`, or + * `makeEffect`. + * + * @see {@link ConstraintCodec} for APIs that need both decoded and encoded codec views. + * @see {@link Codec} for the full schema protocol with codec type views. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintEncoder extends ConstraintCodec {} + +/** + * Lightweight structural constraint for APIs that need schema views and the + * rebuilt schema type, but do not call the full schema protocol. + * + * **When to use** + * + * Use when an API needs to read `Rebuild` in addition to the schema views + * exposed by {@link Constraint}, but does not call methods such as `annotate`, + * `check`, `rebuild`, `make`, or `makeEffect`. + * + * @category models + * @since 4.0.0 + */ +export interface ConstraintRebuildable extends Constraint { + readonly "Rebuild": Constraint +} + /** * Namespace of type-level helpers for {@link Schema}. * @@ -614,7 +789,7 @@ export declare namespace Schema { * @category utility types * @since 3.10.0 */ - export type Type = S extends Top ? S["Type"] : never + export type Type = S extends { readonly "Type": infer T } ? T : never } /** @@ -674,7 +849,8 @@ export declare namespace Codec { * @category utility types * @since 3.10.0 */ - export type Encoded = S extends Top ? S["Encoded"] : never + export type Encoded = S extends { readonly "Encoded": infer E } ? E : never + /** * Extracts the Effect services required during *decoding* from a schema. * @@ -691,7 +867,8 @@ export declare namespace Codec { * @category utility types * @since 4.0.0 */ - export type DecodingServices = S extends Top ? S["DecodingServices"] : never + export type DecodingServices = S extends { readonly "DecodingServices": infer R } ? R : never + /** * Extracts the Effect services required during *encoding* from a schema. * @@ -708,7 +885,7 @@ export declare namespace Codec { * @category utility types * @since 4.0.0 */ - export type EncodingServices = S extends Top ? S["EncodingServices"] : never + export type EncodingServices = S extends { readonly "EncodingServices": infer R } ? R : never } /** @@ -747,8 +924,8 @@ export interface Optic extends Schema { * Most concrete schemas produced by this module implement `Codec`. * * For APIs that only need one direction, prefer the narrower views: - * - {@link Decoder}`` — decode-only - * - {@link Encoder}`` — encode-only + * - {@link ConstraintDecoder}`` — decode-only + * - {@link ConstraintEncoder}`` — encode-only * - {@link Schema}`` — type-only (no encoded representation) * * **Example** (Accepting a codec that decodes to `number` from `string`) @@ -776,60 +953,6 @@ export interface Codec extends readonly "Rebuild": Codec } -/** - * A {@link Codec} view for APIs that only *decode* (parse/validate) values. - * - * **Details** - * - * Use `Decoder` to accept "any schema that can decode to `T`" without - * constraining or depending on the encoded representation (`Encoded` is - * `unknown`) or encoding services. - * - * **Example** (Accepting schemas that only need to decode) - * - * ```ts - * import { Schema } from "effect" - * - * declare function validate(decoder: Schema.Decoder): (input: unknown) => T - * - * validate(Schema.String) // ok - * validate(Schema.NumberFromString) // ok - * ``` - * - * @category models - * @since 4.0.0 - */ -export interface Decoder extends Codec { - readonly "Rebuild": Decoder -} - -/** - * A {@link Codec} view for APIs that only *encode* values. - * - * **Details** - * - * Use `Encoder` to accept "any schema that can encode to `E`" without - * constraining or depending on the decoded `Type` (`Type` is `unknown`) or - * decoding services. - * - * **Example** (Accepting schemas that only need to encode) - * - * ```ts - * import { Schema } from "effect" - * - * declare function serialize(encoder: Schema.Encoder): (value: unknown) => E - * - * serialize(Schema.String) // ok — encodes to string - * serialize(Schema.NumberFromString) // ok — encodes number to string - * ``` - * - * @category models - * @since 4.0.0 - */ -export interface Encoder extends Codec { - readonly "Rebuild": Encoder -} - /** * Returns a codec widened to the full {@link Codec} interface, prompting * TypeScript to infer all four type parameters (`T`, `E`, `RD`, `RE`). @@ -987,7 +1110,7 @@ function makeStandardResult(exit: Exit_.Exit>): St * @category Standard Schema * @since 4.0.0 */ -export function toStandardSchemaV1>( +export function toStandardSchemaV1>( self: S, options?: { readonly leafHook?: SchemaIssue.LeafHook | undefined @@ -1037,7 +1160,7 @@ export function toStandardSchemaV1>( } } -function toBaseStandardJSONSchemaV1(self: Top, target: StandardJSONSchemaV1.Target): JsonSchema.JsonSchema { +function toBaseStandardJSONSchemaV1(self: Constraint, target: StandardJSONSchemaV1.Target): JsonSchema.JsonSchema { const doc2020_12 = toJsonSchemaDocument(self) if (target === "draft-2020-12") { const schema = doc2020_12.schema @@ -1066,7 +1189,9 @@ function toBaseStandardJSONSchemaV1(self: Top, target: StandardJSONSchemaV1.Targ * @category Standard Schema * @since 4.0.0 */ -export function toStandardJSONSchemaV1(self: S): StandardJSONSchemaV1 & S { +export function toStandardJSONSchemaV1( + self: S +): StandardJSONSchemaV1 & S { const jsonSchema: StandardJSONSchemaV1.Props["jsonSchema"] = { input(options) { return toBaseStandardJSONSchemaV1(self, options.target) @@ -1173,7 +1298,8 @@ export const is = SchemaParser.is * @category guards * @since 4.0.0 */ -export const asserts: (schema: S, input: I) => asserts input is I & S["Type"] = SchemaParser.asserts +export const asserts: (schema: S, input: I) => asserts input is I & S["Type"] = + SchemaParser.asserts /** * Decodes an `unknown` input against a schema, returning an `Effect` that @@ -1196,7 +1322,7 @@ export const asserts: (schema: S, input: I) => asserts input i * @category decoding * @since 4.0.0 */ -export function decodeUnknownEffect(schema: S, options?: SchemaAST.ParseOptions) { +export function decodeUnknownEffect(schema: S, options?: SchemaAST.ParseOptions) { const parser = SchemaParser.decodeUnknownEffect(schema, options) return ( input: unknown, @@ -1227,7 +1353,7 @@ export function decodeUnknownEffect(schema: S, options?: SchemaAS * @category decoding * @since 4.0.0 */ -export const decodeEffect: ( +export const decodeEffect: ( schema: S, options?: SchemaAST.ParseOptions ) => ( @@ -1303,7 +1429,7 @@ function runSchemaErrorSync( * @category decoding * @since 4.0.0 */ -export function decodeUnknownExit>(schema: S, options?: SchemaAST.ParseOptions) { +export function decodeUnknownExit>(schema: S, options?: SchemaAST.ParseOptions) { const parser = SchemaParser.decodeUnknownExit(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): Exit_.Exit => { return InternalSchema.mapSchemaIssueExit(parser(input, options)) @@ -1340,7 +1466,7 @@ export function decodeUnknownExit>(schema: S, options * @category decoding * @since 4.0.0 */ -export const decodeExit: >( +export const decodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit_.Exit = decodeUnknownExit @@ -1371,7 +1497,7 @@ export const decodeExit: >( * @category decoding * @since 3.10.0 */ -export const decodeUnknownOption: >( +export const decodeUnknownOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeUnknownOption @@ -1401,10 +1527,10 @@ export const decodeUnknownOption: >( * @category decoding * @since 3.10.0 */ -export const decodeOption: >( +export const decodeOption: >( schema: S, options?: SchemaAST.ParseOptions -) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option_.Option = decodeUnknownOption +) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.decodeOption /** * Decodes an `unknown` input against a schema, returning a `Result` that @@ -1435,7 +1561,7 @@ export const decodeOption: >( * @category decoding * @since 4.0.0 */ -export function decodeUnknownResult>(schema: S, options?: SchemaAST.ParseOptions) { +export function decodeUnknownResult>(schema: S, options?: SchemaAST.ParseOptions) { const parser = SchemaParser.decodeUnknownResult(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): Result_.Result => { return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) @@ -1470,7 +1596,7 @@ export function decodeUnknownResult>(schema: S, optio * @category decoding * @since 4.0.0 */ -export const decodeResult: >( +export const decodeResult: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result_.Result = @@ -1503,7 +1629,10 @@ export const decodeResult: >( * @category decoding * @since 3.10.0 */ -export function decodeUnknownPromise>(schema: S, options?: SchemaAST.ParseOptions) { +export function decodeUnknownPromise>( + schema: S, + options?: SchemaAST.ParseOptions +) { const parser = decodeUnknownEffect(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): Promise => { return runSchemaErrorPromise(parser(input, options)) @@ -1537,7 +1666,7 @@ export function decodeUnknownPromise>(schema: S, opti * @category decoding * @since 3.10.0 */ -export const decodePromise: >( +export const decodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise = decodeUnknownPromise @@ -1586,7 +1715,7 @@ export const decodePromise: >( * @category decoding * @since 4.0.0 */ -export function decodeUnknownSync>(schema: S, options?: SchemaAST.ParseOptions) { +export function decodeUnknownSync>(schema: S, options?: SchemaAST.ParseOptions) { const parser = decodeUnknownEffect(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): S["Type"] => { return runSchemaErrorSync(parser(input, options)) @@ -1619,7 +1748,7 @@ export function decodeUnknownSync>(schema: S, options * @category decoding * @since 4.0.0 */ -export const decodeSync: >( +export const decodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync @@ -1656,7 +1785,7 @@ export const decodeSync: >( * @category encoding * @since 4.0.0 */ -export function encodeUnknownEffect(schema: S, options?: SchemaAST.ParseOptions) { +export function encodeUnknownEffect(schema: S, options?: SchemaAST.ParseOptions) { const parser = SchemaParser.encodeUnknownEffect(schema, options) return ( input: unknown, @@ -1687,7 +1816,7 @@ export function encodeUnknownEffect(schema: S, options?: SchemaAS * @category encoding * @since 4.0.0 */ -export const encodeEffect: ( +export const encodeEffect: ( schema: S, options?: SchemaAST.ParseOptions ) => ( @@ -1724,7 +1853,7 @@ export const encodeEffect: ( * @category encoding * @since 4.0.0 */ -export function encodeUnknownExit>(schema: S, options?: SchemaAST.ParseOptions) { +export function encodeUnknownExit>(schema: S, options?: SchemaAST.ParseOptions) { const parser = SchemaParser.encodeUnknownExit(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): Exit_.Exit => { return InternalSchema.mapSchemaIssueExit(parser(input, options)) @@ -1761,7 +1890,7 @@ export function encodeUnknownExit>(schema: S, options * @category encoding * @since 4.0.0 */ -export const encodeExit: >( +export const encodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit_.Exit = encodeUnknownExit @@ -1792,7 +1921,7 @@ export const encodeExit: >( * @category encoding * @since 3.10.0 */ -export const encodeUnknownOption: >( +export const encodeUnknownOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: unknown, options?: SchemaAST.ParseOptions) => Option_.Option = @@ -1823,10 +1952,10 @@ export const encodeUnknownOption: >( * @category encoding * @since 3.10.0 */ -export const encodeOption: >( +export const encodeOption: >( schema: S, options?: SchemaAST.ParseOptions -) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option_.Option = encodeUnknownOption +) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option_.Option = SchemaParser.encodeOption /** * Encodes an `unknown` input against a schema, returning a `Result` that @@ -1856,7 +1985,7 @@ export const encodeOption: >( * @category encoding * @since 4.0.0 */ -export function encodeUnknownResult>(schema: S, options?: SchemaAST.ParseOptions) { +export function encodeUnknownResult>(schema: S, options?: SchemaAST.ParseOptions) { const parser = SchemaParser.encodeUnknownResult(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): Result_.Result => { return Result_.mapError(parser(input, options), (issue) => new SchemaError(issue)) @@ -1891,7 +2020,7 @@ export function encodeUnknownResult>(schema: S, optio * @category encoding * @since 4.0.0 */ -export const encodeResult: >( +export const encodeResult: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result_.Result = @@ -1923,7 +2052,10 @@ export const encodeResult: >( * @category encoding * @since 3.10.0 */ -export function encodeUnknownPromise>(schema: S, options?: SchemaAST.ParseOptions) { +export function encodeUnknownPromise>( + schema: S, + options?: SchemaAST.ParseOptions +) { const parser = encodeUnknownEffect(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): Promise => { return runSchemaErrorPromise(parser(input, options)) @@ -1957,7 +2089,7 @@ export function encodeUnknownPromise>(schema: S, opti * @category encoding * @since 3.10.0 */ -export const encodePromise: >( +export const encodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise = encodeUnknownPromise @@ -1988,7 +2120,7 @@ export const encodePromise: >( * @category encoding * @since 4.0.0 */ -export function encodeUnknownSync>(schema: S, options?: SchemaAST.ParseOptions) { +export function encodeUnknownSync>(schema: S, options?: SchemaAST.ParseOptions) { const parser = encodeUnknownEffect(schema, options) return (input: unknown, options?: SchemaAST.ParseOptions): S["Encoded"] => { return runSchemaErrorSync(parser(input, options) as Effect.Effect) @@ -2019,7 +2151,7 @@ export function encodeUnknownSync>(schema: S, options * @category encoding * @since 4.0.0 */ -export const encodeSync: >( +export const encodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync @@ -2041,7 +2173,7 @@ export const encodeSync: >( * @category constructors * @since 3.10.0 */ -export const make: (ast: S["ast"], options?: object) => S = InternalSchema.make +export const make: (ast: S["ast"], options?: object) => S = InternalSchema.make /** * Transforms a schema into a class that can be extended with `extends`. The @@ -2086,18 +2218,11 @@ export function isSchema(u: unknown): u is Top { * @category models * @since 4.0.0 */ -export interface optionalKey extends - Bottom< - S["Type"], - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface optionalKey extends + BottomLazy< S["ast"], optionalKey, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], "optional", S["~type.constructor.default"], @@ -2105,12 +2230,19 @@ export interface optionalKey extends "optional" > { + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] readonly schema: S } interface optionalKeyLambda extends Lambda { - (self: S): optionalKey - readonly "~lambda.out": this["~lambda.in"] extends Top ? optionalKey : never + (self: S): optionalKey + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optionalKey : never } /** @@ -2140,8 +2272,8 @@ export const optionalKey = Struct_.lambda((schema) => ) interface requiredKeyLambda extends Lambda { - (self: optionalKey): S - readonly "~lambda.out": this["~lambda.in"] extends optionalKey ? this["~lambda.in"]["schema"] + (self: optionalKey): S + readonly "~lambda.out": this["~lambda.in"] extends optionalKey ? this["~lambda.in"]["schema"] : "Error: schema not eligible for requiredKey" } @@ -2164,13 +2296,13 @@ export const requiredKey = Struct_.lambda((self) => self.sche * @category models * @since 3.10.0 */ -export interface optional extends optionalKey> { +export interface optional extends optionalKey> { readonly "Rebuild": optional } interface optionalLambda extends Lambda { - (self: S): optional - readonly "~lambda.out": this["~lambda.in"] extends Top ? optional : never + (self: S): optional + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? optional : never } /** @@ -2205,8 +2337,8 @@ interface optionalLambda extends Lambda { export const optional = Struct_.lambda((self) => optionalKey(UndefinedOr(self))) interface requiredLambda extends Lambda { - (self: optional): S - readonly "~lambda.out": this["~lambda.in"] extends optional ? this["~lambda.in"]["schema"]["members"][0] + (self: optional): S + readonly "~lambda.out": this["~lambda.in"] extends optional ? this["~lambda.in"]["schema"]["members"][0] : "Error: schema not eligible for required" } @@ -2233,18 +2365,11 @@ export const required = Struct_.lambda((self) => self.schema.mem * @category models * @since 4.0.0 */ -export interface mutableKey extends - Bottom< - S["Type"], - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface mutableKey extends + BottomLazy< S["ast"], mutableKey, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], "mutable", S["~type.optionality"], S["~type.constructor.default"], @@ -2252,12 +2377,19 @@ export interface mutableKey extends S["~encoded.optionality"] > { + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] readonly schema: S } interface mutableKeyLambda extends Lambda { - (self: S): mutableKey - readonly "~lambda.out": this["~lambda.in"] extends Top ? mutableKey : never + (self: S): mutableKey + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? mutableKey : never } /** @@ -2272,8 +2404,8 @@ export const mutableKey = Struct_.lambda((schema) => ) interface readonlyKeyLambda extends Lambda { - (self: mutableKey): S - readonly "~lambda.out": this["~lambda.in"] extends mutableKey ? this["~lambda.in"]["schema"] + (self: mutableKey): S + readonly "~lambda.out": this["~lambda.in"] extends mutableKey ? this["~lambda.in"]["schema"] : "Error: schema not eligible for readonlyKey" } @@ -2296,29 +2428,31 @@ export const readonlyKey = Struct_.lambda((self) => self.sche * @category transforming * @since 4.0.0 */ -export interface toType extends - Bottom< - S["Type"], - S["Type"], - never, - never, +export interface toType extends + BottomLazy< S["ast"], toType, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], S["~encoded.mutability"], S["~encoded.optionality"] > -{} +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Type"] + readonly "DecodingServices": never + readonly "EncodingServices": never + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} interface toTypeLambda extends Lambda { - (self: S): toType - readonly "~lambda.out": this["~lambda.in"] extends Top ? toType : never + (self: S): toType + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toType : never } /** @@ -2336,29 +2470,31 @@ export const toType = Struct_.lambda((schema) => make(SchemaAST.to * @category transforming * @since 4.0.0 */ -export interface toEncoded extends - Bottom< - S["Encoded"], - S["Encoded"], - never, - never, +export interface toEncoded extends + BottomLazy< SchemaAST.AST, toEncoded, - S["Encoded"], - S["Encoded"], - ReadonlyArray, - S["Encoded"], + ReadonlyArray, S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], S["~encoded.mutability"], S["~encoded.optionality"] > -{} +{ + readonly "Type": S["Encoded"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": never + readonly "EncodingServices": never + readonly "~type.make.in": S["Encoded"] + readonly "~type.make": S["Encoded"] + readonly "Iso": S["Encoded"] + readonly schema: S +} interface toEncodedLambda extends Lambda { - (self: S): toEncoded - readonly "~lambda.out": this["~lambda.in"] extends Top ? toEncoded : never + (self: S): toEncoded + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? toEncoded : never } /** @@ -2379,17 +2515,10 @@ const FlipTypeId = "~effect/Schema/flip" * @since 4.0.0 */ export interface flip extends - Bottom< - S["Encoded"], - S["Type"], - S["EncodingServices"], - S["DecodingServices"], + BottomLazy< SchemaAST.AST, flip, - S["Encoded"], - S["Encoded"], - ReadonlyArray, - S["Encoded"], + ReadonlyArray, S["~encoded.mutability"], S["~encoded.optionality"], ConstructorDefault, @@ -2397,6 +2526,13 @@ export interface flip extends S["~type.optionality"] > { + readonly "Type": S["Encoded"] + readonly "Encoded": S["Type"] + readonly "DecodingServices": S["EncodingServices"] + readonly "EncodingServices": S["DecodingServices"] + readonly "~type.make.in": S["Encoded"] + readonly "~type.make": S["Encoded"] + readonly "Iso": S["Encoded"] readonly [FlipTypeId]: typeof FlipTypeId readonly schema: S } @@ -2498,7 +2634,7 @@ export declare namespace TemplateLiteral { * @category utility types * @since 4.0.0 */ - export interface SchemaPart extends Top { + export interface SchemaPart extends Constraint { readonly Encoded: string | number | bigint } @@ -2531,7 +2667,7 @@ export declare namespace TemplateLiteral { Template extends string, Next > = Next extends LiteralPart ? `${Template}${Next}` - : Next extends Codec ? `${Template}${E}` + : Next extends { readonly Encoded: infer E extends LiteralPart } ? `${Template}${E}` : never /** @@ -2565,7 +2701,9 @@ export interface TemplateLiteral extends } function templateLiteralFromParts(parts: Parts) { - return new SchemaAST.TemplateLiteral(parts.map((part) => isSchema(part) ? part.ast : new SchemaAST.Literal(part))) + return new SchemaAST.TemplateLiteral( + parts.map((part) => isSchema(part) ? part.ast : new SchemaAST.Literal(part as TemplateLiteral.LiteralPart)) + ) } /** @@ -2633,15 +2771,18 @@ export declare namespace TemplateLiteralParser { * @since 3.10.0 */ export interface TemplateLiteralParser extends - Bottom< - TemplateLiteralParser.Type, - TemplateLiteral.Encoded, - never, - never, + BottomLazy< SchemaAST.Arrays, TemplateLiteralParser > { + readonly "Type": TemplateLiteralParser.Type + readonly "Encoded": TemplateLiteral.Encoded + readonly "DecodingServices": never + readonly "EncodingServices": never + readonly "~type.make.in": TemplateLiteralParser.Type + readonly "~type.make": TemplateLiteralParser.Type + readonly "Iso": TemplateLiteralParser.Type readonly parts: Parts } @@ -2921,8 +3062,21 @@ export const BigInt: BigInt = make(SchemaAST.bigInt) export interface Void extends Bottom {} /** - * Schema for the `void` type. Accepts `undefined` as the encoded value. + * Schema for a TypeScript `void` return value. + * + * **When to use** + * + * Use when you need to model the return value of a function, RPC, or endpoint + * whose result is intentionally ignored. + * + * **Details** * + * Runtime parsing accepts any present value and discards it, producing + * `undefined`. The public decoded and encoded TypeScript representation remains + * `void`, so typed construction, decoding, and encoding APIs are still modeled + * as `void`. + * + * @see {@link Undefined} for a schema that matches only the exact `undefined` value. * @category schemas * @since 3.10.0 */ @@ -2999,7 +3153,7 @@ export declare namespace Struct { * @category utility types * @since 3.10.0 */ - export type Fields = { readonly [x: PropertyKey]: Top } + export type Fields = { readonly [x: PropertyKey]: Constraint } type TypeOptionalKeys = { [K in keyof Fields]: Fields[K] extends { readonly "~type.optionality": "optional" } ? K @@ -3011,15 +3165,46 @@ export declare namespace Struct { : never }[keyof Fields] - type Type_< + type SetOptional = Omit & Partial> + + type Mutable = { -readonly [K in keyof A]: A[K] } + + type SetMutable = Omit & Mutable> + + type Side = "Type" | "Iso" | "Encoded" + + type EncodedOptionalKeys = { + [K in keyof Fields]: Fields[K] extends { readonly "~encoded.optionality": "optional" } ? K + : never + }[keyof Fields] + + type EncodedMutableKeys = { + [K in keyof Fields]: Fields[K] extends { readonly "~encoded.mutability": "mutable" } ? K + : never + }[keyof Fields] + + type SideOptionalKeys = S extends "Encoded" ? EncodedOptionalKeys + : TypeOptionalKeys + + type SideMutableKeys = S extends "Encoded" ? EncodedMutableKeys + : TypeMutableKeys + + type ReadonlySide = { readonly [K in keyof F]: F[K][S] } + + type View< F extends Fields, - O extends keyof F = TypeOptionalKeys, - M extends keyof F = TypeMutableKeys - > = - & { readonly [K in keyof F as K extends M | O ? never : K]: F[K]["Type"] } - & { readonly [K in keyof F as K extends O ? K extends M ? never : K : never]?: F[K]["Type"] } - & { -readonly [K in keyof F as K extends M ? K extends O ? never : K : never]: F[K]["Type"] } - & { -readonly [K in keyof F as K extends M & O ? K : never]?: F[K]["Type"] } + S extends Side, + O extends keyof F = SideOptionalKeys, + M extends keyof F = SideMutableKeys + > = [O | M] extends [never] ? ReadonlySide + : [M] extends [never] ? Simplify, O>> + : [O] extends [never] ? Simplify, M>> + : Simplify< + SetMutable< + SetOptional, O>, + Extract, O>, M> + > + > /** * Computes the decoded object type for a struct field map. @@ -3032,17 +3217,7 @@ export declare namespace Struct { * @category utility types * @since 3.10.0 */ - export type Type = Simplify> - - type Iso_< - F extends Fields, - O extends keyof F = TypeOptionalKeys, - M extends keyof F = TypeMutableKeys - > = - & { readonly [K in keyof F as K extends M | O ? never : K]: F[K]["Iso"] } - & { readonly [K in keyof F as K extends O ? K extends M ? never : K : never]?: F[K]["Iso"] } - & { -readonly [K in keyof F as K extends M ? K extends O ? never : K : never]: F[K]["Iso"] } - & { -readonly [K in keyof F as K extends M & O ? K : never]?: F[K]["Iso"] } + export type Type = View /** * Computes the iso object type for a struct field map from each field schema's @@ -3056,27 +3231,7 @@ export declare namespace Struct { * @category utility types * @since 4.0.0 */ - export type Iso = Simplify> - - type EncodedOptionalKeys = { - [K in keyof Fields]: Fields[K] extends { readonly "~encoded.optionality": "optional" } ? K - : never - }[keyof Fields] - - type EncodedMutableKeys = { - [K in keyof Fields]: Fields[K] extends { readonly "~encoded.mutability": "mutable" } ? K - : never - }[keyof Fields] - - type Encoded_< - F extends Fields, - O extends keyof F = EncodedOptionalKeys, - M extends keyof F = EncodedMutableKeys - > = - & { readonly [K in keyof F as K extends M | O ? never : K]: F[K]["Encoded"] } - & { readonly [K in keyof F as K extends O ? K extends M ? never : K : never]?: F[K]["Encoded"] } - & { -readonly [K in keyof F as K extends M ? K extends O ? never : K : never]: F[K]["Encoded"] } - & { -readonly [K in keyof F as K extends M & O ? K : never]?: F[K]["Encoded"] } + export type Iso = View /** * Computes the encoded object type for a struct field map. @@ -3090,7 +3245,7 @@ export declare namespace Struct { * @category utility types * @since 3.10.0 */ - export type Encoded = Simplify> + export type Encoded = View /** * Union of all decoding service requirements needed by the schemas in a struct @@ -3115,12 +3270,12 @@ export declare namespace Struct { : never }[keyof Fields] - type MakeIn_< + type ReadonlyMakeIn = { readonly [K in keyof F]: F[K]["~type.make"] } + + type MakeInView< F extends Fields, - O = TypeOptionalKeys | TypeConstructorDefaultedKeys - > = - & { readonly [K in keyof F as K extends O ? never : K]: F[K]["~type.make"] } - & { readonly [K in keyof F as K extends O ? K : never]?: F[K]["~type.make"] } + O extends keyof F = TypeOptionalKeys | TypeConstructorDefaultedKeys + > = [O] extends [never] ? ReadonlyMakeIn : Simplify, O>> /** * Computes the input object type accepted when constructing a struct value. @@ -3133,7 +3288,7 @@ export declare namespace Struct { * @category utility types * @since 4.0.0 */ - export type MakeIn = Simplify> + export type MakeIn = MakeInView } /** @@ -3142,18 +3297,14 @@ export declare namespace Struct { * @category models * @since 3.10.0 */ -export interface Struct extends - Bottom< - Struct.Type, - Struct.Encoded, - Struct.DecodingServices, - Struct.EncodingServices, - SchemaAST.Objects, - Struct, - Struct.MakeIn, - Struct.Iso - > -{ +export interface Struct extends BottomLazy> { + readonly "Type": Struct.Type + readonly "Encoded": Struct.Encoded + readonly "DecodingServices": Struct.DecodingServices + readonly "EncodingServices": Struct.EncodingServices + readonly "~type.make.in": Struct.MakeIn + readonly "~type.make": Struct.MakeIn + readonly "Iso": Struct.Iso /** * The field definitions of this struct. Spread them into a new struct to * reuse fields across schemas. @@ -3300,7 +3451,7 @@ export function fieldsAssign(fields: NewF * @since 4.0.0 */ export interface encodeKeys< - S extends Top & { readonly fields: Struct.Fields }, + S extends Constraint & { readonly fields: Struct.Fields }, M extends { readonly [K in keyof S["fields"]]?: PropertyKey } > extends decodeTo< @@ -3347,7 +3498,7 @@ const canonicalPropertyKey = (key: PropertyKey): string | symbol => * @since 4.0.0 */ export function encodeKeys< - S extends Top & { readonly fields: Struct.Fields }, + S extends Constraint & { readonly fields: Struct.Fields }, const M extends { readonly [K in keyof S["fields"]]?: PropertyKey } >(mapping: M) { return function(self: S): encodeKeys { @@ -3489,7 +3640,7 @@ export declare namespace Record { * @category utility types * @since 3.10.0 */ - export type Type = Value extends + export type Type = Value extends { readonly "~type.optionality": "optional" } ? Value extends { readonly "~type.mutability": "mutable" } ? { [P in Key["Type"]]?: Value["Type"] } : { readonly [P in Key["Type"]]?: Value["Type"] } @@ -3503,7 +3654,7 @@ export declare namespace Record { * @category utility types * @since 4.0.0 */ - export type Iso = Value extends + export type Iso = Value extends { readonly "~type.optionality": "optional" } ? Value extends { readonly "~type.mutability": "mutable" } ? { [P in Key["Iso"]]?: Value["Iso"] } : { readonly [P in Key["Iso"]]?: Value["Iso"] } @@ -3522,7 +3673,7 @@ export declare namespace Record { * @category utility types * @since 3.10.0 */ - export type Encoded = Value extends + export type Encoded = Value extends { readonly "~encoded.optionality": "optional" } ? Value extends { readonly "~encoded.mutability": "mutable" } ? { [P in Key["Encoded"]]?: Value["Encoded"] } : { readonly [P in Key["Encoded"]]?: Value["Encoded"] } @@ -3536,7 +3687,7 @@ export declare namespace Record { * @category utility types * @since 4.0.0 */ - export type DecodingServices = + export type DecodingServices = | Key["DecodingServices"] | Value["DecodingServices"] @@ -3547,7 +3698,7 @@ export declare namespace Record { * @category utility types * @since 4.0.0 */ - export type EncodingServices = + export type EncodingServices = | Key["EncodingServices"] | Value["EncodingServices"] @@ -3563,7 +3714,7 @@ export declare namespace Record { * @category utility types * @since 4.0.0 */ - export type MakeIn = Value extends + export type MakeIn = Value extends { readonly "~encoded.optionality": "optional" } ? Value extends { readonly "~encoded.mutability": "mutable" } ? { [P in Key["~type.make"]]?: Value["~type.make"] } : { readonly [P in Key["~type.make"]]?: Value["~type.make"] } @@ -3577,18 +3728,19 @@ export declare namespace Record { * @category models * @since 4.0.0 */ -export interface $Record extends - Bottom< - Record.Type, - Record.Encoded, - Record.DecodingServices, - Record.EncodingServices, +export interface $Record extends + BottomLazy< SchemaAST.Objects, - $Record, - Simplify>, - Record.Iso + $Record > { + readonly "Type": Record.Type + readonly "Encoded": Record.Encoded + readonly "DecodingServices": Record.DecodingServices + readonly "EncodingServices": Record.EncodingServices + readonly "~type.make.in": Simplify> + readonly "~type.make": Simplify> + readonly "Iso": Record.Iso readonly key: Key readonly value: Value } @@ -3625,7 +3777,7 @@ export interface $Record extends * @category constructors * @since 3.10.0 */ -export function Record( +export function Record( key: Key, value: Value, options?: { @@ -3659,7 +3811,7 @@ export declare namespace StructWithRest { * @category utility types * @since 4.0.0 */ - export type Objects = Top & { readonly ast: SchemaAST.Objects } + export type Objects = Constraint & { readonly ast: SchemaAST.Objects } /** * Readonly list of record schemas that provide the additional index signatures @@ -3668,7 +3820,7 @@ export declare namespace StructWithRest { * @category utility types * @since 3.10.0 */ - export type Records = ReadonlyArray<$Record> + export type Records = ReadonlyArray<$Record> type MergeTuple> = T extends readonly [infer Head, ...infer Tail] ? Head & MergeTuple @@ -3765,24 +3917,52 @@ export declare namespace StructWithRest { [I in keyof Records]: Records[I][Side] extends object ? IncompatibleKeys : never }[number] + type IncompatibleRecords = + | IncompatibleSideKeys + | IncompatibleSideKeys + | IncompatibleSideKeys + | IncompatibleSideKeys + /** - * Validates that the records are compatible with the struct. + * Checks whether fixed fields are compatible with the rest record schemas. + * + * **Details** + * + * Returns `true` when all fixed fields can also satisfy the matching rest + * index signatures. Returns a diagnostic object when TypeScript would make + * the resulting intersection too narrow for one or more fixed keys. + * + * **Example** (Checking record compatibility) + * + * ```ts + * import { Schema } from "effect" + * + * const user = Schema.Struct({ id: Schema.String }) + * const stringExtras = [Schema.Record(Schema.String, Schema.String)] as const + * + * type UserCheck = Schema.StructWithRest.ValidateRecords + * + * const userCheck: UserCheck = true + * void userCheck + * + * const counter = Schema.Struct({ count: Schema.NumberFromString }) + * + * type CounterCheck = Schema.StructWithRest.ValidateRecords + * // ^? { "incompatible index signatures": "count" } + * + * const counterCheck = null as unknown as CounterCheck + * void counterCheck + * ``` * * @category utility types * @since 4.0.0 */ - export type ValidateRecords = [ - | IncompatibleSideKeys - | IncompatibleSideKeys - | IncompatibleSideKeys - | IncompatibleSideKeys - ] extends [never] ? unknown + export type ValidateRecords< + S extends Objects, + Records extends StructWithRest.Records + > = [IncompatibleRecords] extends [never] ? true : { - "incompatible index signatures": - | IncompatibleSideKeys - | IncompatibleSideKeys - | IncompatibleSideKeys - | IncompatibleSideKeys + "incompatible index signatures": IncompatibleRecords } } @@ -3796,17 +3976,18 @@ export interface StructWithRest< S extends StructWithRest.Objects, Records extends StructWithRest.Records > extends - Bottom< - Simplify>, - Simplify>, - StructWithRest.DecodingServices, - StructWithRest.EncodingServices, + BottomLazy< SchemaAST.Objects, - StructWithRest, - Simplify>, - Simplify> + StructWithRest > { + readonly "Type": Simplify> + readonly "Encoded": Simplify> + readonly "DecodingServices": StructWithRest.DecodingServices + readonly "EncodingServices": StructWithRest.EncodingServices + readonly "~type.make.in": Simplify> + readonly "~type.make": Simplify> + readonly "Iso": Simplify> readonly schema: S readonly records: Records } @@ -3815,6 +3996,13 @@ export interface StructWithRest< * Extends a struct schema with one or more record (index-signature) schemas, * producing a schema whose decoded type intersects the struct and all records. * + * **Gotchas** + * + * TypeScript index signatures also apply to fixed keys. `StructWithRest` does + * not reject incompatible fixed fields at the call site; use + * `StructWithRest.ValidateRecords` when you want an explicit type-level + * compatibility check. + * * **Example** (Defining structs with string-indexed extra keys) * * ```ts @@ -3837,7 +4025,7 @@ export function StructWithRest< const Records extends StructWithRest.Records >( schema: S, - records: Records & StructWithRest.ValidateRecords + records: Records ): StructWithRest { return make(SchemaAST.structWithRest(schema.ast, records.map(SchemaAST.getAST)), { schema, records }) } @@ -3862,7 +4050,7 @@ export declare namespace Tuple { * @category utility types * @since 3.10.0 */ - export type Elements = ReadonlyArray + export type Elements = ReadonlyArray type Type_< Elements, @@ -3980,17 +4168,18 @@ export declare namespace Tuple { * @since 3.10.0 */ export interface Tuple extends - Bottom< - Tuple.Type, - Tuple.Encoded, - Tuple.DecodingServices, - Tuple.EncodingServices, + BottomLazy< SchemaAST.Arrays, - Tuple, - Tuple.MakeIn, - Tuple.Iso + Tuple > { + readonly "Type": Tuple.Type + readonly "Encoded": Tuple.Encoded + readonly "DecodingServices": Tuple.DecodingServices + readonly "EncodingServices": Tuple.EncodingServices + readonly "~type.make.in": Tuple.MakeIn + readonly "~type.make": Tuple.MakeIn + readonly "Iso": Tuple.Iso readonly elements: Elements /** * Returns a new tuple with the elements modified by the provided function. @@ -4050,7 +4239,7 @@ function makeTuple(ast: SchemaAST.Arrays, eleme * @category constructors * @since 3.10.0 */ -export function Tuple>(elements: Elements): Tuple { +export function Tuple>(elements: Elements): Tuple { return makeTuple(SchemaAST.tuple(elements), elements) } @@ -4074,7 +4263,7 @@ export declare namespace TupleWithRest { * @category utility types * @since 3.10.0 */ - export type TupleType = Top & { + export type TupleType = Constraint & { readonly Type: ReadonlyArray readonly Encoded: ReadonlyArray readonly ast: SchemaAST.Arrays @@ -4093,7 +4282,7 @@ export declare namespace TupleWithRest { * @category utility types * @since 3.10.0 */ - export type Rest = readonly [Top, ...Array] + export type Rest = readonly [Constraint, ...Array] /** * Computes the decoded tuple type for a `TupleWithRest`. @@ -4108,7 +4297,7 @@ export declare namespace TupleWithRest { * @since 3.10.0 */ export type Type, Rest extends TupleWithRest.Rest> = Rest extends - readonly [infer Head extends Top, ...infer Tail extends ReadonlyArray] ? Readonly<[ + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? Readonly<[ ...T, ...Array, ...{ readonly [K in keyof Tail]: Tail[K]["Type"] } @@ -4128,7 +4317,7 @@ export declare namespace TupleWithRest { * @since 4.0.0 */ export type Iso, Rest extends TupleWithRest.Rest> = Rest extends - readonly [infer Head extends Top, ...infer Tail extends ReadonlyArray] ? Readonly<[ + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? Readonly<[ ...T, ...Array, ...{ readonly [K in keyof Tail]: Tail[K]["Iso"] } @@ -4148,7 +4337,7 @@ export declare namespace TupleWithRest { * @since 3.10.0 */ export type Encoded, Rest extends TupleWithRest.Rest> = Rest extends - readonly [infer Head extends Top, ...infer Tail extends ReadonlyArray] ? readonly [ + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? readonly [ ...E, ...Array, ...{ readonly [K in keyof Tail]: Tail[K]["Encoded"] } @@ -4168,7 +4357,7 @@ export declare namespace TupleWithRest { * @since 4.0.0 */ export type MakeIn, Rest extends TupleWithRest.Rest> = Rest extends - readonly [infer Head extends Top, ...infer Tail extends ReadonlyArray] ? readonly [ + readonly [infer Head extends Constraint, ...infer Tail extends ReadonlyArray] ? readonly [ ...M, ...Array, ...{ readonly [K in keyof Tail]: Tail[K]["~type.make"] } @@ -4186,17 +4375,18 @@ export interface TupleWithRest< S extends TupleWithRest.TupleType, Rest extends TupleWithRest.Rest > extends - Bottom< - TupleWithRest.Type, - TupleWithRest.Encoded, - S["DecodingServices"] | Rest[number]["DecodingServices"], - S["EncodingServices"] | Rest[number]["EncodingServices"], + BottomLazy< SchemaAST.Arrays, - TupleWithRest, - TupleWithRest.MakeIn, - TupleWithRest.Iso + TupleWithRest > { + readonly "Type": TupleWithRest.Type + readonly "Encoded": TupleWithRest.Encoded + readonly "DecodingServices": S["DecodingServices"] | Rest[number]["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] | Rest[number]["EncodingServices"] + readonly "~type.make.in": TupleWithRest.MakeIn + readonly "~type.make": TupleWithRest.MakeIn + readonly "Iso": TupleWithRest.Iso readonly schema: S readonly rest: Rest } @@ -4244,24 +4434,25 @@ export function TupleWithRest, const Rest extend * @category models * @since 4.0.0 */ -export interface $Array extends - Bottom< - ReadonlyArray, - ReadonlyArray, - S["DecodingServices"], - S["EncodingServices"], +export interface $Array extends + BottomLazy< SchemaAST.Arrays, - $Array, - ReadonlyArray, - ReadonlyArray + $Array > { + readonly "Type": ReadonlyArray + readonly "Encoded": ReadonlyArray + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": ReadonlyArray + readonly "~type.make": ReadonlyArray + readonly "Iso": ReadonlyArray readonly value: S } interface ArrayLambda extends Lambda { - (self: S): $Array - readonly "~lambda.out": this["~lambda.in"] extends Top ? $Array : never + (self: S): $Array + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? $Array : never } /** @@ -4300,24 +4491,25 @@ export { * @category models * @since 3.10.0 */ -export interface NonEmptyArray extends - Bottom< - readonly [S["Type"], ...Array], - readonly [S["Encoded"], ...Array], - S["DecodingServices"], - S["EncodingServices"], +export interface NonEmptyArray extends + BottomLazy< SchemaAST.Arrays, - NonEmptyArray, - readonly [S["~type.make"], ...Array], - readonly [S["Iso"], ...Array] + NonEmptyArray > { + readonly "Type": readonly [S["Type"], ...Array] + readonly "Encoded": readonly [S["Encoded"], ...Array] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": readonly [S["~type.make"], ...Array] + readonly "~type.make": readonly [S["~type.make"], ...Array] + readonly "Iso": readonly [S["Iso"], ...Array] readonly value: S } interface NonEmptyArrayLambda extends Lambda { - (self: S): NonEmptyArray - readonly "~lambda.out": this["~lambda.in"] extends Top ? NonEmptyArray : never + (self: S): NonEmptyArray + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? NonEmptyArray : never } /** @@ -4348,7 +4540,7 @@ export const NonEmptyArray = Struct_.lambda((schema) => * @category constructors * @since 3.10.0 */ -export interface ArrayEnsure extends decodeTo<$Array>, Union]>> { +export interface ArrayEnsure extends decodeTo<$Array>, Union]>> { readonly "Rebuild": ArrayEnsure } @@ -4378,7 +4570,7 @@ export interface ArrayEnsure extends decodeTo<$Array>, * @category constructors * @since 3.10.0 */ -export function ArrayEnsure(schema: S): ArrayEnsure { +export function ArrayEnsure(schema: S): ArrayEnsure { return Union([schema, ArraySchema(schema)]).pipe(decodeTo( ArraySchema(toType(schema)), SchemaTransformation.transform({ @@ -4394,7 +4586,7 @@ export function ArrayEnsure(schema: S): ArrayEnsure { * @category models * @since 4.0.0 */ -export interface UniqueArray extends $Array { +export interface UniqueArray extends $Array { readonly "Rebuild": UniqueArray } @@ -4409,7 +4601,7 @@ export interface UniqueArray extends $Array { * @category constructors * @since 4.0.0 */ -export function UniqueArray(item: S): UniqueArray { +export function UniqueArray(item: S): UniqueArray { return ArraySchema(item).check(isUnique()) } @@ -4419,19 +4611,11 @@ export function UniqueArray(item: S): UniqueArray { * @category transforming * @since 3.10.0 */ -export interface mutable extends - Bottom< - Mutable, - Mutable, - S["DecodingServices"], - S["EncodingServices"], +export interface mutable extends + BottomLazy< S["ast"], mutable, - // "~type.make" and "~type.make.in" as they are because they are contravariant - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -4439,12 +4623,20 @@ export interface mutable e S["~encoded.optionality"] > { + readonly "Type": Mutable + readonly "Encoded": Mutable + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + // "~type.make" and "~type.make.in" as they are because they are contravariant + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] readonly schema: S } interface mutableLambda extends Lambda { - (self: S): mutable - readonly "~lambda.out": this["~lambda.in"] extends Top & { readonly "ast": SchemaAST.Arrays } ? + (self: S): mutable + readonly "~lambda.out": this["~lambda.in"] extends Constraint & { readonly "ast": SchemaAST.Arrays } ? mutable : "Error: schema not eligible for mutable" } @@ -4476,18 +4668,19 @@ export const mutable = Struct_.lambda((schema) => { * @category models * @since 3.10.0 */ -export interface Union> extends - Bottom< - { [K in keyof Members]: Members[K]["Type"] }[number], - { [K in keyof Members]: Members[K]["Encoded"] }[number], - { [K in keyof Members]: Members[K]["DecodingServices"] }[number], - { [K in keyof Members]: Members[K]["EncodingServices"] }[number], +export interface Union> extends + BottomLazy< SchemaAST.Union<{ [K in keyof Members]: Members[K]["ast"] }[number]>, - Union, - { [K in keyof Members]: Members[K]["~type.make"] }[number], - { [K in keyof Members]: Members[K]["Iso"] }[number] + Union > { + readonly "Type": { [K in keyof Members]: Members[K]["Type"] }[number] + readonly "Encoded": { [K in keyof Members]: Members[K]["Encoded"] }[number] + readonly "DecodingServices": { [K in keyof Members]: Members[K]["DecodingServices"] }[number] + readonly "EncodingServices": { [K in keyof Members]: Members[K]["EncodingServices"] }[number] + readonly "~type.make.in": { [K in keyof Members]: Members[K]["~type.make"] }[number] + readonly "~type.make": { [K in keyof Members]: Members[K]["~type.make"] }[number] + readonly "Iso": { [K in keyof Members]: Members[K]["Iso"] }[number] readonly members: Members /** * Returns a new union with the members modified by the provided function. @@ -4505,7 +4698,7 @@ export interface Union> extends * use this option if you have verified that your refinements remain correct * after the transformation. */ - mapMembers>( + mapMembers>( f: (members: Members) => To, options?: { readonly unsafePreserveChecks?: boolean | undefined @@ -4513,13 +4706,13 @@ export interface Union> extends ): Union>> } -function makeUnion>( +function makeUnion>( ast: SchemaAST.Union, members: Members ): Union { return make(ast, { members, - mapMembers>( + mapMembers>( this: Union, f: (members: Members) => To, options?: { @@ -4559,7 +4752,7 @@ function makeUnion>( * @category constructors * @since 3.10.0 */ -export function Union>( +export function Union>( members: Members, options?: { mode?: "anyOf" | "oneOf" } ): Union { @@ -4580,7 +4773,7 @@ export interface Literals> /** * Map over the members of the union. */ - mapMembers>(f: (members: this["members"]) => To): Union>> + mapMembers>(f: (members: this["members"]) => To): Union>> pick>(literals: L2): Literals @@ -4610,7 +4803,7 @@ export function Literals>( return make(SchemaAST.union(members, "anyOf", undefined), { literals, members, - mapMembers>( + mapMembers>( this: Literals, f: (members: Literals["members"]) => To ): Union>> { @@ -4633,13 +4826,13 @@ export function Literals>( * @category models * @since 3.10.0 */ -export interface NullOr extends Union { +export interface NullOr extends Union { readonly "Rebuild": NullOr } interface NullOrLambda extends Lambda { - (self: S): NullOr - readonly "~lambda.out": this["~lambda.in"] extends Top ? NullOr : never + (self: S): NullOr + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? NullOr : never } /** @@ -4656,13 +4849,13 @@ export const NullOr = Struct_.lambda((self) => Union([self, Null]) * @category models * @since 3.10.0 */ -export interface UndefinedOr extends Union { +export interface UndefinedOr extends Union { readonly "Rebuild": UndefinedOr } interface UndefinedOrLambda extends Lambda { - (self: S): UndefinedOr - readonly "~lambda.out": this["~lambda.in"] extends Top ? UndefinedOr : never + (self: S): UndefinedOr + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? UndefinedOr : never } /** @@ -4679,13 +4872,13 @@ export const UndefinedOr = Struct_.lambda((self) => Union([se * @category models * @since 3.10.0 */ -export interface NullishOr extends Union { +export interface NullishOr extends Union { readonly "Rebuild": NullishOr } interface NullishOrLambda extends Lambda { - (self: S): NullishOr - readonly "~lambda.out": this["~lambda.in"] extends Top ? NullishOr : never + (self: S): NullishOr + readonly "~lambda.out": this["~lambda.in"] extends Constraint ? NullishOr : never } /** @@ -4702,25 +4895,26 @@ export const NullishOr = Struct_.lambda((self) => Union([self, * @category models * @since 3.10.0 */ -export interface suspend extends - Bottom< - S["Type"], - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface suspend extends + BottomLazy< SchemaAST.Suspend, suspend, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], S["~encoded.mutability"], S["~encoded.optionality"] > -{} +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] +} /** * Creates a suspended schema that defers evaluation until needed. This is @@ -4746,7 +4940,7 @@ export interface suspend extends * @category constructors * @since 3.10.0 */ -export function suspend(f: () => S): suspend { +export function suspend(f: () => S): suspend { return make(new SchemaAST.Suspend(() => f().ast)) } @@ -4779,18 +4973,11 @@ export function check( * @category filtering * @since 3.10.0 */ -export interface refine extends - Bottom< - T, - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface refine extends + BottomLazy< S["ast"], refine, - S["~type.make.in"], - T, S["~type.parameters"], - T, S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -4798,6 +4985,13 @@ export interface refine extends S["~encoded.optionality"] > { + readonly "Type": T + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": T + readonly "Iso": T readonly schema: S } @@ -4816,7 +5010,7 @@ export interface refine extends * @category filtering * @since 3.10.0 */ -export function refine( +export function refine( refinement: (value: S["Type"]) => value is T, annotations?: Annotations.Filter ) { @@ -4832,18 +5026,11 @@ type DistributeBrands = UnionToIntersection extends - Bottom< - S["Type"] & DistributeBrands, - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface brand extends + BottomLazy< S["ast"], brand, - S["~type.make.in"], - S["Type"] & DistributeBrands, S["~type.parameters"], - S["Type"] & DistributeBrands, S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -4851,6 +5038,13 @@ export interface brand extends S["~encoded.optionality"] > { + readonly "Type": S["Type"] & DistributeBrands + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["Type"] & DistributeBrands + readonly "Iso": S["Type"] & DistributeBrands readonly schema: S readonly identifier: string } @@ -4875,7 +5069,7 @@ export interface brand extends * @since 3.10.0 */ export function brand(identifier: B) { - return (schema: S): brand => + return (schema: S): brand => make(SchemaAST.brand(schema.ast, identifier), { schema, identifier }) } @@ -4900,18 +5094,11 @@ export function fromBrand>(identifier: string, ctor: * @category decoding * @since 4.0.0 */ -export interface middlewareDecoding extends - Bottom< - S["Type"], - S["Encoded"], - RD, - S["EncodingServices"], +export interface middlewareDecoding extends + BottomLazy< S["ast"], middlewareDecoding, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -4919,6 +5106,13 @@ export interface middlewareDecoding extends S["~encoded.optionality"] > { + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": RD + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] readonly schema: S } @@ -4947,7 +5141,7 @@ export interface middlewareDecoding extends * @category decoding * @since 4.0.0 */ -export function middlewareDecoding( +export function middlewareDecoding( decode: ( effect: Effect.Effect, SchemaIssue.Issue, S["DecodingServices"]>, options: SchemaAST.ParseOptions @@ -4966,18 +5160,11 @@ export function middlewareDecoding( * @category encoding * @since 4.0.0 */ -export interface middlewareEncoding extends - Bottom< - S["Type"], - S["Encoded"], - S["DecodingServices"], - RE, +export interface middlewareEncoding extends + BottomLazy< S["ast"], middlewareEncoding, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -4985,6 +5172,13 @@ export interface middlewareEncoding extends S["~encoded.optionality"] > { + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": RE + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] readonly schema: S } @@ -5013,7 +5207,7 @@ export interface middlewareEncoding extends * @category encoding * @since 4.0.0 */ -export function middlewareEncoding( +export function middlewareEncoding( encode: ( effect: Effect.Effect, SchemaIssue.Issue, S["EncodingServices"]>, options: SchemaAST.ParseOptions @@ -5048,9 +5242,9 @@ export function middlewareEncoding( * @category error handling * @since 4.0.0 */ -export function catchDecoding( +export function catchDecoding( f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue> -): (self: S) => S["Rebuild"] { +): (self: S) => middlewareDecoding { return catchDecodingWithContext(f) } @@ -5074,11 +5268,11 @@ export function catchDecoding( * @category error handling * @since 4.0.0 */ -export function catchDecodingWithContext( +export function catchDecodingWithContext( f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue, R> ) { return (self: S): middlewareDecoding => - self.pipe(middlewareDecoding(Effect.catchEager(f))) + middlewareDecoding(Effect.catchEager(f))(self) } /** @@ -5093,9 +5287,9 @@ export function catchDecodingWithContext( * @category error handling * @since 4.0.0 */ -export function catchEncoding( +export function catchEncoding( f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue> -): (self: S) => S["Rebuild"] { +): (self: S) => middlewareEncoding { return catchEncodingWithContext(f) } @@ -5119,11 +5313,11 @@ export function catchEncoding( * @category error handling * @since 4.0.0 */ -export function catchEncodingWithContext( +export function catchEncodingWithContext( f: (issue: SchemaIssue.Issue) => Effect.Effect, SchemaIssue.Issue, R> ) { return (self: S): middlewareEncoding => - self.pipe(middlewareEncoding(Effect.catchEager(f))) + middlewareEncoding(Effect.catchEager(f))(self) } /** @@ -5132,18 +5326,11 @@ export function catchEncodingWithContext( * @category transforming * @since 4.0.0 */ -export interface decodeTo extends - Bottom< - To["Type"], - From["Encoded"], - To["DecodingServices"] | From["DecodingServices"] | RD, - To["EncodingServices"] | From["EncodingServices"] | RE, +export interface decodeTo extends + BottomLazy< To["ast"], decodeTo, - To["~type.make.in"], - To["Iso"], To["~type.parameters"], - To["~type.make"], To["~type.mutability"], To["~type.optionality"], To["~type.constructor.default"], @@ -5151,6 +5338,13 @@ export interface decodeTo { + readonly "Type": To["Type"] + readonly "Encoded": From["Encoded"] + readonly "DecodingServices": To["DecodingServices"] | From["DecodingServices"] | RD + readonly "EncodingServices": To["EncodingServices"] | From["EncodingServices"] | RE + readonly "~type.make.in": To["~type.make.in"] + readonly "~type.make": To["~type.make"] + readonly "Iso": To["Iso"] readonly from: From readonly to: To } @@ -5161,7 +5355,7 @@ export interface decodeTo extends decodeTo {} +export interface compose extends decodeTo {} /** * Creates a schema that transforms from a source schema to a target schema. @@ -5211,15 +5405,15 @@ export interface compose extends decodeTo(to: To): (from: From) => compose -export function decodeTo( +export function decodeTo(to: To): (from: From) => compose +export function decodeTo( to: To, transformation: { readonly decode: SchemaGetter.Getter, NoInfer, RD> readonly encode: SchemaGetter.Getter, NoInfer, RE> } ): (from: From) => decodeTo -export function decodeTo( +export function decodeTo( to: To, transformation?: { readonly decode: SchemaGetter.Getter @@ -5284,12 +5478,12 @@ export function decodeTo(transformation: { +export function decode(transformation: { readonly decode: SchemaGetter.Getter readonly encode: SchemaGetter.Getter }) { return (self: S): decodeTo, S, RD, RE> => { - return self.pipe(decodeTo(toType(self), transformation)) + return decodeTo, S, RD, RE>(toType(self), transformation)(self) } } @@ -5322,17 +5516,17 @@ export function decode(transformation: { * @category transforming * @since 4.0.0 */ -export function encodeTo( +export function encodeTo( to: To -): (from: From) => decodeTo -export function encodeTo( +): (from: From) => decodeTo +export function encodeTo( to: To, transformation: { readonly decode: SchemaGetter.Getter, NoInfer, RD> readonly encode: SchemaGetter.Getter, NoInfer, RE> } ): (from: From) => decodeTo -export function encodeTo( +export function encodeTo( to: To, transformation?: { readonly decode: SchemaGetter.Getter @@ -5341,8 +5535,8 @@ export function encodeTo => { return transformation ? - to.pipe(decodeTo(from, transformation)) : - to.pipe(decodeTo(from)) + decodeTo(from, transformation)(to) : + decodeTo(from)(to) } } @@ -5371,12 +5565,12 @@ export function encodeTo(transformation: { +export function encode(transformation: { readonly decode: SchemaGetter.Getter readonly encode: SchemaGetter.Getter }) { return (self: S): decodeTo, RD, RE> => { - return toEncoded(self).pipe(decodeTo(self, transformation)) + return decodeTo, RD, RE>(self, transformation)(toEncoded(self)) } } @@ -5400,18 +5594,11 @@ export interface WithoutConstructorDefault { * @category constructors * @since 3.10.0 */ -export interface withConstructorDefault extends - Bottom< - S["Type"], - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface withConstructorDefault extends + BottomLazy< S["ast"], withConstructorDefault, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], "with-default", @@ -5419,6 +5606,13 @@ export interface withConstructorDefault { + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] readonly schema: S } @@ -5449,7 +5643,7 @@ export interface withConstructorDefault( +export function withConstructorDefault( // `S["~type.make.in"]` instead of `S["Type"]` is intentional here because // it makes easier to define the default value if there are nested defaults defaultValue: Effect.Effect @@ -5464,7 +5658,9 @@ export function withConstructorDefault extends decodeTo>, R> { +export interface withDecodingDefaultKey + extends decodeTo>, R> +{ readonly "Rebuild": withDecodingDefaultKey } @@ -5518,7 +5714,7 @@ export type DecodingDefaultOptions = { * @category decoding * @since 4.0.0 */ -export function withDecodingDefaultKey( +export function withDecodingDefaultKey( defaultValue: Effect.Effect, options?: DecodingDefaultOptions ) { @@ -5537,7 +5733,7 @@ export function withDecodingDefaultKey( * @category decoding * @since 4.0.0 */ -export interface withDecodingDefaultTypeKey +export interface withDecodingDefaultTypeKey extends decodeTo, R>, optionalKey> { readonly "Rebuild": withDecodingDefaultTypeKey @@ -5565,7 +5761,7 @@ export interface withDecodingDefaultTypeKey * @category decoding * @since 4.0.0 */ -export function withDecodingDefaultTypeKey( +export function withDecodingDefaultTypeKey( defaultValue: Effect.Effect, options?: DecodingDefaultOptions ) { @@ -5583,7 +5779,7 @@ export function withDecodingDefaultTypeKey( * @category decoding * @since 3.10.0 */ -export interface withDecodingDefault extends decodeTo>, R> { +export interface withDecodingDefault extends decodeTo>, R> { readonly "Rebuild": withDecodingDefault } @@ -5626,7 +5822,7 @@ export interface withDecodingDefault extends decodeTo< * @category decoding * @since 3.10.0 */ -export function withDecodingDefault( +export function withDecodingDefault( defaultValue: Effect.Effect, options?: DecodingDefaultOptions ) { @@ -5645,7 +5841,7 @@ export function withDecodingDefault( * @category decoding * @since 4.0.0 */ -export interface withDecodingDefaultType +export interface withDecodingDefaultType extends decodeTo, R>, optional> { readonly "Rebuild": withDecodingDefaultType @@ -5678,7 +5874,7 @@ export interface withDecodingDefaultType * @category decoding * @since 4.0.0 */ -export function withDecodingDefaultType( +export function withDecodingDefaultType( defaultValue: Effect.Effect, options?: DecodingDefaultOptions ) { @@ -5833,8 +6029,8 @@ type Flatten = Schemas extends readonly [infer Head, ...infer Tail] type TaggedUnionUtils< Tag extends PropertyKey, - Members extends ReadonlyArray, - Flattened extends ReadonlyArray = Flatten< + Members extends ReadonlyArray, + Flattened extends ReadonlyArray = Flatten< Members > > = { @@ -5868,7 +6064,7 @@ type TaggedUnionUtils< */ export type toTaggedUnion< Tag extends PropertyKey, - Members extends ReadonlyArray + Members extends ReadonlyArray > = Union & TaggedUnionUtils /** @@ -5896,7 +6092,7 @@ export type toTaggedUnion< * @since 4.0.0 */ export function toTaggedUnion(tag: Tag) { - return >( + return >( self: Union ): toTaggedUnion => { const cases: Record = {} @@ -5907,7 +6103,7 @@ export function toTaggedUnion(tag: Tag) { return Object.assign(self, { cases, isAnyOf, guards, match }) as any - function walk(schema: Top) { + function walk(schema: Constraint) { const ast = schema.ast if ( @@ -5950,17 +6146,19 @@ export function toTaggedUnion(tag: Tag) { * @category models * @since 4.0.0 */ -export interface TaggedUnion> extends - Bottom< - { [K in keyof Cases]: Cases[K]["Type"] }[keyof Cases], - { [K in keyof Cases]: Cases[K]["Encoded"] }[keyof Cases], - { [K in keyof Cases]: Cases[K]["DecodingServices"] }[keyof Cases], - { [K in keyof Cases]: Cases[K]["EncodingServices"] }[keyof Cases], +export interface TaggedUnion> extends + BottomLazy< SchemaAST.Union, - TaggedUnion, - { [K in keyof Cases]: Cases[K]["~type.make"] }[keyof Cases] + TaggedUnion > { + readonly "Type": { [K in keyof Cases]: Cases[K]["Type"] }[keyof Cases] + readonly "Encoded": { [K in keyof Cases]: Cases[K]["Encoded"] }[keyof Cases] + readonly "DecodingServices": { [K in keyof Cases]: Cases[K]["DecodingServices"] }[keyof Cases] + readonly "EncodingServices": { [K in keyof Cases]: Cases[K]["EncodingServices"] }[keyof Cases] + readonly "~type.make.in": { [K in keyof Cases]: Cases[K]["~type.make"] }[keyof Cases] + readonly "~type.make": { [K in keyof Cases]: Cases[K]["~type.make"] }[keyof Cases] + readonly "Iso": { [K in keyof Cases]: Cases[K]["Type"] }[keyof Cases] readonly cases: Cases readonly isAnyOf: ( keys: ReadonlyArray @@ -6023,17 +6221,10 @@ export function TaggedUnion extends - Bottom< - Self, - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], + BottomLazy< S["ast"], S["Rebuild"], - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -6041,6 +6232,13 @@ export interface Opaque extends S["~encoded.optionality"] > { + readonly "Type": Self + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] new(_: never): S["Type"] & Brand } @@ -6119,7 +6317,7 @@ export function instanceOf any, Iso = In * @since 4.0.0 */ export function link() { - return ( + return ( encodeTo: To, transformation: { readonly decode: SchemaGetter.Getter> @@ -8132,7 +8330,7 @@ export function isPropertiesLengthBetween(minimum: number, maximum: number, anno * @category Object checks * @since 4.0.0 */ -export function isPropertyNames(keySchema: Top, annotations?: Annotations.Filter) { +export function isPropertyNames(keySchema: Constraint, annotations?: Annotations.Filter) { const propertyNames = toEncoded(keySchema) const parser = SchemaParser._issue(propertyNames.ast) return makeFilter( @@ -8258,7 +8456,7 @@ export const Char: Char = String.check(isLengthBetween(1, 1)) * @category Option * @since 3.10.0 */ -export interface Option extends +export interface Option extends declareConstructor< Option_.Option, Option_.Option, @@ -8281,7 +8479,7 @@ export interface Option extends * @category Option * @since 4.0.0 */ -export type OptionIso = +export type OptionIso = | { readonly _tag: "None" } | { readonly _tag: "Some"; readonly value: A["Iso"] } @@ -8291,7 +8489,7 @@ export type OptionIso = * @category Option * @since 3.10.0 */ -export function Option(value: A): Option { +export function Option(value: A): Option { const schema = declareConstructor< Option_.Option, Option_.Option, @@ -8360,7 +8558,7 @@ export function Option(value: A): Option { * @category Option * @since 3.10.0 */ -export interface OptionFromNullOr extends decodeTo>, NullOr> { +export interface OptionFromNullOr extends decodeTo>, NullOr> { readonly "Rebuild": OptionFromNullOr } @@ -8375,7 +8573,7 @@ export interface OptionFromNullOr extends decodeTo(schema: S): OptionFromNullOr { +export function OptionFromNullOr(schema: S): OptionFromNullOr { return NullOr(schema).pipe(decodeTo( Option(toType(schema)), SchemaTransformation.optionFromNullOr() @@ -8388,7 +8586,7 @@ export function OptionFromNullOr(schema: S): OptionFromNullOr * @category Option * @since 3.10.0 */ -export interface OptionFromUndefinedOr extends decodeTo>, UndefinedOr> { +export interface OptionFromUndefinedOr extends decodeTo>, UndefinedOr> { readonly "Rebuild": OptionFromUndefinedOr } @@ -8404,7 +8602,7 @@ export interface OptionFromUndefinedOr extends decodeTo(schema: S): OptionFromUndefinedOr { +export function OptionFromUndefinedOr(schema: S): OptionFromUndefinedOr { return UndefinedOr(schema).pipe(decodeTo( Option(toType(schema)), SchemaTransformation.optionFromUndefinedOr() @@ -8417,7 +8615,7 @@ export function OptionFromUndefinedOr(schema: S): OptionFromUndef * @category Option * @since 3.10.0 */ -export interface OptionFromNullishOr extends decodeTo>, NullishOr> { +export interface OptionFromNullishOr extends decodeTo>, NullishOr> { readonly "Rebuild": OptionFromNullishOr } @@ -8434,7 +8632,7 @@ export interface OptionFromNullishOr extends decodeTo( +export function OptionFromNullishOr( schema: S, options?: { onNoneEncoding: null | undefined @@ -8452,7 +8650,7 @@ export function OptionFromNullishOr( * @category Option * @since 4.0.0 */ -export interface OptionFromOptionalKey extends decodeTo>, optionalKey> { +export interface OptionFromOptionalKey extends decodeTo>, optionalKey> { readonly "Rebuild": OptionFromOptionalKey } @@ -8467,7 +8665,7 @@ export interface OptionFromOptionalKey extends decodeTo(schema: S): OptionFromOptionalKey { +export function OptionFromOptionalKey(schema: S): OptionFromOptionalKey { return optionalKey(schema).pipe(decodeTo( Option(toType(schema)), SchemaTransformation.optionFromOptionalKey() @@ -8480,7 +8678,7 @@ export function OptionFromOptionalKey(schema: S): OptionFromOptio * @category Option * @since 4.0.0 */ -export interface OptionFromOptional extends decodeTo>, optional> { +export interface OptionFromOptional extends decodeTo>, optional> { readonly "Rebuild": OptionFromOptional } @@ -8497,7 +8695,7 @@ export interface OptionFromOptional extends decodeTo(schema: S): OptionFromOptional { +export function OptionFromOptional(schema: S): OptionFromOptional { return optional(schema).pipe(decodeTo( Option(toType(schema)), SchemaTransformation.optionFromOptional() @@ -8510,7 +8708,9 @@ export function OptionFromOptional(schema: S): OptionFromOptional * @category Option * @since 4.0.0 */ -export interface OptionFromOptionalNullOr extends decodeTo>, optional>> { +export interface OptionFromOptionalNullOr + extends decodeTo>, optional>> +{ readonly "Rebuild": OptionFromOptionalNullOr } @@ -8528,7 +8728,7 @@ export interface OptionFromOptionalNullOr extends decodeTo extends +export interface Result extends declareConstructor< Result_.Result, Result_.Result, @@ -8579,7 +8779,7 @@ export interface Result extends * @category schemas * @since 4.0.0 */ -export type ResultIso = +export type ResultIso = | { readonly _tag: "Success"; readonly success: A["Iso"] } | { readonly _tag: "Failure"; readonly failure: E["Iso"] } @@ -8589,7 +8789,7 @@ export type ResultIso = * @category schemas * @since 4.0.0 */ -export function Result( +export function Result( success: A, failure: E ): Result { @@ -8672,7 +8872,7 @@ export function Result( * @category Redacted * @since 3.10.0 */ -export interface Redacted extends +export interface Redacted extends declareConstructor< Redacted_.Redacted, Redacted_.Redacted, @@ -8703,10 +8903,11 @@ export interface Redacted extends * into JSON, it will fail with an error. This is useful when the wrapped schema is * sensitive and should not be exposed in JSON. * + * @see {@link RedactedFromValue} for decoding raw values and wrapping them in `Redacted`. * @category Redacted * @since 3.10.0 */ -export function Redacted(value: S, options?: { +export function Redacted(value: S, options?: { readonly label?: string | undefined readonly disallowJsonEncode?: boolean | undefined }): Redacted { @@ -8783,7 +8984,7 @@ export function Redacted(value: S, options?: { * @category Redacted * @since 4.0.0 */ -export interface RedactedFromValue +export interface RedactedFromValue extends decodeTo>, middlewareDecoding> { readonly "Rebuild": RedactedFromValue @@ -8796,8 +8997,8 @@ export interface RedactedFromValue * @category Redacted * @since 4.0.0 */ -export function redact(schema: S): middlewareDecoding { - return schema.pipe(middlewareDecoding(Effect.mapErrorEager(SchemaIssue.redact))) +export function redact(schema: S): middlewareDecoding { + return middlewareDecoding(Effect.mapErrorEager(SchemaIssue.redact))(schema) } /** @@ -8805,10 +9006,11 @@ export function redact(schema: S): middlewareDecoding(value: S, options?: { +export function RedactedFromValue(value: S, options?: { readonly label?: string | undefined readonly disallowEncode?: boolean | undefined }): RedactedFromValue { @@ -8837,7 +9039,7 @@ export function RedactedFromValue(value: S, options?: { * @category CauseReason * @since 4.0.0 */ -export interface CauseReason extends +export interface CauseReason extends declareConstructor< Cause_.Reason, Cause_.Reason, @@ -8861,7 +9063,7 @@ export interface CauseReason extends * @category CauseReason * @since 4.0.0 */ -export type CauseReasonIso = { +export type CauseReasonIso = { readonly _tag: "Fail" readonly error: E["Iso"] } | { @@ -8892,7 +9094,7 @@ export type CauseReasonIso = { * @category CauseReason * @since 4.0.0 */ -export function CauseReason(error: E, defect: D): CauseReason { +export function CauseReason(error: E, defect: D): CauseReason { const schema = declareConstructor, Cause_.Reason, CauseReasonIso>()( [error, defect], ([error, defect]) => (input, ast, options) => { @@ -9010,7 +9212,7 @@ function causeReasonToFormatter(error: Formatter, defect: Formatter extends +export interface Cause extends declareConstructor< Cause_.Cause, Cause_.Cause, @@ -9038,7 +9240,7 @@ export interface Cause extends * @category Cause * @since 4.0.0 */ -export type CauseIso = ReadonlyArray> +export type CauseIso = ReadonlyArray> /** * Creates a schema for `Cause` values using separate schemas for typed failures @@ -9061,7 +9263,7 @@ export type CauseIso = ReadonlyArray(error: E, defect: D): Cause { +export function Cause(error: E, defect: D): Cause { const schema = declareConstructor, Cause_.Cause, CauseIso>()( [error, defect], ([error, defect]) => { @@ -9287,7 +9489,7 @@ export function Defect(options?: ErrorOptions): Defect { * @category Exit * @since 3.10.0 */ -export interface Exit extends +export interface Exit extends declareConstructor< Exit_.Exit, Exit_.Exit, @@ -9312,7 +9514,7 @@ export interface Exit extends * @category Exit * @since 4.0.0 */ -export type ExitIso = { +export type ExitIso = { readonly _tag: "Success" readonly value: A["Iso"] } | { @@ -9332,7 +9534,11 @@ export type ExitIso = { * @category Exit * @since 3.10.0 */ -export function Exit(value: A, error: E, defect: D): Exit { +export function Exit( + value: A, + error: E, + defect: D +): Exit { const schema = declareConstructor< Exit_.Exit, Exit_.Exit, @@ -9439,7 +9645,7 @@ export function Exit(value: A, erro * @category ReadonlyMap * @since 4.0.0 */ -export interface $ReadonlyMap extends +export interface $ReadonlyMap extends declareConstructor< globalThis.ReadonlyMap, globalThis.ReadonlyMap, @@ -9459,7 +9665,9 @@ export interface $ReadonlyMap extends * @category ReadonlyMap * @since 4.0.0 */ -export type ReadonlyMapIso = ReadonlyArray +export type ReadonlyMapIso = ReadonlyArray< + readonly [Key["Iso"], Value["Iso"]] +> function oneOfArbitraries( fc: typeof FastCheck, @@ -9558,7 +9766,10 @@ function entriesArbitrary( * @category ReadonlyMap * @since 3.10.0 */ -export function ReadonlyMap(key: Key, value: Value): $ReadonlyMap { +export function ReadonlyMap( + key: Key, + value: Value +): $ReadonlyMap { const schema = declareConstructor< globalThis.ReadonlyMap, globalThis.ReadonlyMap, @@ -9619,7 +9830,7 @@ export function ReadonlyMap(key: Key, value: * @category HashMap * @since 3.10.0 */ -export interface HashMap extends +export interface HashMap extends declareConstructor< HashMap_.HashMap, HashMap_.HashMap, @@ -9639,7 +9850,9 @@ export interface HashMap extends * @category HashMap * @since 4.0.0 */ -export type HashMapIso = ReadonlyArray +export type HashMapIso = ReadonlyArray< + readonly [Key["Iso"], Value["Iso"]] +> /** * Schema for hash maps whose keys and values conform to the provided schemas. @@ -9647,7 +9860,7 @@ export type HashMapIso = ReadonlyArray(key: Key, value: Value): HashMap { +export function HashMap(key: Key, value: Value): HashMap { const schema = declareConstructor< HashMap_.HashMap, HashMap_.HashMap, @@ -9709,7 +9922,7 @@ export function HashMap(key: Key, value: Val * @category ReadonlySet * @since 4.0.0 */ -export interface $ReadonlySet extends +export interface $ReadonlySet extends declareConstructor< globalThis.ReadonlySet, globalThis.ReadonlySet, @@ -9728,7 +9941,7 @@ export interface $ReadonlySet extends * @category ReadonlySet * @since 4.0.0 */ -export type ReadonlySetIso = ReadonlyArray +export type ReadonlySetIso = ReadonlyArray /** * Schema for readonly sets whose values conform to the provided element schema. @@ -9736,7 +9949,7 @@ export type ReadonlySetIso = ReadonlyArray * @category ReadonlySet * @since 3.10.0 */ -export function ReadonlySet(value: Value): $ReadonlySet { +export function ReadonlySet(value: Value): $ReadonlySet { const schema = declareConstructor< globalThis.ReadonlySet, globalThis.ReadonlySet, @@ -9798,7 +10011,7 @@ export function ReadonlySet(value: Value): $ReadonlySet extends +export interface HashSet extends declareConstructor< HashSet_.HashSet, HashSet_.HashSet, @@ -9817,7 +10030,7 @@ export interface HashSet extends * @category HashSet * @since 4.0.0 */ -export type HashSetIso = ReadonlyArray +export type HashSetIso = ReadonlyArray /** * Schema for hash sets whose values conform to the provided element schema. @@ -9825,7 +10038,7 @@ export type HashSetIso = ReadonlyArray * @category HashSet * @since 3.10.0 */ -export function HashSet(value: Value): HashSet { +export function HashSet(value: Value): HashSet { const schema = declareConstructor< HashSet_.HashSet, HashSet_.HashSet, @@ -9887,7 +10100,7 @@ export function HashSet(value: Value): HashSet { * @category Chunk * @since 3.10.0 */ -export interface Chunk extends +export interface Chunk extends declareConstructor< Chunk_.Chunk, Chunk_.Chunk, @@ -9913,7 +10126,7 @@ export interface Chunk extends * @category Chunk * @since 4.0.0 */ -export type ChunkIso = ReadonlyArray +export type ChunkIso = ReadonlyArray /** * Schema for chunks whose values conform to the provided element schema. @@ -9921,7 +10134,7 @@ export type ChunkIso = ReadonlyArray * @category Chunk * @since 3.10.0 */ -export function Chunk(value: Value): Chunk { +export function Chunk(value: Value): Chunk { const schema = declareConstructor< Chunk_.Chunk, Chunk_.Chunk, @@ -10139,7 +10352,7 @@ type DateArbitraryConstraints = FastCheck.DateConstraints & { } function dateArbitraryConstraints( - constraint: Annotations.ToArbitrary.Constraint | undefined, + constraint: Annotations.ToArbitrary.GenerationConstraint | undefined, ordered: Annotations.ToArbitrary.OrderedConstraint | undefined, base?: DateArbitraryConstraints | undefined, toDate?: (value: T) => globalThis.Date @@ -10697,7 +10910,7 @@ export const UnknownFromJsonString: UnknownFromJsonString = fromJsonString(Unkno * @category models * @since 4.0.0 */ -export interface fromJsonString extends decodeTo { +export interface fromJsonString extends decodeTo { readonly "Rebuild": fromJsonString } @@ -10767,8 +10980,11 @@ export interface fromJsonString extends decodeTo { * @category constructors * @since 4.0.0 */ -export function fromJsonString(schema: S): fromJsonString { +export function fromJsonString(schema: S): fromJsonString { + const identifier = SchemaAST.resolveIdentifier(schema.ast) return String.annotate({ + // Give the transport wrapper its own name so the decoded payload keeps its identifier. + identifier: identifier === undefined ? undefined : `${identifier}JsonString`, expected: "a string that will be decoded as JSON", contentMediaType: "application/json", contentSchema: SchemaAST.toEncoded(schema.ast) @@ -10919,7 +11135,7 @@ export const FormData: FormData = instanceOf(globalThis.FormData, { * @category FormData * @since 4.0.0 */ -export interface fromFormData extends decodeTo { +export interface fromFormData extends decodeTo { readonly "Rebuild": fromFormData } @@ -10939,10 +11155,8 @@ export interface fromFormData extends decodeTo { * * You can express nested values using bracket notation. * - * If you want to decode values that are not strings, use - * `Schema.toCodecStringTree` with the `keepDeclarations: true` option. - * This serializer preserves values such as numbers and `Blob` objects when - * compatible with the schema. + * If you want to decode string fields into non-string primitive values, use + * `Schema.toCodecStringTree`. * * **Example** (Decoding a flat structure) * @@ -10996,8 +11210,7 @@ export interface fromFormData extends decodeTo { * Schema.toCodecStringTree( * Schema.Struct({ * a: Schema.Int - * }), - * { keepDeclarations: true } + * }) * ) * ) * @@ -11011,7 +11224,7 @@ export interface fromFormData extends decodeTo { * @category decoding * @since 4.0.0 */ -export function fromFormData(schema: S): fromFormData { +export function fromFormData(schema: S): fromFormData { return FormData.pipe(decodeTo(schema, SchemaTransformation.fromFormData)) } @@ -11060,7 +11273,7 @@ export const URLSearchParams: URLSearchParams = instanceOf(globalThis.URLSearchP * @category search params * @since 4.0.0 */ -export interface fromURLSearchParams extends decodeTo { +export interface fromURLSearchParams extends decodeTo { readonly "Rebuild": fromURLSearchParams } @@ -11144,7 +11357,7 @@ export interface fromURLSearchParams extends decodeTo(schema: S): fromURLSearchParams { +export function fromURLSearchParams(schema: S): fromURLSearchParams { return URLSearchParams.pipe(decodeTo(schema, SchemaTransformation.fromURLSearchParams)) } @@ -12163,18 +12376,11 @@ export const DateTimeZonedFromString: DateTimeZonedFromString = DateTimeZonedStr * @category models * @since 3.10.0 */ -export interface Class extends - Bottom< - Self, - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface Class extends + BottomLazy< SchemaAST.Declaration, decodeTo, S>, - RequiredKeys extends never ? void | S["~type.make.in"] : S["~type.make.in"], - S["Iso"], readonly [S], - Self, S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -12182,6 +12388,14 @@ export interface Class { + readonly "Type": Self + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": RequiredKeys extends never ? void | S["~type.make.in"] + : S["~type.make.in"] + readonly "~type.make": Self + readonly "Iso": S["Iso"] new( ...args: {} extends S["~type.make.in"] ? [props?: S["~type.make.in"], options?: MakeOptions] : [props: S["~type.make.in"], options?: MakeOptions] @@ -12262,6 +12476,8 @@ type InheritStaticMembers = C & Pick, @@ -12278,9 +12494,12 @@ function makeClass< const out = class extends Inherited { constructor(...[input, options]: ReadonlyArray) { - input = input ?? {} - const validated = struct.make(input, options) - super({ ...input, ...validated }, { ...options, disableChecks: true }) + const internalOptions = options as MakeOptions | undefined + const payload = internalOptions?.["~payload"] + const value = payload?.token === payloadToken + ? payload.value + : struct.make(input ?? {}, options) + super(value, { ...options, disableChecks: true, "~payload": { token: payloadToken, value } }) } static readonly [TypeId] = TypeId @@ -12368,7 +12587,7 @@ function getClassTypeId(identifier: string) { return `~effect/Schema/Class/${identifier}` } -function getClassSchemaFactory( +function getClassSchemaFactory( from: S, identifier: string, annotations: Annotations.Declaration | undefined @@ -12377,34 +12596,34 @@ function getClassSchemaFactory( return ) => any) & { readonly identifier: string }>( self: Self ): decodeTo, S> => { - if (memo === undefined) { - const transformation = getClassTransformation(self) - const to = make>( - new SchemaAST.Declaration( - [from.ast], - () => (input, ast) => { - return input instanceof self || - Predicate.hasProperty(input, getClassTypeId(identifier)) ? - Effect.succeed(input) : - Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) - }, - { - identifier, - [SchemaAST.ClassTypeId]: ([from]: readonly [SchemaAST.AST]) => new SchemaAST.Link(from, transformation), - toCodec: ([from]: readonly [Codec]) => new SchemaAST.Link(from.ast, transformation), - toArbitrary: ([from]: readonly [Annotations.ToArbitrary.TypeParameter]) => () => ({ - arbitrary: from.arbitrary.map((args: S["Type"]) => new self(args)), - terminal: from.terminal?.map((args: S["Type"]) => new self(args)) - }), - toFormatter: ([from]: readonly [Formatter]) => (t: Self) => `${self.identifier}(${from(t)})`, - "~sentinels": SchemaAST.collectSentinels(from.ast), - ...annotations - } - ) - ) - memo = from.pipe(decodeTo(to, transformation)) + if (memo !== undefined) { + return memo } - return memo + const transformation = getClassTransformation(self) + const to = make>( + new SchemaAST.Declaration( + [from.ast], + () => (input, ast) => { + return input instanceof self || + Predicate.hasProperty(input, getClassTypeId(identifier)) ? + Effect.succeed(input) : + Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + }, + { + identifier, + [SchemaAST.ClassTypeId]: ([from]: readonly [SchemaAST.AST]) => new SchemaAST.Link(from, transformation), + toCodec: ([from]: readonly [Codec]) => new SchemaAST.Link(from.ast, transformation), + toArbitrary: ([from]: readonly [Annotations.ToArbitrary.TypeParameter]) => () => ({ + arbitrary: from.arbitrary.map((args: S["Type"]) => new self(args)), + terminal: from.terminal?.map((args: S["Type"]) => new self(args)) + }), + toFormatter: ([from]: readonly [Formatter]) => (t: Self) => `${self.identifier}(${from(t)})`, + "~sentinels": SchemaAST.collectSentinels(from.ast), + ...annotations + } + ) + ) + return memo = decodeTo, S>(to, transformation)(from) } } @@ -12714,7 +12933,7 @@ export type LazyArbitrary = (fc: typeof FastCheck) => FastCheck.Arbitrary * @category Arbitrary * @since 4.0.0 */ -export function toArbitraryLazy(schema: S): LazyArbitrary { +export function toArbitraryLazy(schema: S): LazyArbitrary { const lawc = InternalArbitrary.memoized(schema.ast) return (fc) => lawc(fc, {}) } @@ -12748,12 +12967,12 @@ export function toArbitraryLazy(schema: S): LazyArbitrary(schema: S): FastCheck.Arbitrary -export function toArbitrary( +export function toArbitrary(schema: S): FastCheck.Arbitrary +export function toArbitrary( schema: S, options: { readonly report: true } ): Annotations.ToArbitrary.WithReport> -export function toArbitrary( +export function toArbitrary( schema: S, options?: { readonly report?: boolean } ): FastCheck.Arbitrary | Annotations.ToArbitrary.WithReport> { @@ -12804,14 +13023,14 @@ export function overrideToFormatter(toFormatter: () => Formatter< * @category Formatter * @since 4.0.0 */ -export function toFormatter(schema: Schema, options?: { +export function toFormatter(schema: S, options?: { readonly onBefore?: | ((ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Formatter) => Formatter | undefined) | undefined -}): Formatter { +}): Formatter { return recur(schema.ast) - function recur(ast: SchemaAST.AST): Formatter { + function recur(ast: SchemaAST.AST): Formatter { // --------------------------------------------- // handle annotation // --------------------------------------------- @@ -12995,7 +13214,7 @@ export function toEquivalence(schema: Schema): Equivalence.Equivalence * @category Representation * @since 4.0.0 */ -export function toRepresentation(schema: Top): SchemaRepresentation.Document { +export function toRepresentation(schema: Constraint): SchemaRepresentation.Document { return InternalStandard.fromAST(schema.ast) } @@ -13096,7 +13315,10 @@ export interface ToJsonSchemaOptions { * @category converting * @since 4.0.0 */ -export function toJsonSchemaDocument(schema: Top, options?: ToJsonSchemaOptions): JsonSchema.Document<"draft-2020-12"> { +export function toJsonSchemaDocument( + schema: Constraint, + options?: ToJsonSchemaOptions +): JsonSchema.Document<"draft-2020-12"> { const sd = toRepresentation(schema) const jd = InternalStandard.toJsonSchemaDocument(sd, options) return { @@ -13110,6 +13332,34 @@ export function toJsonSchemaDocument(schema: Top, options?: ToJsonSchemaOptions) // Canonical Codecs // ----------------------------------------------------------------------------- +/** + * Type-level representation returned by {@link toCodecJson}. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export interface toCodecJson extends + BottomLazy< + S["ast"], + toCodecJson, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": Json + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + /** * Derives a canonical JSON codec from a schema. The encoded form is `Json`, and * decoding produces the schema's `Type`. @@ -13117,11 +13367,11 @@ export function toJsonSchemaDocument(schema: Top, options?: ToJsonSchemaOptions) * @category Canonical Codecs * @since 4.0.0 */ -export function toCodecJson(schema: Codec): Codec { - return make(toCodecJsonTop(schema.ast)) +export function toCodecJson(schema: S): toCodecJson { + return make(toCodecJsonTop(schema.ast), { schema }) } -const toCodecJsonTop = SchemaAST.toCodec((ast) => { +const toCodecJsonTop = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { const out = toCodecJsonBase(ast, toCodecJsonTop) return out !== ast && SchemaAST.isOptional(ast) ? SchemaAST.optionalKeyLastLink(out) : out }) @@ -13188,7 +13438,7 @@ function toCodecJsonBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Sche * @category Canonical Codecs * @since 4.0.0 */ -export function toCodecIso(schema: S): Codec { +export function toCodecIso(schema: S): Codec { return make(toCodecIsoTop(SchemaAST.toType(schema.ast))) } @@ -13226,6 +13476,34 @@ function toCodecIsoBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Schem */ export type StringTree = Tree +/** + * Type-level representation returned by {@link toCodecStringTree}. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export interface toCodecStringTree extends + BottomLazy< + S["ast"], + toCodecStringTree, + ReadonlyArray, + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": StringTree + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] + readonly schema: S +} + /** * Converts a schema to the StringTree canonical codec, where every leaf value * becomes a string while preserving the original structure. @@ -13235,33 +13513,60 @@ export type StringTree = Tree * Declarations are converted to `undefined` (unless they have a * `toCodecJson` or `toCodec` annotation). * - * Options: + * @category Canonical Codecs + * @since 4.0.0 + */ +export function toCodecStringTree(schema: S): toCodecStringTree { + return make(serializerStringTree(schema.ast), { schema }) +} + +/** + * Type-level representation returned by {@link toCodecArrayFromSingle}. + * + * @category Canonical Codecs + * @since 4.0.0 + */ +export interface toCodecArrayFromSingle extends + BottomLazy< + S["ast"], + toCodecArrayFromSingle, + S["~type.parameters"], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > +{ + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] +} + +/** + * Allows array schemas to decode from either an array input or a single value + * input. + * + * **When to use** * - * - `keepDeclarations`: if `true`, it **does not** convert declarations to - * `undefined` but instead keeps them as they are (unless they have a - * `toCodecJson` or `toCodec` annotation). + * Use when you need to accept transport formats that may represent a + * single-item array as a bare value, such as query-string or form-data adapters. * - * Defaults to `false`. + * **Gotchas** + * + * This combinator is intentionally not part of `toCodecStringTree`; it adds a + * decoding convenience rather than a canonical StringTree representation. It + * does not parse comma-separated strings. * * @category Canonical Codecs * @since 4.0.0 */ -export function toCodecStringTree(schema: Codec): Codec -export function toCodecStringTree( - schema: Codec, - options: { readonly keepDeclarations: true } // Used in FormData -): Codec -export function toCodecStringTree( - schema: Codec, - options?: { readonly keepDeclarations?: boolean | undefined } -): Codec { - return make( - toCodecEnsureArray( - options?.keepDeclarations === true - ? serializerStringTreeKeepDeclarations(schema.ast) - : serializerStringTree(schema.ast) - ) - ) +export function toCodecArrayFromSingle(schema: S): toCodecArrayFromSingle { + return make(toCodecArrayFromSingleTop(schema.ast)) } type XmlEncoderOptions = { @@ -13477,7 +13782,15 @@ const booleanToString = new SchemaAST.Link( ) ) -const serializerStringTree = SchemaAST.toCodec((ast) => { +const SERIALIZER_ENSURE_ARRAY = "~effect/Schema/SERIALIZER_ENSURE_ARRAY" + +const isSerializerArrayFromSingle = (ast: SchemaAST.AST): boolean => + SchemaAST.isUnion(ast) && ast.annotations?.[SERIALIZER_ENSURE_ARRAY] === true + +const serializerStringTree = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { + if (isSerializerArrayFromSingle(ast)) { + return ast + } const out = serializerTree(ast, serializerStringTree, (ast) => SchemaAST.replaceEncoding(ast, [unknownToUndefined])) if (out !== ast && SchemaAST.isOptional(ast)) { return SchemaAST.optionalKeyLastLink(out) @@ -13493,53 +13806,46 @@ const unknownToUndefined = new SchemaAST.Link( ) ) -const serializerStringTreeKeepDeclarations = SchemaAST.toCodec((ast) => { - const out = serializerTree(ast, serializerStringTreeKeepDeclarations, identity) - if (out !== ast && SchemaAST.isOptional(ast)) { - return SchemaAST.optionalKeyLastLink(out) - } - return out -}) +const toArrayFromSingleInputElement = (ast: SchemaAST.AST): SchemaAST.AST => + SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(SchemaAST.unknown) : SchemaAST.unknown -const SERIALIZER_ENSURE_ARRAY = "~effect/Schema/SERIALIZER_ENSURE_ARRAY" +const arrayFromSingleTransformation = new SchemaTransformation.Transformation( + SchemaGetter.transform((input: ReadonlyArray | string) => typeof input === "string" ? [input] : input), + SchemaGetter.passthrough() +) -const toCodecEnsureArray = SchemaAST.toCodec((ast) => { - if (SchemaAST.isUnion(ast) && ast.annotations?.[SERIALIZER_ENSURE_ARRAY]) { +const toCodecArrayFromSingleTop = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { + if (isSerializerArrayFromSingle(ast)) { return ast } - const out = onSerializerEnsureArray(ast) + const out = onSerializerArrayFromSingle(ast) if (SchemaAST.isArrays(out)) { - const ensure = new SchemaAST.Union( - [ - out, - SchemaAST.decodeTo( - SchemaAST.string, - out, - new SchemaTransformation.Transformation( - SchemaGetter.split(), - SchemaGetter.passthrough() - ) - ) - ], - "anyOf", - { [SERIALIZER_ENSURE_ARRAY]: true } + const ensure = SchemaAST.decodeTo( + new SchemaAST.Union( + [ + new SchemaAST.Arrays( + out.isMutable, + out.elements.map(toArrayFromSingleInputElement), + out.rest.map(toArrayFromSingleInputElement) + ), + SchemaAST.string + ], + "anyOf", + { [SERIALIZER_ENSURE_ARRAY]: true } + ), + out, + arrayFromSingleTransformation ) return SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(ensure) : ensure } return out }) -function onSerializerEnsureArray(ast: SchemaAST.AST): SchemaAST.AST { - switch (ast._tag) { - default: - return ast - case "Declaration": - case "Arrays": - case "Objects": - case "Union": - case "Suspend": - return ast.recur(toCodecEnsureArray) - } +function onSerializerArrayFromSingle(ast: SchemaAST.AST): SchemaAST.AST { + return ast._tag === "Declaration" || ast._tag === "Arrays" || ast._tag === "Objects" || ast._tag === "Union" || + ast._tag === "Suspend" + ? ast.recur(toCodecArrayFromSingleTop) + : ast } // ----------------------------------------------------------------------------- @@ -13553,7 +13859,7 @@ function onSerializerEnsureArray(ast: SchemaAST.AST): SchemaAST.AST { * @category Optic * @since 4.0.0 */ -export function toIso(schema: S): Optic_.Iso { +export function toIso(schema: S): Optic_.Iso { const serializer = toCodecIso(schema) return Optic_.makeIso(SchemaParser.encodeSync(serializer), SchemaParser.decodeSync(serializer)) } @@ -13564,7 +13870,7 @@ export function toIso(schema: S): Optic_.Iso * @category Optic * @since 4.0.0 */ -export function toIsoSource(_: S): Optic_.Iso { +export function toIsoSource(_: S): Optic_.Iso { return Optic_.id() } @@ -13574,7 +13880,7 @@ export function toIsoSource(_: S): Optic_.Iso(_: S): Optic_.Iso { +export function toIsoFocus(_: S): Optic_.Iso { return Optic_.id() } @@ -13584,18 +13890,11 @@ export function toIsoFocus(_: S): Optic_.Iso * @category Optic * @since 4.0.0 */ -export interface overrideToCodecIso extends - Bottom< - S["Type"], - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface overrideToCodecIso extends + BottomLazy< S["ast"], overrideToCodecIso, - S["~type.make.in"], - Iso, S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -13603,6 +13902,13 @@ export interface overrideToCodecIso extends S["~encoded.optionality"] > { + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": Iso readonly schema: S } @@ -13623,7 +13929,7 @@ export interface overrideToCodecIso extends * @category Optic * @since 4.0.0 */ -export function overrideToCodecIso( +export function overrideToCodecIso( to: Codec, transformation: { readonly decode: SchemaGetter.Getter @@ -13696,7 +14002,7 @@ export interface TreeRecord { * @category Tree * @since 4.0.0 */ -export function Tree(node: S) { +export function Tree(node: S) { const Tree$ref = suspend((): Codec< Tree, Tree, @@ -13813,7 +14119,7 @@ export const MutableJson: Codec = make(SchemaAST.MutableJson) * @category Schema Resolvers * @since 4.0.0 */ -export function resolveAnnotations( +export function resolveAnnotations( schema: S ): Annotations.Bottom | undefined { return InternalAnnotations.resolve(schema.ast) @@ -13827,7 +14133,7 @@ export function resolveAnnotations( * @category Schema Resolvers * @since 4.0.0 */ -export function resolveAnnotationsKey(schema: S): Annotations.Key | undefined { +export function resolveAnnotationsKey(schema: S): Annotations.Key | undefined { return schema.ast.context?.annotations } @@ -13956,7 +14262,7 @@ export declare namespace Annotations { * @category models * @since 4.0.0 */ - export interface Bottom> extends Documentation { + export interface Bottom> extends Documentation { /** * Complete message to use when this schema node reports an issue. * @@ -14016,7 +14322,7 @@ export declare namespace Annotations { * @category utility types * @since 4.0.0 */ - export type Type> = { + export type Type> = { readonly [K in keyof TypeParameters]: Codec } /** @@ -14025,7 +14331,7 @@ export declare namespace Annotations { * @category utility types * @since 4.0.0 */ - export type Encoded> = { + export type Encoded> = { readonly [K in keyof TypeParameters]: Codec } } @@ -14040,7 +14346,7 @@ export declare namespace Annotations { * @category models * @since 4.0.0 */ - export interface Declaration = readonly []> + export interface Declaration = readonly []> extends Bottom { readonly toCodec?: @@ -14153,7 +14459,7 @@ export declare namespace Annotations { * @since 4.0.0 */ export interface Filter { - readonly constraint?: Constraint | undefined + readonly constraint?: GenerationConstraint | undefined readonly candidate?: Candidate | undefined } @@ -14205,10 +14511,10 @@ export declare namespace Annotations { * * **Details** * - * `Constraint` is a generation hint for the current schema AST node, not a - * self-describing validation contract. Each generator consumes the fields it - * understands for the current node and ignores the rest; final schema - * filters still validate every generated value. + * `GenerationConstraint` is a generation hint for the current schema AST + * node, not a self-describing validation contract. Each generator consumes + * the fields it understands for the current node and ignores the rest; + * final schema filters still validate every generated value. * * `minLength` and `maxLength` represent node-local cardinality: string * length for strings, array length for arrays, final own-property count for @@ -14221,7 +14527,7 @@ export declare namespace Annotations { * @category models * @since 4.0.0 */ - export interface Constraint { + export interface GenerationConstraint { readonly minLength?: number | undefined readonly maxLength?: number | undefined readonly patterns?: readonly [string, ...Array] @@ -14264,7 +14570,7 @@ export declare namespace Annotations { * @since 4.0.0 */ export interface Context { - readonly constraint?: ToArbitrary.Constraint | undefined + readonly constraint?: ToArbitrary.GenerationConstraint | undefined readonly recursion?: ToArbitrary.Recursion | undefined } @@ -14330,7 +14636,7 @@ export declare namespace Annotations { * @category models * @since 4.0.0 */ - export interface Declaration> { + export interface Declaration> { ( /* Arbitrary derivations for any type parameters of the schema (if present) */ typeParameters: { readonly [K in keyof TypeParameters]: TypeParameter } @@ -14407,7 +14713,7 @@ export declare namespace Annotations { * @category models * @since 4.0.0 */ - export interface Declaration> { + export interface Declaration> { ( /* Formatters for any type parameters of the schema (if present) */ typeParameters: { readonly [K in keyof TypeParameters]: Formatter } @@ -14432,7 +14738,7 @@ export declare namespace Annotations { * @category models * @since 4.0.0 */ - export interface Declaration> { + export interface Declaration> { ( /* Equivalences for any type parameters of the schema (if present) */ typeParameters: { readonly [K in keyof TypeParameters]: Equivalence.Equivalence } diff --git a/.context/effect/packages/effect/src/SchemaAST.ts b/.context/effect/packages/effect/src/SchemaAST.ts index 683056ce9..a3b7c5450 100644 --- a/.context/effect/packages/effect/src/SchemaAST.ts +++ b/.context/effect/packages/effect/src/SchemaAST.ts @@ -680,7 +680,7 @@ export class Declaration extends Base { } private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { const tps = mapOrSame(this.typeParameters, recur) - return tps === this.typeParameters ? + return tps === this.typeParameters && checks === this.checks && encodingChecks === this.encodingChecks ? this : new Declaration(tps, this.run, this.annotations, checks, undefined, this.context, encodingChecks) } @@ -793,13 +793,20 @@ export { } /** - * AST node matching the `void` type (accepts `undefined` at runtime). + * AST node matching TypeScript `void` return-value semantics. + * + * **When to use** + * + * Use when you need an AST node for a value whose result is intentionally + * ignored. * * **Details** * - * Behaves like {@link Undefined} for parsing but represents the TypeScript - * `void` type semantically. + * Parsers built from this node accept any present runtime input and map it to + * `undefined`. Public schemas built from it may still expose `void` as their + * typed decoded and encoded representation. * + * @see {@link undefined} for the AST singleton that matches only exact `undefined` * @see {@link void_ void} * @see {@link isVoid} * @category models @@ -809,7 +816,7 @@ export class Void extends Base { readonly _tag = "Void" /** @internal */ getParser() { - return fromConst(this, undefined) + return fromAnyToConst(undefined) } /** @internal */ toCodecJson(): AST { @@ -828,8 +835,13 @@ export { * * **When to use** * - * Use when constructing or comparing AST nodes that represent the TypeScript - * `void` type and accept `undefined` at runtime. + * Use when constructing or comparing AST nodes for TypeScript `void` return + * values whose result is intentionally ignored. + * + * **Details** + * + * The node parses any present runtime value as `undefined`; schemas may still + * expose `void` on their typed decoded and encoded sides. * * @see {@link Void} for the AST node class * @see {@link undefined} for the sibling AST singleton that matches exactly `undefined` @@ -1704,7 +1716,8 @@ export class Arrays extends Base { private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { const elements = mapOrSame(this.elements, recur) const rest = mapOrSame(this.rest, recur) - return elements === this.elements && rest === this.rest ? + return elements === this.elements && rest === this.rest && checks === this.checks && + encodingChecks === this.encodingChecks ? this : new Arrays( this.isMutable, @@ -2245,7 +2258,8 @@ export class Objects extends Base { : new IndexSignature(p, t, merge) }) - return props === this.propertySignatures && indexes === this.indexSignatures + return props === this.propertySignatures && indexes === this.indexSignatures && checks === this.checks && + encodingChecks === this.encodingChecks ? this : new Objects( props, @@ -2345,7 +2359,7 @@ export function struct( } /** @internal */ -export function getAST(self: S): S["ast"] { +export function getAST(self: S): S["ast"] { return self.ast } @@ -2358,7 +2372,7 @@ export function tuple( } /** @internal */ -export function union>( +export function union>( members: Members, mode: "anyOf" | "oneOf", checks: Checks | undefined @@ -2413,7 +2427,6 @@ function getCandidateTypes(ast: AST): ReadonlyArray { case "Null": return ["null"] case "Undefined": - case "Void": return ["undefined"] case "String": case "TemplateLiteral": @@ -2661,7 +2674,7 @@ export class Union extends Base { } private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { const types = mapOrSame(this.types, recur) - return types === this.types ? + return types === this.types && checks === this.checks && encodingChecks === this.encodingChecks ? this : new Union(types, this.mode, this.annotations, checks, undefined, this.context, encodingChecks) } @@ -2860,19 +2873,6 @@ export class Suspend extends Base { } } -/** @internal */ -export function getEncodingChecks(ast: AST): Checks | undefined { - switch (ast._tag) { - case "Declaration": - case "Arrays": - case "Objects": - case "Union": - return ast.encodingChecks - default: - return undefined - } -} - // ----------------------------------------------------------------------------- // Checks // ----------------------------------------------------------------------------- @@ -3140,6 +3140,14 @@ export function applyToLastLink(f: (ast: AST) => AST) { return (ast: A): A => ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, f)) : ast } +/** @internal */ +export function applyToSelfOrLastLinkEncoding(f: (ast: AST) => AST) { + function out(ast: AST): AST { + return ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, out)) : f(ast) + } + return memoize(out) +} + /** @internal */ export function middlewareDecoding( ast: AST, @@ -3384,9 +3392,13 @@ export const toType = memoize((ast: A): A => { } const out: any = ast const type = out.recur?.(toType) ?? out - if (getEncodingChecks(type)) { + const encodingChecks = type.encodingChecks + if (encodingChecks) { return modifyOwnPropertyDescriptors(type, (d) => { d.encodingChecks.value = undefined + if (type === ast) { + d.checks.value = combineChecks(type.checks, encodingChecks) + } }) } return type @@ -3492,6 +3504,11 @@ function fromConst( } } +function fromAnyToConst(value: T): SchemaParser.Parser { + const succeed = Effect.succeedSome(value) + return (oinput) => oinput._tag === "None" ? Effect.succeedNone : succeed +} + function fromRefinement( ast: AST, refinement: (input: unknown) => input is T @@ -3560,15 +3577,7 @@ export const enumsToLiterals = memoize((ast: Enum): Union => { ) }) -/** @internal */ -export function toCodec(f: (ast: AST) => AST) { - function out(ast: AST): AST { - return ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, out)) : f(ast) - } - return memoize(out) -} - -const parameterFromPropertyKey = toCodec((ast) => { +const parameterFromPropertyKey = applyToSelfOrLastLinkEncoding((ast) => { switch (ast._tag) { default: return ast @@ -3580,7 +3589,7 @@ const parameterFromPropertyKey = toCodec((ast) => { }) /** @internal */ -export const parameterFromString = toCodec((ast) => { +export const parameterFromString = applyToSelfOrLastLinkEncoding((ast) => { switch (ast._tag) { default: return ast @@ -3592,7 +3601,7 @@ export const parameterFromString = toCodec((ast) => { } }) -const partFromString = toCodec((ast) => { +const partFromString = applyToSelfOrLastLinkEncoding((ast) => { switch (ast._tag) { default: return ast @@ -3970,10 +3979,7 @@ const StringTree = new Declaration( isStringTree(input) ? Effect.succeed(input) : Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))), - { - expected: "StringTree", - toCodecStringTree: () => new Link(unknown, SchemaTransformation.passthrough()) - } + { expected: "StringTree" } ) /** @internal */ diff --git a/.context/effect/packages/effect/src/SchemaParser.ts b/.context/effect/packages/effect/src/SchemaParser.ts index c8ff819a5..d0f2b3589 100644 --- a/.context/effect/packages/effect/src/SchemaParser.ts +++ b/.context/effect/packages/effect/src/SchemaParser.ts @@ -67,7 +67,7 @@ const recurDefaults = memoize((ast: SchemaAST.AST): SchemaAST.AST => { * @category constructors * @since 4.0.0 */ -export function makeEffect(schema: S) { +export function makeEffect(schema: S) { const ast = recurDefaults(SchemaAST.toType(schema.ast)) const parser = run(ast) return (input: S["~type.make.in"], options?: Schema.MakeOptions): Effect.Effect => { @@ -98,7 +98,7 @@ export function makeEffect(schema: S) { * @category constructors * @since 4.0.0 */ -export function makeOption(schema: S) { +export function makeOption(schema: S) { const parser = makeEffect(schema) return (input: S["~type.make.in"], options?: Schema.MakeOptions): Option.Option => { const exit = Effect.runSyncExit(parser(input, options)) @@ -132,7 +132,7 @@ export function makeOption(schema: S) { * @category constructors * @since 4.0.0 */ -export function make(schema: S) { +export function make(schema: S) { const parser = makeEffect(schema) return (input: S["~type.make.in"], options?: Schema.MakeOptions): S["Type"] => { const exit = Effect.runSyncExit(parser(input, options)) @@ -222,7 +222,7 @@ export function _issue(ast: SchemaAST.AST) { * @category Asserting * @since 4.0.0 */ -export function asserts(schema: S, input: I): asserts input is I & S["Type"] { +export function asserts(schema: S, input: I): asserts input is I & S["Type"] { const parser = asExit(run(SchemaAST.toType(schema.ast))) const exit = parser(input, SchemaAST.defaultParseOptions) if (Exit.isFailure(exit)) { @@ -255,7 +255,7 @@ export function asserts(schema: S, input: I): asserts i * @category decoding * @since 4.0.0 */ -export function decodeUnknownEffect( +export function decodeUnknownEffect( schema: S, options?: SchemaAST.ParseOptions ): ( @@ -290,7 +290,7 @@ export function decodeUnknownEffect( * @category decoding * @since 4.0.0 */ -export const decodeEffect: ( +export const decodeEffect: ( schema: S, options?: SchemaAST.ParseOptions ) => ( @@ -322,7 +322,7 @@ export const decodeEffect: ( * @category decoding * @since 3.10.0 */ -export function decodeUnknownPromise>( +export function decodeUnknownPromise>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Promise { @@ -354,7 +354,7 @@ export function decodeUnknownPromise>( * @category decoding * @since 3.10.0 */ -export function decodePromise>( +export function decodePromise>( schema: S, options?: SchemaAST.ParseOptions ): (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Promise { @@ -390,7 +390,7 @@ export function decodePromise>( * @category decoding * @since 4.0.0 */ -export function decodeUnknownExit>( +export function decodeUnknownExit>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit { @@ -423,14 +423,14 @@ export function decodeUnknownExit>( * @category decoding * @since 4.0.0 */ -export const decodeExit: >( +export const decodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Exit.Exit = decodeUnknownExit /** @internal */ -export function decodeUnknownOption>( +export function decodeUnknownOption>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option { @@ -438,7 +438,7 @@ export function decodeUnknownOption>( } /** @internal */ -export const decodeOption: >( +export const decodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Option.Option = decodeUnknownOption @@ -469,7 +469,7 @@ export const decodeOption: >( * @category decoding * @since 4.0.0 */ -export function decodeUnknownResult>( +export function decodeUnknownResult>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result { @@ -502,7 +502,7 @@ export function decodeUnknownResult>( * @category decoding * @since 4.0.0 */ -export const decodeResult: >( +export const decodeResult: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => Result.Result = @@ -534,7 +534,7 @@ export const decodeResult: >( * @category decoding * @since 3.10.0 */ -export function decodeUnknownSync>( +export function decodeUnknownSync>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => S["Type"] { @@ -568,7 +568,7 @@ export function decodeUnknownSync>( * @category decoding * @since 3.10.0 */ -export const decodeSync: >( +export const decodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Encoded"], options?: SchemaAST.ParseOptions) => S["Type"] = decodeUnknownSync @@ -594,7 +594,7 @@ export const decodeSync: >( * @category encoding * @since 4.0.0 */ -export function encodeUnknownEffect( +export function encodeUnknownEffect( schema: S, options?: SchemaAST.ParseOptions ): ( @@ -628,7 +628,7 @@ export function encodeUnknownEffect( * @category encoding * @since 4.0.0 */ -export const encodeEffect: ( +export const encodeEffect: ( schema: S, options?: SchemaAST.ParseOptions ) => ( @@ -660,7 +660,7 @@ export const encodeEffect: ( * @category encoding * @since 3.10.0 */ -export const encodeUnknownPromise = >( +export const encodeUnknownPromise = >( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Promise => @@ -691,7 +691,7 @@ export const encodeUnknownPromise = >( * @category encoding * @since 3.10.0 */ -export const encodePromise: >( +export const encodePromise: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Promise = encodeUnknownPromise @@ -722,7 +722,7 @@ export const encodePromise: >( * @category encoding * @since 4.0.0 */ -export function encodeUnknownExit>( +export function encodeUnknownExit>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Exit.Exit { @@ -755,14 +755,14 @@ export function encodeUnknownExit>( * @category encoding * @since 4.0.0 */ -export const encodeExit: >( +export const encodeExit: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Exit.Exit = encodeUnknownExit /** @internal */ -export function encodeUnknownOption>( +export function encodeUnknownOption>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Option.Option { @@ -770,7 +770,7 @@ export function encodeUnknownOption>( } /** @internal */ -export const encodeOption: >( +export const encodeOption: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Option.Option = encodeUnknownOption @@ -803,7 +803,7 @@ export const encodeOption: >( * @category encoding * @since 4.0.0 */ -export function encodeUnknownResult>( +export function encodeUnknownResult>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => Result.Result { @@ -836,7 +836,7 @@ export function encodeUnknownResult>( * @category encoding * @since 4.0.0 */ -export const encodeResult: >( +export const encodeResult: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => Result.Result = @@ -867,7 +867,7 @@ export const encodeResult: >( * @category encoding * @since 3.10.0 */ -export function encodeUnknownSync>( +export function encodeUnknownSync>( schema: S, options?: SchemaAST.ParseOptions ): (input: unknown, options?: SchemaAST.ParseOptions) => S["Encoded"] { @@ -901,7 +901,7 @@ export function encodeUnknownSync>( * @category encoding * @since 3.10.0 */ -export const encodeSync: >( +export const encodeSync: >( schema: S, options?: SchemaAST.ParseOptions ) => (input: S["Type"], options?: SchemaAST.ParseOptions) => S["Encoded"] = encodeUnknownSync @@ -1007,11 +1007,14 @@ export interface Parser { const recur = memoize( (ast: SchemaAST.AST): Parser => { let parser: Parser - const encodingChecks = SchemaAST.getEncodingChecks(ast) - const resolvedChecks = ast.checks ?? encodingChecks - const astOptions = (resolvedChecks ? resolvedChecks[resolvedChecks.length - 1].annotations : ast.annotations) + const checks = ast.checks + const encoding = ast.encoding + const links = encoding + const len = links?.length ?? 0 + const encodingChecks = (ast as any).encodingChecks + const astOptions = (checks ? checks[checks.length - 1].annotations : ast.annotations) ?.["parseOptions"] - if (!ast.context && !ast.encoding && !ast.checks && !encodingChecks) { + if (!ast.context && !encoding && !checks && !encodingChecks) { return (ou, options) => { parser ??= ast.getParser(recur) if (astOptions) { @@ -1022,15 +1025,15 @@ const recur = memoize( } const isStructural = SchemaAST.isArrays(ast) || SchemaAST.isObjects(ast) || (SchemaAST.isDeclaration(ast) && ast.typeParameters.length > 0) + const structuralChecks = checks && isStructural ? + checks.filter((check) => check.annotations?.[SchemaAST.STRUCTURAL_ANNOTATION_KEY]) : + undefined return (ou, options) => { if (astOptions) { options = { ...options, ...astOptions } } - const encoding = ast.encoding let srou: Effect.Effect, SchemaIssue.Issue, unknown> | undefined - if (encoding) { - const links = encoding - const len = links.length + if (links) { for (let i = len - 1; i >= 0; i--) { const link = links[i] const to = link.to @@ -1047,58 +1050,63 @@ const recur = memoize( } parser ??= ast.getParser(recur) - let sroa = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options) + const parseLocal = (localOu: Option.Option) => { + let sroa = parser(localOu, options) - if (encodingChecks && !options?.disableChecks) { - sroa = Effect.flatMapEager(sroa, (oa) => { - if (Option.isSome(ou) && Option.isSome(oa)) { - const issues: Array = [] + if (encodingChecks && !options?.disableChecks) { + sroa = Effect.flatMapEager(sroa, (oa) => { + if (Option.isSome(localOu) && Option.isSome(oa)) { + const issues: Array = [] - SchemaAST.collectIssues(encodingChecks, ou.value, issues, ast, options) + SchemaAST.collectIssues(encodingChecks, localOu.value, issues, ast, options) - if (Arr.isArrayNonEmpty(issues)) { - return Effect.fail(new SchemaIssue.Composite(ast, ou, issues)) + if (Arr.isArrayNonEmpty(issues)) { + return Effect.fail(new SchemaIssue.Composite(ast, localOu, issues)) + } } - } - return Effect.succeed(oa) - }) - } - - if (ast.checks && !options?.disableChecks) { - const checks = ast.checks - if (options?.errors === "all" && isStructural && Option.isSome(ou)) { - sroa = mapSchemaIssueEffect(sroa, (issue) => { - const issues: Array = [] - SchemaAST.collectIssues( - checks.filter((check) => check.annotations?.[SchemaAST.STRUCTURAL_ANNOTATION_KEY]), - ou.value, - issues, - ast, - options - ) - const out: SchemaIssue.Issue = Arr.isArrayNonEmpty(issues) - ? issue._tag === "Composite" && issue.ast === ast - ? new SchemaIssue.Composite(ast, issue.actual, [...issue.issues, ...issues]) - : new SchemaIssue.Composite(ast, ou, [issue, ...issues]) - : issue - return out + return Effect.succeed(oa) }) } - sroa = Effect.flatMapEager(sroa, (oa) => { - if (Option.isSome(oa)) { - const value = oa.value - const issues: Array = [] - SchemaAST.collectIssues(checks, value, issues, ast, options) + if (checks && !options?.disableChecks) { + if (options?.errors === "all" && structuralChecks && structuralChecks.length > 0 && Option.isSome(localOu)) { + sroa = mapSchemaIssueEffect(sroa, (issue) => { + const issues: Array = [] + SchemaAST.collectIssues( + structuralChecks, + localOu.value, + issues, + ast, + options + ) + const out: SchemaIssue.Issue = Arr.isArrayNonEmpty(issues) + ? issue._tag === "Composite" && issue.ast === ast + ? new SchemaIssue.Composite(ast, issue.actual, [...issue.issues, ...issues]) + : new SchemaIssue.Composite(ast, localOu, [issue, ...issues]) + : issue + return out + }) + } + sroa = Effect.flatMapEager(sroa, (oa) => { + if (Option.isSome(oa)) { + const value = oa.value + const issues: Array = [] + + SchemaAST.collectIssues(checks, value, issues, ast, options) - if (Arr.isArrayNonEmpty(issues)) { - return Effect.fail(new SchemaIssue.Composite(ast, oa, issues)) + if (Arr.isArrayNonEmpty(issues)) { + return Effect.fail(new SchemaIssue.Composite(ast, oa, issues)) + } } - } - return Effect.succeed(oa) - }) + return Effect.succeed(oa) + }) + } + + return sroa } + const sroa = srou ? Effect.flatMapEager(srou, parseLocal) : parseLocal(ou) + return sroa } } diff --git a/.context/effect/packages/effect/src/SchemaTransformation.ts b/.context/effect/packages/effect/src/SchemaTransformation.ts index e53467de9..bf87deb07 100644 --- a/.context/effect/packages/effect/src/SchemaTransformation.ts +++ b/.context/effect/packages/effect/src/SchemaTransformation.ts @@ -1312,8 +1312,8 @@ export function optionFromOptional(): Transformation, T | un * * **Details** * - * Decoding calls `new URL(s)` and fails with `InvalidValue` if the string is - * not a valid URL. Encoding returns `url.href`. + * Decoding checks `URL.canParse(s)` and fails with `InvalidValue` if the string + * is not a valid URL. Encoding returns `url.href`. * * **Example** (Converting a string to a URL) * @@ -1333,10 +1333,9 @@ export function optionFromOptional(): Transformation, T | un */ export const urlFromString: Transformation = transformOrFail({ decode: (s) => - Effect.try({ - try: () => new URL(s), - catch: () => new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid URL string: ${s}` }) - }), + URL.canParse(s) + ? Effect.succeed(new URL(s)) + : Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid URL string: ${s}` })), encode: (url) => Effect.succeed(url.href) }) diff --git a/.context/effect/packages/effect/src/String.ts b/.context/effect/packages/effect/src/String.ts index 98b119fb5..615ec1d7b 100644 --- a/.context/effect/packages/effect/src/String.ts +++ b/.context/effect/packages/effect/src/String.ts @@ -1266,10 +1266,21 @@ export const noCase: { }): string => { const delimiter = options?.delimiter ?? " " const transform = options?.transform ?? toLowerCase - const result = input - .replace(SPLIT_REGEXP[0], "$1\0$2") - .replace(SPLIT_REGEXP[1], "$1\0$2") - .replace(STRIP_REGEXP, "\0") + return normalizeCase(input, SPLIT_REGEXP, STRIP_REGEXP, delimiter, transform) +}) + +const normalizeCase = ( + input: string, + splitRegExp: ReadonlyArray, + stripRegExp: RegExp, + delimiter: string, + transform: (part: string, index: number, parts: ReadonlyArray) => string +): string => { + let result = input + for (const regexp of splitRegExp) { + result = result.replace(regexp, "$1\0$2") + } + result = result.replace(stripRegExp, "\0") let start = 0 let end = result.length // Trim the delimiter from around the output string. @@ -1282,20 +1293,21 @@ export const noCase: { // Transform each token independently. return result.slice(start, end).split("\0").map(transform).join(delimiter) -}) +} -// Support camel case ("camelCase" -> "camel Case" and "CAMELCase" -> "CAMEL Case"). -const SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g] +// Support camel case ("camelCase" -> "camel Case" and "CAMELCase" -> "CAMEL Case") +// and digit boundaries ("camel2case" -> "camel 2 case"). +const SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g, /([A-Z])([0-9])/gi, /([0-9])([A-Z])/gi] + +// Config paths preserve digit groups such as "v2" while still supporting camel case. +const CONFIG_SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g] // Remove all non-word characters. const STRIP_REGEXP = /[^A-Z0-9]+/gi -const pascalCaseTransform = (input: string, index: number): string => { +const pascalCaseTransform = (input: string): string => { const firstChar = input.charAt(0) const lowerChars = input.substring(1).toLowerCase() - if (index > 0 && firstChar >= "0" && firstChar <= "9") { - return `_${firstChar}${lowerChars}` - } return `${firstChar.toUpperCase()}${lowerChars}` } @@ -1322,7 +1334,7 @@ export const pascalCase: (self: string) => string = noCase({ const camelCaseTransform = (input: string, index: number): string => index === 0 ? input.toLowerCase() - : pascalCaseTransform(input, index) + : pascalCaseTransform(input) /** * Converts a string to camelCase. @@ -1358,6 +1370,7 @@ export const camelCase: (self: string) => string = noCase({ * @see {@link kebabCase} for lowercase hyphen-separated output * @see {@link camelCase} for lower-initial camelCase output * @see {@link pascalCase} for upper-initial PascalCase output + * @see {@link configCase} for configuration key casing that preserves numeric word groups * @see {@link noCase} for configurable delimiters and part transforms * * @category transforming @@ -1368,6 +1381,27 @@ export const constantCase: (self: string) => string = noCase({ transform: toUpperCase }) +/** + * Converts a string to CONFIG_CASE (uppercase with underscores) for + * configuration keys. + * + * **When to use** + * + * Use to normalize configuration path segments into environment-variable-like + * keys while preserving numeric word groups such as `v2`. + * + * **Details** + * + * Unlike {@link constantCase}, digit-letter boundaries are not split. For + * example, `"api-v2 xml"` becomes `"API_V2_XML"`. + * + * @see {@link constantCase} for standard uppercase underscore-separated output + * @category transforming + * @since 4.0.0 + */ +export const configCase: (self: string) => string = (self) => + normalizeCase(self, CONFIG_SPLIT_REGEXP, STRIP_REGEXP, "_", toUpperCase) + /** * Converts a string to kebab-case (lowercase with hyphens). * diff --git a/.context/effect/packages/effect/src/internal/concurrency.ts b/.context/effect/packages/effect/src/internal/concurrency.ts deleted file mode 100644 index 480cd73a4..000000000 --- a/.context/effect/packages/effect/src/internal/concurrency.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Effect } from "../Effect.ts" -import { CurrentConcurrency } from "../References.ts" -import type { Concurrency } from "../Types.ts" -import * as effect from "./effect.ts" - -/** @internal */ -export const match = ( - concurrency: Concurrency | undefined, - sequential: () => Effect, - unbounded: () => Effect, - bounded: (limit: number) => Effect -): Effect => { - switch (concurrency) { - case undefined: - return sequential() - case "unbounded": - return unbounded() - case "inherit": - return effect.flatMap(CurrentConcurrency, (concurrency) => - concurrency === "unbounded" - ? unbounded() - : concurrency > 1 - ? bounded(concurrency) - : sequential()) - default: - return concurrency > 1 ? bounded(concurrency) : sequential() - } -} - -/** @internal */ -export const matchSimple = ( - concurrency: Concurrency | undefined, - sequential: () => Effect, - concurrent: () => Effect -): Effect => { - switch (concurrency) { - case undefined: - return sequential() - case "unbounded": - return concurrent() - case "inherit": - return effect.flatMap( - CurrentConcurrency, - (concurrency) => - concurrency === "unbounded" || concurrency > 1 - ? concurrent() - : sequential() - ) - default: - return concurrency > 1 ? concurrent() : sequential() - } -} diff --git a/.context/effect/packages/effect/src/internal/core.ts b/.context/effect/packages/effect/src/internal/core.ts index f0a8c4316..ebcfbbc0b 100644 --- a/.context/effect/packages/effect/src/internal/core.ts +++ b/.context/effect/packages/effect/src/internal/core.ts @@ -87,7 +87,7 @@ export const StructuralProto = { const thatKeys = Object.keys(that) if (selfKeys.length !== thatKeys.length) return false for (let i = 0; i < selfKeys.length; i++) { - if (selfKeys[i] !== thatKeys[i] && !Equal.equals(this[selfKeys[i]], that[selfKeys[i]])) { + if (selfKeys[i] !== thatKeys[i] || !Equal.equals(this[selfKeys[i]], that[selfKeys[i]])) { return false } } diff --git a/.context/effect/packages/effect/src/internal/effect.ts b/.context/effect/packages/effect/src/internal/effect.ts index 80a8f0cf2..60debc6cb 100644 --- a/.context/effect/packages/effect/src/internal/effect.ts +++ b/.context/effect/packages/effect/src/internal/effect.ts @@ -310,7 +310,9 @@ export const causeSquash = (self: Cause.Cause): unknown => { } /** @internal */ -export const causePrettyErrors = (self: Cause.Cause): Array => { +export const causePrettyErrors = (self: Cause.Cause, options?: { + readonly includeCauseInStack?: boolean | undefined +}): Array => { const errors: Array = [] const interrupts: Array = [] if (self.reasons.length === 0) return errors @@ -326,7 +328,8 @@ export const causePrettyErrors = (self: Cause.Cause): Array => { errors.push( causePrettyError( failure._tag === "Die" ? failure.defect : failure.error as any, - failure.annotations + failure.annotations, + options ) ) } @@ -337,7 +340,7 @@ export const causePrettyErrors = (self: Cause.Cause): Array => { const error = new globalThis.Error("All fibers interrupted without error", { cause }) error.name = "InterruptError" error.stack = `${error.name}: ${error.message}` - errors.push(causePrettyError(error, interrupts[0].annotations)) + errors.push(causePrettyError(error, interrupts[0].annotations, options)) } setStackTraceLimit(prevStackLimit) @@ -347,7 +350,10 @@ export const causePrettyErrors = (self: Cause.Cause): Array => { /** @internal */ export const causePrettyError = ( original: Record | Error, - annotations?: ReadonlyMap + annotations?: ReadonlyMap, + options?: { + readonly includeCauseInStack?: boolean | undefined + } ): Error => { const kind = typeof original let error: Error @@ -364,6 +370,9 @@ export const causePrettyError = ( const stack = `${error.name}: ${error.message}` error.stack = annotations ? addStackAnnotations(stack, annotations) : stack } + if (options?.includeCauseInStack) { + error.stack = renderPrettyError(error)! + } for (const key of Object.keys(original)) { if (!(key in error)) { ;(error as any)[key] = (original as any)[key] @@ -459,10 +468,10 @@ const currentStackTrace = (frame: StackFrame): string => { /** @internal */ export const causePretty = (cause: Cause.Cause): string => - causePrettyErrors(cause).map((e) => - e.cause ? `${e.stack} {\n${renderErrorCause(e.cause as Error, " ")}\n}` : e.stack - ) - .join("\n") + causePrettyErrors(cause).map(renderPrettyError).join("\n") + +const renderPrettyError = (e: Error): string | undefined => + e.cause ? `${e.stack} {\n${renderErrorCause(e.cause as Error, " ")}\n}` : e.stack const renderErrorCause = (cause: Error, prefix: string) => { const lines = cause.stack!.split("\n") @@ -892,10 +901,18 @@ export const suspend: ( }) /** @internal */ -export const fromOption: (option: Option.Option) => Effect.Effect = Option.match({ - onNone: () => fail(new NoSuchElementError("Effect.fromOption: Option.none")), - onSome: succeed -}) +export const fromOption: | LazyArg, E = Cause.NoSuchElementError>( + arg: Arg, + ...rest: [Arg] extends [Option.Option] ? [onNone?: LazyArg] : [] +) => [Arg] extends [Option.Option] ? Effect.Effect + : [Arg] extends [LazyArg] ? (option: Option.Option) => Effect.Effect + : never = dual( + (args) => args.length >= 2 || Option.isOption(args[0]), + (option: Option.Option, onNone?: LazyArg): Effect.Effect => + Option.isNone(option) + ? fail(onNone ? onNone() : new NoSuchElementError("Effect.fromOption: Option.none")) + : succeed(option.value) + ) /** @internal */ export const fromResult: (result: Result.Result) => Effect.Effect = Result.match({ @@ -3154,23 +3171,6 @@ export const unwrapReason: { ) as any ) -/** @internal */ -export const mapErrorCause: { - ( - f: (e: Cause.Cause) => Cause.Cause - ): (self: Effect.Effect) => Effect.Effect - ( - self: Effect.Effect, - f: (e: Cause.Cause) => Cause.Cause - ): Effect.Effect -} = dual( - 2, - ( - self: Effect.Effect, - f: (e: Cause.Cause) => Cause.Cause - ): Effect.Effect => catchCause(self, (cause) => failCauseSync(() => f(cause))) -) - /** @internal */ export const mapError: { ( @@ -5360,10 +5360,10 @@ const succeedFalse = succeed(false) class Latch implements _Latch.Latch { waiters: Array<(_: Effect.Effect) => void> = [] scheduled = false - private isOpen: boolean + private _isOpen: boolean constructor(isOpen: boolean) { - this.isOpen = isOpen + this._isOpen = isOpen } private scheduleUnsafe(fiber: Fiber.Fiber) { @@ -5384,19 +5384,19 @@ class Latch implements _Latch.Latch { } open = withFiber((fiber) => { - if (this.isOpen) return succeedFalse - this.isOpen = true + if (this._isOpen) return succeedFalse + this._isOpen = true return this.scheduleUnsafe(fiber) }) - release = withFiber((fiber) => this.isOpen ? succeedFalse : this.scheduleUnsafe(fiber)) + release = withFiber((fiber) => this._isOpen ? succeedFalse : this.scheduleUnsafe(fiber)) openUnsafe() { - if (this.isOpen) return false - this.isOpen = true + if (this._isOpen) return false + this._isOpen = true this.flushWaiters() return true } await = callback((resume) => { - if (this.isOpen) { + if (this._isOpen) { return resume(void_) } this.waiters.push(resume) @@ -5408,12 +5408,15 @@ class Latch implements _Latch.Latch { }) }) closeUnsafe() { - if (!this.isOpen) return false - this.isOpen = false + if (!this._isOpen) return false + this._isOpen = false return true } close = sync(() => this.closeUnsafe()) whenOpen = (self: Effect.Effect): Effect.Effect => flatMap(this.await, () => self) + isOpen() { + return this._isOpen + } } /** @internal */ @@ -5827,11 +5830,12 @@ const performanceNowNanos = (function() { const bigint1e6 = BigInt(1_000_000) if (typeof performance === "undefined" || typeof performance.now === "undefined") { return () => BigInt(Date.now()) * bigint1e6 - } else if (typeof performance.timeOrigin === "number" && performance.timeOrigin === 0) { - return () => BigInt(Math.round(performance.now() * 1_000_000)) } - const origin = (BigInt(Date.now()) * bigint1e6) - BigInt(Math.round(performance.now() * 1_000_000)) - return () => origin + BigInt(Math.round(performance.now() * 1_000_000)) + let origin: bigint + return () => { + origin ??= (BigInt(Date.now()) * bigint1e6) - BigInt(Math.round(performance.now() * 1_000_000)) + return origin + BigInt(Math.round(performance.now() * 1_000_000)) + } })() const processOrPerformanceNow = (function() { const processHrtime = @@ -5841,7 +5845,7 @@ const processOrPerformanceNow = (function() { if (!processHrtime) { return performanceNowNanos } - const origin = performanceNowNanos() - processHrtime.bigint() + const origin = (BigInt(Date.now()) * BigInt(1e6)) - processHrtime.bigint() return () => origin + processHrtime.bigint() })() diff --git a/.context/effect/packages/effect/src/internal/matcher.ts b/.context/effect/packages/effect/src/internal/matcher.ts index dd0db24c0..a9d20eebf 100644 --- a/.context/effect/packages/effect/src/internal/matcher.ts +++ b/.context/effect/packages/effect/src/internal/matcher.ts @@ -520,11 +520,6 @@ export const instanceOf = any>( constructor: A ): SafeRefinement, never> => ((u: unknown) => u instanceof constructor) as any -/** @internal */ -export const instanceOfUnsafe: any>( - constructor: A -) => SafeRefinement, InstanceType> = instanceOf - /** @internal */ export const orElse = Ret>(f: F) => diff --git a/.context/effect/packages/effect/src/internal/request.ts b/.context/effect/packages/effect/src/internal/request.ts index 6c7d17993..7eb1662b4 100644 --- a/.context/effect/packages/effect/src/internal/request.ts +++ b/.context/effect/packages/effect/src/internal/request.ts @@ -180,7 +180,7 @@ const removeEntryUnsafe = ( if (entry.uninterruptible) return const batchMap = pendingBatches.get(resolver) if (!batchMap) return - const key = resolver.batchKey(entry.request as any) + const key = resolver.batchKey(entry) const batch = batchMap.get(key) if (!batch) return diff --git a/.context/effect/packages/effect/src/internal/schema/arbitrary.ts b/.context/effect/packages/effect/src/internal/schema/arbitrary.ts index 90c9d6772..8fe969071 100644 --- a/.context/effect/packages/effect/src/internal/schema/arbitrary.ts +++ b/.context/effect/packages/effect/src/internal/schema/arbitrary.ts @@ -22,7 +22,7 @@ const emptyRecursionStack: RecursionStack = [] type RecursionStack = ReadonlyArray type Context = Schema.Annotations.ToArbitrary.Context -type Constraint = Schema.Annotations.ToArbitrary.Constraint +type Constraint = Schema.Annotations.ToArbitrary.GenerationConstraint type OrderedConstraint = Schema.Annotations.ToArbitrary.OrderedConstraint type ArbitraryFilter = Schema.Annotations.ToArbitrary.Filter diff --git a/.context/effect/packages/effect/src/internal/schema/representation.ts b/.context/effect/packages/effect/src/internal/schema/representation.ts index 7711b8f1f..5d4ea6ace 100644 --- a/.context/effect/packages/effect/src/internal/schema/representation.ts +++ b/.context/effect/packages/effect/src/internal/schema/representation.ts @@ -127,6 +127,7 @@ export function fromASTs(asts: readonly [SchemaAST.AST, ...Array] case "Any": case "Boolean": case "Symbol": + case "ObjectKeyword": return { _tag: last._tag, ...annotations } case "String": { const contentMediaType = last.annotations?.contentMediaType @@ -159,11 +160,6 @@ export function fromASTs(asts: readonly [SchemaAST.AST, ...Array] symbol: last.symbol, ...annotations } - case "ObjectKeyword": - return { - _tag: last._tag, - ...annotations - } case "Enum": return { _tag: last._tag, diff --git a/.context/effect/packages/effect/src/internal/schema/schema.ts b/.context/effect/packages/effect/src/internal/schema/schema.ts index 47bd608e1..a8ff800ec 100644 --- a/.context/effect/packages/effect/src/internal/schema/schema.ts +++ b/.context/effect/packages/effect/src/internal/schema/schema.ts @@ -28,7 +28,7 @@ const SchemaProto = { } /** @internal */ -export function make(ast: S["ast"], options?: object): S { +export function make(ast: S["ast"], options?: object): S { const self = Object.create(SchemaProto) if (options) { Object.assign(self, options) diff --git a/.context/effect/packages/effect/src/testing/TestSchema.ts b/.context/effect/packages/effect/src/testing/TestSchema.ts index 7c1a656f7..95405b0e7 100644 --- a/.context/effect/packages/effect/src/testing/TestSchema.ts +++ b/.context/effect/packages/effect/src/testing/TestSchema.ts @@ -12,6 +12,7 @@ import * as assert from "node:assert" import type * as Context from "../Context.ts" import * as Effect from "../Effect.ts" +import { pipe } from "../Function.ts" import * as Record from "../Record.ts" import * as Result from "../Result.ts" import * as Schema from "../Schema.ts" @@ -48,7 +49,7 @@ import * as FastCheck from "../testing/FastCheck.ts" * @category testing * @since 4.0.0 */ -export class Asserts { +export class Asserts { /** * Static helpers for comparing schema AST structures. * @@ -313,7 +314,7 @@ export class Asserts { * @category testing * @since 4.0.0 */ -export class Decoding { +export class Decoding { readonly schema: S readonly decodeUnknownEffect: ( input: unknown, @@ -350,16 +351,16 @@ export class Decoding { * * @see {@link fail} for asserting decoding failures */ - async succeed>( + async succeed>( this: Decoding, input: unknown ): Promise - async succeed>( + async succeed>( this: Decoding, input: unknown, expected: S["Type"] ): Promise - async succeed>( + async succeed>( this: Decoding, input: unknown, expected?: S["Type"] @@ -393,7 +394,7 @@ export class Decoding { * * @see {@link succeed} for asserting successful decoding */ - async fail>( + async fail>( this: Decoding, input: unknown, message: string @@ -420,7 +421,7 @@ export class Decoding { implementation: Service ): Decoding>> { return new Decoding( - this.schema.pipe(Schema.middlewareDecoding(Effect.provideService(service, implementation))), + pipe(this.schema, Schema.middlewareDecoding(Effect.provideService(service, implementation))), this.options ) } @@ -453,7 +454,7 @@ export class Decoding { * @category testing * @since 4.0.0 */ -export class Encoding { +export class Encoding { readonly schema: S readonly encodeUnknownEffect: ( input: unknown, @@ -490,16 +491,16 @@ export class Encoding { * * @see {@link fail} for asserting encoding failures */ - async succeed>( + async succeed>( this: Encoding, input: unknown ): Promise - async succeed>( + async succeed>( this: Encoding, input: unknown, expected: S["Encoded"] ): Promise - async succeed>( + async succeed>( this: Encoding, input: unknown, expected?: S["Encoded"] @@ -533,7 +534,7 @@ export class Encoding { * * @see {@link succeed} for asserting successful encoding */ - async fail>( + async fail>( this: Encoding, input: unknown, message: string @@ -560,7 +561,7 @@ export class Encoding { implementation: Service ): Encoding>> { return new Encoding( - this.schema.pipe(Schema.middlewareEncoding(Effect.provideService(service, implementation))), + pipe(this.schema, Schema.middlewareEncoding(Effect.provideService(service, implementation))), this.options ) } diff --git a/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts b/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts index 51810dcc4..634f9d3d9 100644 --- a/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts +++ b/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts @@ -54,9 +54,9 @@ import * as Tool from "./Tool.ts" * @since 4.0.0 */ export function toCodecAnthropic( - schema: Schema.Codec + schema: Schema.ConstraintCodec ): { - readonly codec: Schema.Codec + readonly codec: Schema.ConstraintCodec readonly jsonSchema: JsonSchema.JsonSchema } { const to = schema.ast diff --git a/.context/effect/packages/effect/src/unstable/ai/Chat.ts b/.context/effect/packages/effect/src/unstable/ai/Chat.ts index cd43d462a..b9a491e9d 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Chat.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Chat.ts @@ -336,7 +336,7 @@ export interface Service { */ readonly generateObject: < ObjectEncoded extends Record, - ObjectSchema extends Schema.Encoder, + ObjectSchema extends Schema.Codec, Options extends NoExcessProperties, Options> >( options: Options & LanguageModel.GenerateObjectOptions, ObjectSchema> diff --git a/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts b/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts index 5b7245f4b..f07ef9571 100644 --- a/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts +++ b/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts @@ -124,7 +124,7 @@ export interface Service { */ readonly generateObject: < ObjectEncoded extends Record, - StructuredOutputSchema extends Schema.Encoder, + StructuredOutputSchema extends Schema.Codec, Options extends NoExcessProperties< GenerateObjectOptions, Options @@ -191,8 +191,8 @@ export interface Service { * @category models * @since 4.0.0 */ -export type CodecTransformer = (schema: Schema.Codec) => { - readonly codec: Schema.Codec +export type CodecTransformer = (schema: Schema.ConstraintCodec) => { + readonly codec: Schema.ConstraintCodec readonly jsonSchema: JsonSchema.JsonSchema } @@ -835,7 +835,7 @@ export const make: (params: { const generateObject = < ObjectEncoded extends Record, - StructuredOutputSchema extends Schema.Encoder, + StructuredOutputSchema extends Schema.Codec, Options extends NoExcessProperties< GenerateObjectOptions, Options @@ -1691,7 +1691,7 @@ export const generateText: { */ export const generateObject = < ObjectEncoded extends Record, - StructuredOutputSchema extends Schema.Encoder, + StructuredOutputSchema extends Schema.Codec, Options extends NoExcessProperties< GenerateObjectOptions, Options @@ -2161,7 +2161,7 @@ const resolveToolkit = , E, R>( : Effect.succeed(toolkit as unknown as Toolkit.WithHandler)) as any /** @internal */ -export const getObjectName = ( +export const getObjectName = ( objectName: string | undefined, schema: StructuredOutputSchema ): string => { @@ -2179,7 +2179,7 @@ export const getObjectName = ( } const resolveStructuredOutput = Effect.fnUntraced(function*< - StructuredOutputSchema extends Schema.Top + StructuredOutputSchema extends Schema.Constraint >(response: ReadonlyArray>, schema: StructuredOutputSchema) { const texts: Array = [] for (const part of response) { diff --git a/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts b/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts index 11aedcda5..34c3f42cb 100644 --- a/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts +++ b/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts @@ -35,7 +35,7 @@ import * as RpcMiddleware from "../rpc/RpcMiddleware.ts" * @category models * @since 4.0.0 */ -export interface optionalWithDefault +export interface optionalWithDefault extends Schema.withConstructorDefault>, Schema.optionalKey>> {} @@ -51,7 +51,7 @@ export interface optionalWithDefault( +export const optionalWithDefault = ( schema: S, defaultValue: () => Schema.optionalKey["Type"] ): optionalWithDefault => { @@ -78,7 +78,9 @@ export const optionalWithDefault = (schema: S): Schema.decodeTo, Schema.optionalKey> => +export const optional = ( + schema: S +): Schema.decodeTo, Schema.optionalKey> => Schema.optionalKey(schema).pipe( Schema.decodeTo(Schema.optional(schema), { decode: SchemaGetter.passthrough() as any, @@ -2460,7 +2462,7 @@ const ParamSchemaTypeId = "~effect/ai/McpSchema/ParamSchema" * @category parameters * @since 4.0.0 */ -export function isParam(schema: Schema.Top): schema is Param { +export function isParam(schema: Schema.Constraint): schema is Param { return Predicate.hasProperty(schema, ParamSchemaTypeId) } @@ -2475,18 +2477,11 @@ export function isParam(schema: Schema.Top): schema is Param * @category parameters * @since 4.0.0 */ -export interface Param extends - Schema.Bottom< - S["Type"], - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], +export interface Param extends + Schema.BottomLazy< S["ast"], Param, - S["~type.make.in"], - S["Iso"], S["~type.parameters"], - S["~type.make"], S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -2494,7 +2489,14 @@ export interface Param extends S["~encoded.optionality"] > { + readonly "Type": S["Type"] + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] readonly "Rebuild": Param + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": S["~type.make"] + readonly "Iso": S["Iso"] readonly [ParamSchemaTypeId]: typeof ParamSchemaTypeId readonly name: Name readonly schema: S @@ -2506,7 +2508,7 @@ export interface Param extends * @category parameters * @since 4.0.0 */ -export function param( +export function param( name: Name, schema: S ): Param { diff --git a/.context/effect/packages/effect/src/unstable/ai/McpServer.ts b/.context/effect/packages/effect/src/unstable/ai/McpServer.ts index 1e45e2b14..a3e90016f 100644 --- a/.context/effect/packages/effect/src/unstable/ai/McpServer.ts +++ b/.context/effect/packages/effect/src/unstable/ai/McpServer.ts @@ -782,7 +782,7 @@ export type ValidateCompletions = * @category models * @since 4.0.0 */ -export type ResourceCompletions> = { +export type ResourceCompletions> = { readonly [ K in Extract as Schemas[K] extends Param ? Id : `param${K}` @@ -818,7 +818,7 @@ export const registerResource: { > readonly annotations?: Context.Context | undefined }): Effect.Effect | McpServer> - >(segments: TemplateStringsArray, ...schemas: Schemas): < + >(segments: TemplateStringsArray, ...schemas: Schemas): < E, R, const Completions extends Partial> = {} @@ -979,7 +979,7 @@ export const resource: { R > }): Layer.Layer> - >(segments: TemplateStringsArray, ...schemas: Schemas): < + >(segments: TemplateStringsArray, ...schemas: Schemas): < E, R, const Completions extends Partial> = {} @@ -1056,7 +1056,7 @@ export const registerPrompt = < } ): Effect.Effect | R, McpServerClient> | McpServer> => { const args = Arr.empty() - const props: Record = options.parameters ?? {} + const props: Record = options.parameters ?? {} for (const [name, prop] of Object.entries(props)) { args.push({ name, @@ -1180,14 +1180,14 @@ export const prompt = < * @category elicitation * @since 4.0.0 */ -export const elicit: , unknown>>(options: { +export const elicit: , unknown>>(options: { readonly message: string readonly schema: S }) => Effect.Effect< S["Type"], ElicitationDeclined, McpServerClient | S["DecodingServices"] -> = Effect.fnUntraced(function*, unknown>>(options: { +> = Effect.fnUntraced(function*, unknown>>(options: { readonly message: string readonly schema: S }) { @@ -1241,7 +1241,7 @@ const makeUriMatcher = () => { return { add, find } as const } -const compileUriTemplate = (segments: TemplateStringsArray, ...schemas: ReadonlyArray) => { +const compileUriTemplate = (segments: TemplateStringsArray, ...schemas: ReadonlyArray) => { let routerPath = segments[0].replace(":", "::") let uriPath = segments[0] const params: Record = {} diff --git a/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts b/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts index 5a6645fbc..54defd5d9 100644 --- a/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts +++ b/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts @@ -51,9 +51,9 @@ import * as Tool from "./Tool.ts" * @since 4.0.0 */ export function toCodecOpenAI( - schema: Schema.Codec + schema: Schema.ConstraintCodec ): { - codec: Schema.Codec + codec: Schema.ConstraintCodec jsonSchema: JsonSchema.JsonSchema } { const to = schema.ast diff --git a/.context/effect/packages/effect/src/unstable/ai/Response.ts b/.context/effect/packages/effect/src/unstable/ai/Response.ts index 058e922e6..c14811ffa 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Response.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Response.ts @@ -1382,7 +1382,7 @@ export interface ToolCallPartMetadata extends ProviderMetadata {} * @category schemas * @since 4.0.0 */ -export const ToolCallPart: ( +export const ToolCallPart: ( name: Name, params: Params ) => Schema.Struct< @@ -1397,7 +1397,7 @@ export const ToolCallPart: > > } -> = ( +> = ( name: Name, params: Params ) => @@ -1595,7 +1595,11 @@ export interface ToolResultPartMetadata extends ProviderMetadata {} * @category schemas * @since 4.0.0 */ -export const ToolResultPart: ( +export const ToolResultPart: < + const Name extends string, + Success extends Schema.Constraint, + Failure extends Schema.Constraint +>( name: Name, success: Success, failure: Failure @@ -1633,8 +1637,8 @@ export const ToolResultPart: > = < const Name extends string, - Success extends Schema.Top, - Failure extends Schema.Top + Success extends Schema.Constraint, + Failure extends Schema.Constraint >( name: Name, success: Success, diff --git a/.context/effect/packages/effect/src/unstable/ai/Tool.ts b/.context/effect/packages/effect/src/unstable/ai/Tool.ts index 9e8d03417..5be1481ae 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Tool.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Tool.ts @@ -139,7 +139,7 @@ export interface NeedsApprovalContext { * @category models * @since 4.0.0 */ -export type NeedsApprovalFunction = ( +export type NeedsApprovalFunction = ( params: Params["Type"], context: NeedsApprovalContext ) => boolean | Effect.Effect @@ -156,7 +156,7 @@ export type NeedsApprovalFunction = ( * @category models * @since 4.0.0 */ -export type NeedsApproval = +export type NeedsApproval = | boolean | NeedsApprovalFunction @@ -196,9 +196,9 @@ export type NeedsApproval = export interface Tool< out Name extends string, out Config extends { - readonly parameters: Schema.Top - readonly success: Schema.Top - readonly failure: Schema.Top + readonly parameters: Schema.Constraint + readonly success: Schema.Constraint + readonly failure: Schema.Constraint readonly failureMode: FailureMode }, out Requirements = never @@ -290,7 +290,7 @@ export interface Tool< /** * Set the schema to use to validate the result of a tool call when successful. */ - setSuccess( + setSuccess( schema: SuccessSchema ): Tool< Name, @@ -306,7 +306,7 @@ export interface Tool< /** * Set the schema to use to validate the result of a tool call when it fails. */ - setFailure( + setFailure( schema: FailureSchema ): Tool< Name, @@ -322,7 +322,7 @@ export interface Tool< /** * Set the schema to use to validate the parameters of a tool call. */ - setParameters( + setParameters( schema: ParametersSchema ): Tool< Name, @@ -387,10 +387,10 @@ export interface ProviderDefined< out Identifier extends `${string}.${string}`, out Name extends string, out Config extends { - readonly args: Schema.Top - readonly parameters: Schema.Top - readonly success: Schema.Top - readonly failure: Schema.Top + readonly args: Schema.Constraint + readonly parameters: Schema.Constraint + readonly success: Schema.Constraint + readonly failure: Schema.Constraint readonly failureMode: FailureMode }, out RequiresHandler extends boolean = false @@ -481,9 +481,9 @@ export interface ProviderDefined< export interface Dynamic< out Name extends string, out Config extends { - readonly parameters: Schema.Top | JsonSchema.JsonSchema - readonly success: Schema.Top - readonly failure: Schema.Top + readonly parameters: Schema.Constraint | JsonSchema.JsonSchema + readonly success: Schema.Constraint + readonly failure: Schema.Constraint readonly failureMode: FailureMode }, out Requirements = never @@ -491,7 +491,7 @@ export interface Dynamic< Tool< Name, { - readonly parameters: Config["parameters"] extends Schema.Top ? Config["parameters"] : typeof Schema.Unknown + readonly parameters: Config["parameters"] extends Schema.Constraint ? Config["parameters"] : typeof Schema.Unknown readonly success: Config["success"] readonly failure: Config["failure"] readonly failureMode: Config["failureMode"] @@ -505,7 +505,7 @@ export interface Dynamic< * The raw JSON Schema for parameters. Present when `parameters` was provided * as a JSON Schema, `undefined` when an Effect Schema was used. */ - readonly jsonSchema: Config["parameters"] extends Schema.Top ? undefined : JsonSchema.JsonSchema + readonly jsonSchema: Config["parameters"] extends Schema.Constraint ? undefined : JsonSchema.JsonSchema } // ============================================================================= @@ -1036,16 +1036,16 @@ const Proto = { addDependency(this: Any) { return userDefinedProto({ ...this }) }, - setParameters(this: Any, parametersSchema: Schema.Top) { + setParameters(this: Any, parametersSchema: Schema.Constraint) { return userDefinedProto({ ...this, parametersSchema }) }, - setSuccess(this: Any, successSchema: Schema.Top) { + setSuccess(this: Any, successSchema: Schema.Constraint) { return userDefinedProto({ ...this, successSchema }) }, - setFailure(this: Any, failureSchema: Schema.Top) { + setFailure(this: Any, failureSchema: Schema.Constraint) { return userDefinedProto({ ...this, failureSchema }) }, annotate(this: Any, tag: Context.Key, value: S) { @@ -1074,9 +1074,9 @@ const DynamicProto = { const userDefinedProto = < const Name extends string, - Parameters extends Schema.Top, - Success extends Schema.Top, - Failure extends Schema.Top, + Parameters extends Schema.Constraint, + Success extends Schema.Constraint, + Failure extends Schema.Constraint, Mode extends FailureMode >(options: { readonly name: Name @@ -1104,10 +1104,10 @@ const userDefinedProto = < const providerDefinedProto = < const Identifier extends `${string}.${string}`, const Name extends string, - Args extends Schema.Top, - Parameters extends Schema.Top, - Success extends Schema.Top, - Failure extends Schema.Top, + Args extends Schema.Constraint, + Parameters extends Schema.Constraint, + Success extends Schema.Constraint, + Failure extends Schema.Constraint, RequiresHandler extends boolean, Mode extends FailureMode >(options: { @@ -1136,9 +1136,9 @@ const providerDefinedProto = < const dynamicProto = < const Name extends string, - Parameters extends Schema.Top | JsonSchema.JsonSchema, - Success extends Schema.Top, - Failure extends Schema.Top, + Parameters extends Schema.Constraint | JsonSchema.JsonSchema, + Success extends Schema.Constraint, + Failure extends Schema.Constraint, Mode extends FailureMode >(options: { readonly name: Name @@ -1194,9 +1194,9 @@ const dynamicProto = < */ export const make = < const Name extends string, - Parameters extends Schema.Top = typeof EmptyParams, - Success extends Schema.Top = typeof Schema.Void, - Failure extends Schema.Top = typeof Schema.Never, + Parameters extends Schema.Constraint = typeof EmptyParams, + Success extends Schema.Constraint = typeof Schema.Void, + Failure extends Schema.Constraint = typeof Schema.Never, Mode extends FailureMode | undefined = undefined, Dependencies extends Array | Context.Key> = [] >(name: Name, options?: { @@ -1317,9 +1317,9 @@ export const dynamic: { const Name extends string, const Options extends { readonly description?: string | undefined - readonly parameters?: Schema.Top | JsonSchema.JsonSchema | undefined - readonly success?: Schema.Top | undefined - readonly failure?: Schema.Top | undefined + readonly parameters?: Schema.Constraint | JsonSchema.JsonSchema | undefined + readonly success?: Schema.Constraint | undefined + readonly failure?: Schema.Constraint | undefined readonly failureMode?: FailureMode | undefined readonly needsApproval?: NeedsApproval | undefined } @@ -1329,12 +1329,14 @@ export const dynamic: { ): Dynamic< Name, { - readonly parameters: Options extends { readonly parameters: infer P } ? P extends Schema.Top ? P + readonly parameters: Options extends { readonly parameters: infer P } ? P extends Schema.Constraint ? P : P extends JsonSchema.JsonSchema ? P : typeof Schema.Unknown : typeof Schema.Unknown - readonly success: Options extends { readonly success: infer S extends Schema.Top } ? S : typeof Schema.Unknown - readonly failure: Options extends { readonly failure: infer F extends Schema.Top } ? F : typeof Schema.Never + readonly success: Options extends { readonly success: infer S extends Schema.Constraint } ? S + : typeof Schema.Unknown + readonly failure: Options extends { readonly failure: infer F extends Schema.Constraint } ? F + : typeof Schema.Never readonly failureMode: Options extends { readonly failureMode: infer M extends FailureMode } ? M : "error" } > @@ -1342,9 +1344,9 @@ export const dynamic: { const Name extends string, const Options extends { readonly description?: string | undefined - readonly parameters?: Schema.Top | JsonSchema.JsonSchema | undefined - readonly success?: Schema.Top | undefined - readonly failure?: Schema.Top | undefined + readonly parameters?: Schema.Constraint | JsonSchema.JsonSchema | undefined + readonly success?: Schema.Constraint | undefined + readonly failure?: Schema.Constraint | undefined readonly failureMode?: FailureMode | undefined readonly needsApproval?: NeedsApproval | undefined } @@ -1408,10 +1410,10 @@ export const dynamic: { export const providerDefined = < const Identifier extends `${string}.${string}`, const Name extends string, - Args extends Schema.Top = typeof Schema.Void, - Parameters extends Schema.Top = typeof Schema.Void, - Success extends Schema.Top = typeof Schema.Void, - Failure extends Schema.Top = typeof Schema.Never, + Args extends Schema.Constraint = typeof Schema.Void, + Parameters extends Schema.Constraint = typeof Schema.Void, + Success extends Schema.Constraint = typeof Schema.Void, + Failure extends Schema.Constraint = typeof Schema.Never, RequiresHandler extends boolean = false >(options: { /** @@ -1664,7 +1666,7 @@ export const getJsonSchema = (tool: Tool, options?: { * @category converting * @since 4.0.0 */ -export const getJsonSchemaFromSchema = (schema: S, options?: { +export const getJsonSchemaFromSchema = (schema: S, options?: { readonly transformer?: CodecTransformer }): JsonSchema.JsonSchema => { if (Predicate.isNotUndefined(options?.transformer)) { diff --git a/.context/effect/packages/effect/src/unstable/cli/Argument.ts b/.context/effect/packages/effect/src/unstable/cli/Argument.ts index ca0fe5dda..ac10422f5 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Argument.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Argument.ts @@ -17,6 +17,7 @@ import type * as Redacted from "../../Redacted.ts" import type * as Result from "../../Result.ts" import type * as Schema from "../../Schema.ts" import type * as CliError from "./CliError.ts" +import type { Environment } from "./Command.ts" import * as Param from "./Param.ts" import type * as Primitive from "./Primitive.ts" @@ -261,7 +262,7 @@ export const fileParse = ( */ export const fileSchema = ( name: string, - schema: Schema.Decoder, + schema: Schema.ConstraintDecoder, options?: Primitive.FileSchemaOptions | undefined ): Argument => Param.fileSchema(Param.argumentKind, name, schema, options) @@ -339,11 +340,11 @@ export const withDescription: { */ export const withDefault: { ( - defaultValue: B | Effect.Effect + defaultValue: B | Effect.Effect ): (self: Argument) => Argument ( self: Argument, - defaultValue: B | Effect.Effect + defaultValue: B | Effect.Effect ): Argument } = Param.withDefault @@ -471,15 +472,15 @@ export const map: { */ export const mapEffect: { ( - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): (self: Argument) => Argument ( self: Argument, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): Argument } = dual(2, ( self: Argument, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ) => Param.mapEffect(self, f)) /** @@ -589,8 +590,8 @@ export const between: { * @since 4.0.0 */ export const withSchema: { - (schema: Schema.Codec): (self: Argument) => Argument - (self: Argument, schema: Schema.Codec): Argument + (schema: Schema.Codec): (self: Argument) => Argument + (self: Argument, schema: Schema.Codec): Argument } = dual(2, (self: Argument, schema: Schema.Codec) => Param.withSchema(self, schema)) /** diff --git a/.context/effect/packages/effect/src/unstable/cli/Flag.ts b/.context/effect/packages/effect/src/unstable/cli/Flag.ts index 4ab5ee058..9ca7bfd33 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Flag.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Flag.ts @@ -18,6 +18,7 @@ import type * as Redacted from "../../Redacted.ts" import type * as Result from "../../Result.ts" import type * as Schema from "../../Schema.ts" import type * as CliError from "./CliError.ts" +import type { Environment } from "./Command.ts" import * as Param from "./Param.ts" import type * as Primitive from "./Primitive.ts" @@ -355,7 +356,7 @@ export const fileParse = ( */ export const fileSchema = ( name: string, - schema: Schema.Decoder, + schema: Schema.ConstraintDecoder, options?: Primitive.FileSchemaOptions | undefined ): Flag => Param.fileSchema(Param.flagKind, name, schema, options) @@ -579,8 +580,8 @@ export const optional = (param: Flag): Flag> => Param.opt * @since 4.0.0 */ export const withDefault: { - (defaultValue: B | Effect.Effect): (self: Flag) => Flag - (self: Flag, defaultValue: B | Effect.Effect): Flag + (defaultValue: B | Effect.Effect): (self: Flag) => Flag + (self: Flag, defaultValue: B | Effect.Effect): Flag } = Param.withDefault /** @@ -677,15 +678,15 @@ export const map: { */ export const mapEffect: { ( - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): (self: Flag) => Flag ( self: Flag, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ): Flag } = dual(2, ( self: Flag, - f: (a: A) => Effect.Effect + f: (a: A) => Effect.Effect ) => Param.mapEffect(self, f)) /** @@ -981,6 +982,6 @@ export const orElseResult: { * @since 4.0.0 */ export const withSchema: { - (schema: Schema.Codec): (self: Flag) => Flag - (self: Flag, schema: Schema.Codec): Flag + (schema: Schema.Codec): (self: Flag) => Flag + (self: Flag, schema: Schema.Codec): Flag } = dual(2, (self: Flag, schema: Schema.Codec) => Param.withSchema(self, schema)) diff --git a/.context/effect/packages/effect/src/unstable/cli/Param.ts b/.context/effect/packages/effect/src/unstable/cli/Param.ts index 734ecbc9a..1b242e10e 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Param.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Param.ts @@ -12,19 +12,16 @@ */ import * as Config from "../../Config.ts" import * as Effect from "../../Effect.ts" -import type * as FileSystem from "../../FileSystem.ts" import { dual, identity } from "../../Function.ts" import * as Option from "../../Option.ts" -import type * as Path from "../../Path.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import * as Predicate from "../../Predicate.ts" import type * as Redacted from "../../Redacted.ts" import * as Result from "../../Result.ts" import * as Schema from "../../Schema.ts" -import type * as Terminal from "../../Terminal.ts" import type { Covariant } from "../../Types.ts" -import type { ChildProcessSpawner } from "../process/ChildProcessSpawner.ts" import * as CliError from "./CliError.ts" +import type { Environment } from "./Command.ts" import * as Primitive from "./Primitive.ts" import * as Prompt from "./Prompt.ts" @@ -56,15 +53,6 @@ export interface Param extends Param.Variance */ export type ParamKind = "argument" | "flag" -/** - * Services that parameter parsing can require, such as filesystem, path, - * terminal, and child-process support. - * - * @category models - * @since 4.0.0 - */ -export type Environment = FileSystem.FileSystem | Path.Path | Terminal.Terminal | ChildProcessSpawner - /** * Defines the kind discriminator for positional argument parameters. * @@ -287,8 +275,6 @@ const Proto = { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const maybeParam = Param.string(Param.flagKind, "name") * * if (Param.isParam(maybeParam)) { @@ -309,8 +295,6 @@ export const isParam = (u: unknown): u is Param => Predicate.has * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const nameParam = Param.string(Param.flagKind, "name") * const optionalParam = Param.optional(nameParam) * @@ -377,8 +361,6 @@ export const makeSingle = (params: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Create a string flag * const nameFlag = Param.string(Param.flagKind, "name") * @@ -409,8 +391,6 @@ export const string = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Create a boolean flag * const verboseFlag = Param.boolean(Param.flagKind, "verbose") * @@ -442,8 +422,6 @@ export const boolean = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Create an integer flag * const portFlag = Param.integer(Param.flagKind, "port") * @@ -474,8 +452,6 @@ export const integer = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Create a float flag * const rateFlag = Param.float(Param.flagKind, "rate") * @@ -506,8 +482,6 @@ export const float = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Create a date flag * const startFlag = Param.date(Param.flagKind, "start-date") * @@ -540,8 +514,6 @@ export const date = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * type Animal = Dog | Cat * * interface Dog { @@ -580,8 +552,6 @@ export const choiceWithValue = < * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const logLevel = Param.choice(Param.flagKind, "log-level", [ * "debug", * "info", @@ -609,8 +579,6 @@ export const choice = < * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Basic path parameter * const outputPath = Param.path(Param.flagKind, "output") * @@ -657,8 +625,6 @@ export const path = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Basic directory parameter * const outputDir = Param.directory(Param.flagKind, "output-dir") * @@ -697,8 +663,6 @@ export const directory = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Basic file parameter * const outputFile = Param.file(Param.flagKind, "output") * @@ -733,8 +697,6 @@ export const file = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Create a password parameter * const password = Param.redacted(Param.flagKind, "password") * @@ -765,8 +727,6 @@ export const redacted = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Read a config file as string * const configContent = Param.fileText(Param.flagKind, "config") * @@ -799,8 +759,6 @@ export const fileText = (kind: Kind, name: string): Para * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Will use the extension of the file passed on the command line to determine * // the parser to use * const config = Param.fileParse(Param.flagKind, "config") @@ -833,8 +791,6 @@ export const fileParse = ( * ```ts * import { Schema } from "effect" * import { Param } from "effect/unstable/cli" - * // @internal - this module is not exported publicly - * * // Parse JSON config file * const configSchema = Schema.Struct({ * port: Schema.Number, @@ -859,7 +815,7 @@ export const fileParse = ( export const fileSchema = ( kind: Kind, name: string, - schema: Schema.Decoder, + schema: Schema.ConstraintDecoder, options?: Primitive.FileSchemaOptions | undefined ): Param => makeSingle({ @@ -886,8 +842,6 @@ export const fileSchema = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const env = Param.keyValuePair(Param.flagKind, "env") * // --env FOO=bar --env BAZ=qux will parse to { FOO: "bar", BAZ: "qux" } * @@ -927,8 +881,6 @@ export const keyValuePair = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const disabledDebugParam = Param.none(Param.flagKind) * * const makeDebugParam = (enableDebug: boolean) => @@ -968,8 +920,6 @@ const FLAG_DASH_REGEXP = /^-+/ * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const force = Param.boolean(Param.flagKind, "force").pipe( * Param.withAlias("-f"), * Param.withAlias("-F") @@ -1009,8 +959,6 @@ export const withAlias: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const verbose = Param.boolean(Param.flagKind, "verbose").pipe( * Param.withAlias("-v"), * Param.withDescription("Enable verbose output") @@ -1045,8 +993,6 @@ export const withDescription: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const experimental = Param.boolean(Param.flagKind, "experimental-foo").pipe( * Param.withHidden * ) @@ -1070,8 +1016,6 @@ export const withHidden = (self: Param): Par * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const port = Param.integer(Param.flagKind, "port").pipe( * Param.map((n) => ({ port: n, url: `http://localhost:${n}` })) * ) @@ -1119,8 +1063,6 @@ const transform = ( * import { Effect } from "effect" * import { CliError, Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const validatedEmail = Param.string(Param.flagKind, "email").pipe( * Param.mapEffect((email) => * email.includes("@") @@ -1170,8 +1112,6 @@ export const mapEffect: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const parsedJson = Param.string(Param.flagKind, "config").pipe( * Param.mapTryCatch( * (str) => JSON.parse(str), @@ -1293,8 +1233,6 @@ export const optional = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Using the pipe operator to make an option optional * const port = Param.integer(Param.flagKind, "port").pipe( * Param.withDefault(8080) @@ -1470,8 +1408,6 @@ export type VariadicParamOptions = { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Basic variadic parameter (0 to infinity) * const tags = Param.variadic(Param.string(Param.flagKind, "tag")) * @@ -1526,8 +1462,6 @@ export const variadic = ( * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Allow 1-3 file inputs * const files = Param.string(Param.flagKind, "file").pipe( * Param.between(1, 3), @@ -1576,8 +1510,6 @@ export const between: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Allow at most 3 warning suppressions * const suppressions = Param.string(Param.flagKind, "suppress").pipe( * Param.atMost(3) @@ -1613,8 +1545,6 @@ export const atMost: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * // Require at least 2 input files * const inputs = Param.string(Param.flagKind, "input").pipe( * Param.atLeast(2), @@ -1652,8 +1582,6 @@ export const atLeast: { * ```ts * import { Option } from "effect" * import { Param } from "effect/unstable/cli" - * // @internal - this module is not exported publicly - * * const positiveInt = Param.integer(Param.flagKind, "count").pipe( * Param.filterMap( * (n) => n > 0 ? Option.some(n) : Option.none(), @@ -1705,8 +1633,6 @@ export const filterMap: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const evenNumber = Param.integer(Param.flagKind, "num").pipe( * Param.filter( * (n) => n % 2 === 0, @@ -1747,8 +1673,6 @@ export const filter: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const port = Param.integer(Param.flagKind, "port").pipe( * Param.withMetavar("PORT"), * Param.filter( @@ -1782,8 +1706,6 @@ export const withMetavar: { * ```ts * import { Schema } from "effect" * import { Param } from "effect/unstable/cli" - * // @internal - this module is not exported publicly - * * const isEmail = Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) * * const Email = Schema.String.pipe( @@ -1799,8 +1721,15 @@ export const withMetavar: { * @since 4.0.0 */ export const withSchema: { - (schema: Schema.Codec): (self: Param) => Param - (self: Param, schema: Schema.Codec): Param + ( + schema: Schema.Codec + ): ( + self: Param + ) => Param + ( + self: Param, + schema: Schema.Codec + ): Param } = dual(2, ( self: Param, schema: Schema.Codec @@ -1826,8 +1755,6 @@ export const withSchema: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const config = Param.file(Param.flagKind, "config").pipe( * Param.orElse(() => Param.string(Param.flagKind, "config-url")) * ) @@ -1867,8 +1794,6 @@ export const orElse: { * ```ts * import { Param } from "effect/unstable/cli" * - * // @internal - this module is not exported publicly - * * const configSource = Param.file(Param.flagKind, "config").pipe( * Param.orElseResult(() => Param.string(Param.flagKind, "config-url")) * ) diff --git a/.context/effect/packages/effect/src/unstable/cli/Primitive.ts b/.context/effect/packages/effect/src/unstable/cli/Primitive.ts index 53c04b44d..8212c74ec 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Primitive.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Primitive.ts @@ -24,6 +24,7 @@ import * as Schema from "../../Schema.ts" import type { Formatter } from "../../SchemaIssue.ts" import type * as Struct from "../../Struct.ts" import type { Covariant } from "../../Types.ts" +import type { Environment } from "./Command.ts" const TypeId = "~effect/cli/Primitive" @@ -58,7 +59,7 @@ const TypeId = "~effect/cli/Primitive" */ export interface Primitive extends Primitive.Variance { readonly _tag: string - readonly parse: (value: string) => Effect.Effect + readonly parse: (value: string) => Effect.Effect } /** @@ -97,9 +98,7 @@ export const isBoolean = (p: Primitive): p is Primitive => p._ const makePrimitive = ( tag: string, - parse: ( - value: string - ) => Effect.Effect + parse: (value: string) => Effect.Effect ): Primitive => Object.assign(Object.create(Proto), { _tag: tag, @@ -108,7 +107,7 @@ const makePrimitive = ( const makeSchemaPrimitive = ( tag: string, - schema: Schema.Codec + schema: Schema.Codec ): Primitive => { const toCodecStringTree = Schema.toCodecStringTree(schema) const decode = Schema.decodeUnknownEffect(toCodecStringTree) @@ -614,7 +613,7 @@ export type FileSchemaOptions = Struct.Simplify< * @since 4.0.0 */ export const fileSchema = ( - schema: Schema.Decoder, + schema: Schema.ConstraintDecoder, options?: FileSchemaOptions | undefined ): Primitive => { const decode = Schema.decodeUnknownEffect(schema) diff --git a/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts b/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts index 5f0ba0d49..485312f67 100644 --- a/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts +++ b/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts @@ -51,54 +51,21 @@ export const eraseLine = `${ESC}2K` /** @internal */ export const beep = BEL -/** @internal */ -export const black = `${ESC}30m` - /** @internal */ export const red = `${ESC}31m` /** @internal */ export const green = `${ESC}32m` -/** @internal */ -export const yellow = `${ESC}33m` - -/** @internal */ -export const blue = `${ESC}34m` - -/** @internal */ -export const magenta = `${ESC}35m` - -/** @internal */ -export const cyan = `${ESC}36m` - /** @internal */ export const white = `${ESC}37m` /** @internal */ export const blackBright = `${ESC}90m` -/** @internal */ -export const redBright = `${ESC}91m` - -/** @internal */ -export const greenBright = `${ESC}92m` - -/** @internal */ -export const yellowBright = `${ESC}93m` - -/** @internal */ -export const blueBright = `${ESC}94m` - -/** @internal */ -export const magentaBright = `${ESC}95m` - /** @internal */ export const cyanBright = `${ESC}96m` -/** @internal */ -export const whiteBright = `${ESC}97m` - export const annotate = (text: string, ...styles: Array>) => { const flat = styles.flat() return `${flat.join("")}${text}${reset}` diff --git a/.context/effect/packages/effect/src/unstable/cli/internal/config.ts b/.context/effect/packages/effect/src/unstable/cli/internal/config.ts index 906fdd368..05bc00db1 100644 --- a/.context/effect/packages/effect/src/unstable/cli/internal/config.ts +++ b/.context/effect/packages/effect/src/unstable/cli/internal/config.ts @@ -46,7 +46,6 @@ * 1. Flat iteration over all params for parsing/validation * 2. Reconstruction of original nested shape for handler input */ -import * as Predicate from "../../../Predicate.ts" import * as Param from "../Param.ts" /* ========================================================================== */ @@ -105,13 +104,6 @@ export declare namespace ConfigInternal { | { readonly _tag: "Nested"; readonly tree: Tree } } -/* ========================================================================== */ -/* Guards */ -/* ========================================================================== */ - -/** @internal */ -export const isConfigInternal = (u: unknown): u is ConfigInternal => Predicate.hasProperty(u, ConfigInternalTypeId) - /* ========================================================================== */ /* Parsing */ /* ========================================================================== */ diff --git a/.context/effect/packages/effect/src/unstable/cli/internal/parser.ts b/.context/effect/packages/effect/src/unstable/cli/internal/parser.ts index 40abd776e..18fb1e3cc 100644 --- a/.context/effect/packages/effect/src/unstable/cli/internal/parser.ts +++ b/.context/effect/packages/effect/src/unstable/cli/internal/parser.ts @@ -27,7 +27,7 @@ import * as Effect from "../../../Effect.ts" import * as Option from "../../../Option.ts" import * as CliError from "../CliError.ts" -import type { Command, ParsedTokens } from "../Command.ts" +import type { Command, Environment, ParsedTokens } from "../Command.ts" import * as Param from "../Param.ts" import * as Primitive from "../Primitive.ts" import { suggest } from "./auto-suggest.ts" @@ -50,7 +50,7 @@ export const parseArgs = ( lexResult: LexResult, command: Command.Any, commandPath: ReadonlyArray = [] -): Effect.Effect => +): Effect.Effect => Effect.gen(function*() { const { tokens, trailingOperands: afterEndOfOptions } = lexResult const newCommandPath = [...commandPath, command.name] diff --git a/.context/effect/packages/effect/src/unstable/cluster/MessageStorage.ts b/.context/effect/packages/effect/src/unstable/cluster/MessageStorage.ts index f1f3b2f57..40a6ad37e 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/MessageStorage.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/MessageStorage.ts @@ -1061,7 +1061,7 @@ export const layerMemory: Layer.Layer< const EnvelopeWithReply: Schema.Struct< { - readonly envelope: Schema.Decoder + readonly envelope: Schema.ConstraintDecoder readonly lastSentReply: Schema.Option> } > = Schema.Struct({ diff --git a/.context/effect/packages/effect/src/unstable/cluster/Reply.ts b/.context/effect/packages/effect/src/unstable/cluster/Reply.ts index c2bda06ae..fea424f5f 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/Reply.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/Reply.ts @@ -234,7 +234,7 @@ export class Chunk extends Data.TaggedClass("Chunk")<{ * * @since 4.0.0 */ - static schemaFrom( + static schemaFrom( success: Success ): Schema.declareConstructor, Chunk, readonly [Success]> { // TODO: extract to a helper function @@ -336,7 +336,11 @@ export class WithExit extends Data.TaggedClass("WithExit")<{ * * @since 4.0.0 */ - static schemaFrom( + static schemaFrom< + Success extends Schema.Constraint, + Error extends Schema.Constraint, + Defect extends Schema.Constraint + >( exitSchema: Schema.Exit ): Schema.declareConstructor< WithExit, diff --git a/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts b/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts index 4190a0b42..5b5c18ae1 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts @@ -241,11 +241,11 @@ export const config: Config.Config = Config.all({ Config.withDefault(defaults.runnerShardWeight) // Config.withDescription("A number that determines how many shards this runner will be assigned relative to other runners.") ), - availableShardGroups: Config.schema(Schema.Array(Schema.String), "availableShardGroups").pipe( + availableShardGroups: Config.schema(Config.Array(Schema.String), "availableShardGroups").pipe( Config.withDefault(["default"]) // Config.withDescription("The shard groups available across all runners.") ), - assignedShardGroups: Config.schema(Schema.Array(Schema.String), "shardGroups").pipe( + assignedShardGroups: Config.schema(Config.Array(Schema.String), "shardGroups").pipe( Config.withDefault(["default"]) // Config.withDescription("The shard groups that are assigned to this runner.") ), diff --git a/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts b/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts index d5d66597e..4778114e7 100644 --- a/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts +++ b/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts @@ -261,7 +261,7 @@ export const MetricLabel = Schema.Struct({ */ export type MetricLabel = Schema.Schema.Type -const metric = (type: Type, state: State) => +const metric = (type: Type, state: State) => Schema.Struct({ id: Schema.String, type: Schema.tag(type), diff --git a/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts b/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts index 1c2acc184..3cc7fa6b9 100644 --- a/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts +++ b/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts @@ -101,7 +101,7 @@ export const encode = (): Channel.Channel< * @category constructors * @since 4.0.0 */ -export const encodeSchema = ( +export const encodeSchema = ( schema: S ) => (): Channel.Channel< @@ -181,7 +181,7 @@ export const decode = (): Channel.Channel< * @category constructors * @since 4.0.0 */ -export const decodeSchema = ( +export const decodeSchema = ( schema: S ) => (): Channel.Channel< @@ -243,7 +243,7 @@ export const duplex = ( * @since 4.0.0 */ export const duplexSchema: { - ( + ( options: { readonly inputSchema: In readonly outputSchema: Out @@ -267,7 +267,7 @@ export const duplexSchema: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > - ( + ( self: Channel.Channel< Arr.NonEmptyReadonlyArray>, OutErr, @@ -290,7 +290,7 @@ export const duplexSchema: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > -} = dual(2, ( +} = dual(2, ( self: Channel.Channel< Arr.NonEmptyReadonlyArray>, OutErr, @@ -325,7 +325,9 @@ export const duplexSchema: { * @category schemas * @since 4.0.0 */ -export interface schema extends Schema.decodeTo>> {} +export interface schema + extends Schema.decodeTo>> +{} /** * Schema for decoding MessagePack bytes into values and encoding values back to @@ -377,7 +379,7 @@ export const transformation: SchemaTransformation.Transformation< * @category schemas * @since 4.0.0 */ -export const schema = (schema: S): schema => +export const schema = (schema: S): schema => (Schema.Uint8Array as Schema.instanceOf>).pipe( Schema.decodeTo(schema, transformation) ) diff --git a/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts b/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts index 51ae787bd..87bcce197 100644 --- a/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts +++ b/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts @@ -106,7 +106,7 @@ export const encode = (): Channel.Channel< * @category constructors * @since 4.0.0 */ -export const encodeSchema = ( +export const encodeSchema = ( schema: S ) => (): Channel.Channel< @@ -130,7 +130,7 @@ export const encodeSchema = ( * @category constructors * @since 4.0.0 */ -export const encodeSchemaString = ( +export const encodeSchemaString = ( schema: S ) => (): Channel.Channel< @@ -222,7 +222,7 @@ export const decode = (options?: { * @category constructors * @since 4.0.0 */ -export const decodeSchema = ( +export const decodeSchema = ( schema: S ) => (options?: { @@ -248,7 +248,7 @@ export const decodeSchema = ( * @category constructors * @since 4.0.0 */ -export const decodeSchemaString = ( +export const decodeSchemaString = ( schema: S ) => (options?: { @@ -440,7 +440,7 @@ export const duplexString: { * @since 4.0.0 */ export const duplexSchema: { - ( + ( options: { readonly inputSchema: In readonly outputSchema: Out @@ -465,7 +465,7 @@ export const duplexSchema: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > - ( + ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, OutErr, @@ -489,7 +489,7 @@ export const duplexSchema: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > -} = dual(2, ( +} = dual(2, ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, OutErr, @@ -527,7 +527,7 @@ export const duplexSchema: { * @since 4.0.0 */ export const duplexSchemaString: { - ( + ( options: { readonly inputSchema: In readonly outputSchema: Out @@ -552,7 +552,7 @@ export const duplexSchemaString: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > - ( + ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, OutErr, @@ -576,7 +576,7 @@ export const duplexSchemaString: { InDone, R | In["EncodingServices"] | Out["DecodingServices"] > -} = dual(2, ( +} = dual(2, ( self: Channel.Channel< Arr.NonEmptyReadonlyArray, OutErr, diff --git a/.context/effect/packages/effect/src/unstable/encoding/Sse.ts b/.context/effect/packages/effect/src/unstable/encoding/Sse.ts index cc7f5dd5e..4f87fb6f4 100644 --- a/.context/effect/packages/effect/src/unstable/encoding/Sse.ts +++ b/.context/effect/packages/effect/src/unstable/encoding/Sse.ts @@ -137,7 +137,7 @@ export const decodeSchema = < * @since 4.0.0 */ export const decodeDataSchema = ( - schema: Schema.Decoder + schema: Schema.ConstraintDecoder ): Channel.Channel< NonEmptyReadonlyArray<{ readonly event: string diff --git a/.context/effect/packages/effect/src/unstable/eventlog/Event.ts b/.context/effect/packages/effect/src/unstable/eventlog/Event.ts index f40709ea8..4129f4c1a 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/Event.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/Event.ts @@ -404,9 +404,9 @@ export function make< export function make(options: { readonly tag: string readonly primaryKey: (payload: Schema.Schema.Type) => string - readonly payload?: Schema.Top | undefined - readonly success?: Schema.Top | undefined - readonly error?: Schema.Top | undefined + readonly payload?: Schema.Constraint | undefined + readonly success?: Schema.Constraint | undefined + readonly error?: Schema.Constraint | undefined }): Event { const payload = options.payload ?? Schema.Void const success = options.success ?? Schema.Void diff --git a/.context/effect/packages/effect/src/unstable/http/HttpBody.ts b/.context/effect/packages/effect/src/unstable/http/HttpBody.ts index 49ff779cb..c43a60d4b 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpBody.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpBody.ts @@ -317,7 +317,7 @@ export const json = (body: unknown, contentType?: string): Effect.Effect( +export const jsonSchema = ( schema: S, options?: ParseOptions | undefined ) => { @@ -335,8 +335,8 @@ export const jsonSchema = ( * @category constructors * @since 4.0.0 */ -export const urlParams = (urlParams: UrlParams.UrlParams, contentType?: string): Uint8Array => - text(UrlParams.toString(urlParams), contentType ?? "application/x-www-form-urlencoded") +export const urlParams = (urlParams: UrlParams.Input, contentType?: string): Uint8Array => + text(UrlParams.toString(UrlParams.fromInput(urlParams)), contentType ?? "application/x-www-form-urlencoded") /** * HTTP body variant backed by Web `FormData`. diff --git a/.context/effect/packages/effect/src/unstable/http/HttpClient.ts b/.context/effect/packages/effect/src/unstable/http/HttpClient.ts index 9740f881b..102237da2 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpClient.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpClient.ts @@ -40,7 +40,7 @@ import * as HttpClientResponse from "./HttpClientResponse.ts" import * as HttpIncomingMessage from "./HttpIncomingMessage.ts" import * as HttpMethod from "./HttpMethod.ts" import * as TraceContext from "./HttpTraceContext.ts" -import * as UrlParams from "./UrlParams.ts" +import * as Url from "./Url.ts" const TypeId = "~effect/http/HttpClient" @@ -637,7 +637,7 @@ export const make = ( Effect.withFiber((fiber) => { const scopedController = scopedRequests.get(request) const controller = scopedController ?? new AbortController() - const urlResult = UrlParams.makeUrl(request.url, request.urlParams, Option.getOrUndefined(request.hash)) + const urlResult = Url.make(request.url, request.urlParams, Option.getOrUndefined(request.hash)) if (Result.isFailure(urlResult)) { return Effect.fail( new Error.HttpClientError({ @@ -1024,6 +1024,10 @@ export declare namespace WithRateLimiter { * Disable automatic limits updates from response headers. */ readonly disableResponseInspection?: boolean | undefined + /** + * Disable adaptive learning from `Retry-After` responses. + */ + readonly disableAdaptiveLearning?: boolean | undefined } } @@ -1066,6 +1070,7 @@ export const withRateLimiter: { const resolveTokens: (request: HttpClientRequest.HttpClientRequest) => number = typeof tokensOption === "function" ? tokensOption : constant(tokensOption ?? 1) + const adaptiveLearningEnabled = !options.disableAdaptiveLearning const getState = (key: string): RateLimiterState => { const current = states.get(key) @@ -1096,13 +1101,39 @@ export const withRateLimiter: { const key = resolveKey(request) const tokens = Math.max(resolveTokens(request), 1) const current = getState(key) - function retry(response: HttpClientResponse.HttpClientResponse) { + function retry(retryAfter: Duration.Duration | undefined) { if (options.disableResponseInspection) return loop(effect, request) - const retryAfter = parseRetryAfter(clock, getHeader(response.headers, "retry-after")) return retryAfter ? Effect.flatMap(Effect.sleep(retryAfter), () => loop(effect, request)) : loop(effect, request) } + const inspectResponse = ( + response: HttpClientResponse.HttpClientResponse, + adaptive: RateLimiter.AdaptiveConsumeResult | undefined + ) => { + onResponse?.(clock, key, response.headers, tokens) + if (options.disableResponseInspection || response.status !== 429) { + return Effect.succeed(undefined) + } + const retryAfter = parseRetryAfter(clock, getHeader(response.headers, "retry-after")) + if (retryAfter === undefined) { + return Effect.succeed(undefined) + } + const delay = parseRateLimitWindow(clock, response.headers) ?? retryAfter + if (adaptive === undefined) { + return Effect.succeed(delay) + } + return Effect.as( + options.limiter.adaptiveFeedback({ + key, + epoch: adaptive.epoch, + tokens, + status: response.status, + retryAfter: delay + }), + delay + ) + } return Effect.flatMap( options.limiter.consume({ algorithm: options.algorithm, @@ -1113,21 +1144,52 @@ export const withRateLimiter: { tokens }), ({ delay }) => { - const run = Effect.matchEffect(effect, { - onSuccess(response) { - onResponse?.(clock, key, response.headers, tokens) - if (response.status !== 429) return Effect.succeed(response) - return retry(response) - }, - onFailure(error) { - if (isTooManyRequestsHttpClientError(error)) { - onResponse?.(clock, key, error.reason.response.headers, tokens) - return retry(error.reason.response) - } - return Effect.fail(error) + const runAdaptive = (): Effect.Effect< + HttpClientResponse.HttpClientResponse, + E | RateLimiter.RateLimiterError, + R + > => { + const runRequest = (adaptive: RateLimiter.AdaptiveConsumeResult | undefined) => { + const request = Effect.matchEffect(effect, { + onSuccess(response) { + return Effect.flatMap(inspectResponse(response, adaptive), (retryAfter) => { + if (response.status !== 429) return Effect.succeed(response) + return retry(retryAfter) + }) + }, + onFailure(error) { + if (isTooManyRequestsHttpClientError(error)) { + return Effect.flatMap( + inspectResponse(error.reason.response, adaptive), + (retryAfter) => retry(retryAfter) + ) + } + return Effect.fail(error) + } + }) + return adaptive === undefined || Duration.isZero(adaptive.delay) + ? request + : Effect.delay(request, adaptive.delay) } - }) - return Duration.isZero(delay) ? run : Effect.delay(run, delay) + if (!adaptiveLearningEnabled) { + return runRequest(undefined) + } + return Effect.flatMap( + options.limiter.adaptiveConsume({ + key, + tokens, + fallbackLimit: current.limit, + fallbackWindow: current.window + }), + (adaptive) => { + if (!Duration.isZero(adaptive.delay) && adaptive.phase === "cooldown") { + return Effect.flatMap(Effect.sleep(adaptive.delay), runAdaptive) + } + return runRequest(adaptive) + } + ) + } + return Duration.isZero(delay) ? runAdaptive() : Effect.flatMap(Effect.sleep(delay), runAdaptive) } ) }) @@ -1180,13 +1242,6 @@ const parseRateLimitWindow = ( clock: Clock, headers: Headers.Headers ): Duration.Duration | undefined => { - const retryAfter = parseRetryAfter( - clock, - getHeader(headers, "retry-after") - ) - if (retryAfter !== undefined) { - return retryAfter - } const resetAfter = parseResetAfter(getHeader(headers, "ratelimit-reset-after", "x-ratelimit-reset-after")) if (resetAfter !== undefined) { return resetAfter diff --git a/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts b/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts index 29abc48d7..feabe9b1e 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts @@ -28,6 +28,7 @@ import * as Stream from "../../Stream.ts" import * as Headers from "./Headers.ts" import * as HttpBody from "./HttpBody.ts" import { hasBody, type HttpMethod } from "./HttpMethod.ts" +import * as Url from "./Url.ts" import * as UrlParams from "./UrlParams.ts" const TypeId = "~effect/http/HttpClientRequest" @@ -116,7 +117,7 @@ const Proto = { export function makeWith( method: HttpMethod, url: string, - urlParams: UrlParams.UrlParams, + urlParams: UrlParams.Input, hash: Option.Option, headers: Headers.Headers, body: HttpBody.HttpBody @@ -707,7 +708,7 @@ export const bodyJsonUnsafe: { * @category combinators * @since 4.0.0 */ -export const schemaBodyJson = ( +export const schemaBodyJson = ( schema: S, options?: ParseOptions | undefined ): { @@ -853,7 +854,7 @@ export const bodyFile: { * @since 4.0.0 */ export function toUrl(self: HttpClientRequest): Option.Option { - const r = UrlParams.makeUrl(self.url, self.urlParams, Option.getOrUndefined(self.hash)) + const r = Url.make(self.url, self.urlParams, Option.getOrUndefined(self.hash)) if (Result.isSuccess(r)) { return Option.some(r.success) } @@ -903,8 +904,8 @@ const parseContentLength = (contentLength: string | null): number | undefined => export const toWebResult = (self: HttpClientRequest, options?: { readonly signal?: AbortSignal | undefined readonly context?: Context.Context | undefined -}): Result.Result => { - const url = UrlParams.makeUrl(self.url, self.urlParams, Option.getOrUndefined(self.hash)) +}): Result.Result => { + const url = Url.make(self.url, self.urlParams, Option.getOrUndefined(self.hash)) if (Result.isFailure(url)) { return Result.fail(url.failure) } @@ -944,7 +945,7 @@ export const toWebResult = (self: HttpClientRequest, options?: { } return Result.try({ try: () => new Request(url.success, requestInit), - catch: (cause) => new UrlParams.UrlParamsError({ cause }) + catch: (cause) => new Url.UrlError({ cause }) }) } @@ -952,14 +953,14 @@ const isReadableStream = (u: unknown): u is ReadableStream => typeof ReadableStream !== "undefined" && u instanceof ReadableStream /** - * Converts an `HttpClientRequest` to a Web `Request`, failing with `UrlParamsError` when the request URL is invalid. + * Converts an `HttpClientRequest` to a Web `Request`, failing with `UrlError` when the request URL is invalid. * * @category converting * @since 4.0.0 */ export const toWeb = (self: HttpClientRequest, options?: { readonly signal?: AbortSignal | undefined -}): Effect.Effect => +}): Effect.Effect => Effect.contextWith((context) => Effect.fromResult(toWebResult(self, { context: context, diff --git a/.context/effect/packages/effect/src/unstable/http/HttpIncomingMessage.ts b/.context/effect/packages/effect/src/unstable/http/HttpIncomingMessage.ts index 141802988..efbb73190 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpIncomingMessage.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpIncomingMessage.ts @@ -61,7 +61,7 @@ export interface HttpIncomingMessage extends Inspectable.Inspectabl * @category schemas * @since 4.0.0 */ -export const schemaBodyJson = (schema: S, options?: ParseOptions | undefined) => { +export const schemaBodyJson = (schema: S, options?: ParseOptions | undefined) => { const decode = Schema.decodeEffect(Schema.toCodecJson(schema)) return ( self: HttpIncomingMessage diff --git a/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts b/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts index 0d723ccf4..4d09d91a2 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts @@ -13,6 +13,7 @@ import { Clock } from "../../Clock.ts" import * as Context from "../../Context.ts" import * as Effect from "../../Effect.ts" +import * as Exit from "../../Exit.ts" import { constant, constFalse } from "../../Function.ts" import * as internalEffect from "../../internal/effect.ts" import * as Layer from "../../Layer.ts" @@ -22,7 +23,7 @@ import type { ReadonlyRecord } from "../../Record.ts" import { TracerEnabled } from "../../References.ts" import { ParentSpan } from "../../Tracer.ts" import * as Headers from "./Headers.ts" -import { causeResponseStripped, exitResponse } from "./HttpServerError.ts" +import { causeResponseStripped } from "./HttpServerError.ts" import { HttpServerRequest } from "./HttpServerRequest.ts" import * as Request from "./HttpServerRequest.ts" import * as Response from "./HttpServerResponse.ts" @@ -218,13 +219,21 @@ export const tracer: ( if (Option.isSome(request.remoteAddress)) { span.attribute("client.address", request.remoteAddress.value) } - const response = exitResponse(exit) + let response: HttpServerResponse + let spanExit = exit + if (Exit.isFailure(exit)) { + const [failureResponse, cause] = causeResponseStripped(exit.cause) + response = failureResponse + spanExit = Option.isSome(cause) ? Exit.failCause(cause.value) : Exit.succeed(response) + } else { + response = exit.value + } span.attribute("http.response.status_code", response.status) const responseHeaders = Headers.redact(response.headers, redactedHeaderNames) for (const name in responseHeaders) { span.attribute(`http.response.header.${name}`, String(responseHeaders[name])) } - span.end(endTime, exit) + span.end(endTime, spanExit) }, 0) return undefined }, true) diff --git a/.context/effect/packages/effect/src/unstable/http/Url.ts b/.context/effect/packages/effect/src/unstable/http/Url.ts index 284d1a3a9..8c73a9f07 100644 --- a/.context/effect/packages/effect/src/unstable/http/Url.ts +++ b/.context/effect/packages/effect/src/unstable/http/Url.ts @@ -9,11 +9,66 @@ * @since 4.0.0 */ import * as Cause from "../../Cause.ts" +import * as Data from "../../Data.ts" import { dual } from "../../Function.ts" import * as Redacted from "../../Redacted.ts" import * as Result from "../../Result.ts" import * as UrlParams from "./UrlParams.ts" +/** + * Error returned when constructing a `URL` fails. + * + * @category errors + * @since 4.0.0 + */ +export class UrlError extends Data.TaggedError("UrlError")<{ + readonly cause: unknown +}> {} + +/** + * Creates a `URL` safely by appending `UrlParams` and an optional hash to a URL string. + * + * **Details** + * + * Returns a `Result` that fails with `UrlError` if the URL cannot be constructed. + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + url: string, + params: UrlParams.UrlParams, + hash: string | undefined +): Result.Result => + Result.try({ + try: () => { + const urlInstance = new URL(url, baseUrl()) + for (let i = 0; i < params.params.length; i++) { + const [key, value] = params.params[i] + if (value !== undefined) { + urlInstance.searchParams.append(key, value) + } + } + if (hash !== undefined) { + urlInstance.hash = hash + } + return urlInstance + }, + catch: (cause) => new UrlError({ cause }) + }) + +const baseUrl = (): string | undefined => { + if ( + "location" in globalThis && + globalThis.location !== undefined && + globalThis.location.origin !== undefined && + globalThis.location.pathname !== undefined + ) { + return location.origin + location.pathname + } + return undefined +} + /** * Parses a URL string safely into a `URL` object, returning a `Result` type for * error handling. @@ -259,11 +314,11 @@ export const setUsername: { * @since 4.0.0 */ export const setUrlParams: { - (urlParams: UrlParams.UrlParams): (url: URL) => URL - (url: URL, urlParams: UrlParams.UrlParams): URL -} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => + (urlParams: UrlParams.Input): (url: URL) => URL + (url: URL, urlParams: UrlParams.Input): URL +} = dual(2, (url: URL, urlParams: UrlParams.Input) => mutate(url, (url) => { - url.search = UrlParams.toString(searchParams) + url.search = UrlParams.toString(UrlParams.fromInput(urlParams)) })) /** @@ -321,9 +376,9 @@ export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput( * @since 4.0.0 */ export const modifyUrlParams: { - (f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL - (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL -} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) => + (f: (urlParams: UrlParams.UrlParams) => UrlParams.Input): (url: URL) => URL + (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.Input): URL +} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.Input) => mutate(url, (url) => { const params = f(UrlParams.fromInput(url.searchParams)) url.search = UrlParams.toString(params) diff --git a/.context/effect/packages/effect/src/unstable/http/UrlParams.ts b/.context/effect/packages/effect/src/unstable/http/UrlParams.ts index c95bf8b80..c2483a24e 100644 --- a/.context/effect/packages/effect/src/unstable/http/UrlParams.ts +++ b/.context/effect/packages/effect/src/unstable/http/UrlParams.ts @@ -9,7 +9,6 @@ * @since 4.0.0 */ import * as Arr from "../../Array.ts" -import * as Data from "../../Data.ts" import * as Effect from "../../Effect.ts" import * as Equal from "../../Equal.ts" import * as Equ from "../../Equivalence.ts" @@ -21,7 +20,6 @@ import * as Option from "../../Option.ts" import type { Pipeable } from "../../Pipeable.ts" import { hasProperty } from "../../Predicate.ts" import type { ReadonlyRecord } from "../../Record.ts" -import * as Result from "../../Result.ts" import * as Schema from "../../Schema.ts" import * as SchemaIssue from "../../SchemaIssue.ts" import * as SchemaTransformation from "../../SchemaTransformation.ts" @@ -65,6 +63,7 @@ export const isUrlParams = (u: unknown): u is UrlParams => hasProperty(u, TypeId * @since 4.0.0 */ export type Input = + | UrlParams | CoercibleRecordInput | Iterable | URLSearchParams @@ -157,6 +156,9 @@ export const make = (params: ReadonlyArray): UrlParam * @since 4.0.0 */ export const fromInput = (input: Input): UrlParams => { + if (isUrlParams(input)) { + return input + } const parsed = fromInputNested(input) const out: Array<[string, string]> = [] for (let i = 0; i < parsed.length; i++) { @@ -446,68 +448,13 @@ export const remove: { (self: UrlParams, key: string): UrlParams } = dual(2, (self: UrlParams, key: string): UrlParams => transform(self, Arr.filter(([k]) => k !== key))) -/** - * Error returned when constructing a `URL` from `UrlParams` fails. - * - * @category errors - * @since 4.0.0 - */ -export class UrlParamsError extends Data.TaggedError("UrlParamsError")<{ - cause: unknown -}> {} - -/** - * Creates a `URL` safely by appending `UrlParams` and an optional hash to a URL string. - * - * **Details** - * - * Returns a `Result` that fails with `UrlParamsError` if the URL cannot be - * constructed. - * - * @category converting - * @since 4.0.0 - */ -export const makeUrl = ( - url: string, - params: UrlParams, - hash: string | undefined -): Result.Result => { - try { - const urlInstance = new URL(url, baseUrl()) - for (let i = 0; i < params.params.length; i++) { - const [key, value] = params.params[i] - if (value !== undefined) { - urlInstance.searchParams.append(key, value) - } - } - if (hash !== undefined) { - urlInstance.hash = hash - } - return Result.succeed(urlInstance) - } catch (e) { - return Result.fail(new UrlParamsError({ cause: e })) - } -} - /** * Serializes `UrlParams` to a URL query string without a leading question mark. * * @category converting * @since 4.0.0 */ -export const toString = (self: UrlParams): string => new URLSearchParams(self.params as any).toString() - -const baseUrl = (): string | undefined => { - if ( - "location" in globalThis && - globalThis.location !== undefined && - globalThis.location.origin !== undefined && - globalThis.location.pathname !== undefined - ) { - return location.origin + location.pathname - } - return undefined -} +export const toString = (input: Input): string => new URLSearchParams(fromInput(input).params as any).toString() /** * Builds a `Record` containing all the key-value pairs in the given `UrlParams` diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts index 4ded3973d..0cbd0404c 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts @@ -312,5 +312,5 @@ const extractResponseContent = ( */ export class AdditionalSchemas extends Context.Service< AdditionalSchemas, - ReadonlyArray + ReadonlyArray >()("effect/httpapi/HttpApi/AdditionalSchemas") {} diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts index b742a9eae..43bdc3a7f 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts @@ -21,6 +21,7 @@ import * as Layer from "../../Layer.ts" import * as Option from "../../Option.ts" import type { Path } from "../../Path.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" +import { hasProperty } from "../../Predicate.ts" import * as Redacted from "../../Redacted.ts" import * as Result from "../../Result.ts" import * as Schema from "../../Schema.ts" @@ -603,13 +604,20 @@ function decodePayload( ) } case "Json": - const json = Effect.orDie(Effect.flatMap(httpRequest.text, (text) => { + return Effect.flatMap(Effect.orDie(httpRequest.text), (text) => { if (text === "") { - return existing.nullOnEmpty ? Effect.succeed(null) : Effect.undefined + return decode(existing.nullOnEmpty ? null : undefined) } - return Effect.succeed(JSON.parse(text)) - })) - return Effect.flatMap(json, decode) + try { + return decode(JSON.parse(text)) + } catch (cause) { + return Effect.fail( + new Schema.SchemaError( + new SchemaIssue.InvalidValue(Option.some(text), { message: `Invalid JSON: ${cause}` }) + ) + ) + } + }) case "Text": return Effect.flatMap(Effect.orDie(httpRequest.text), decode) case "FormUrlEncoded": { @@ -766,6 +774,7 @@ const makeSecurityMiddleware = ( readonly group: HttpApiGroup.AnyWithProps readonly endpoint: HttpApiEndpoint.AnyWithProps }) { + handler = Effect.mapError(handler, (error) => new HandlerError(error)) let lastResult: Result.Result | undefined for (let i = 0; i < entries.length; i++) { const { decode, middleware } = entries[i] @@ -776,6 +785,9 @@ const makeSecurityMiddleware = ( group: options.group }))) if (Result.isFailure(result)) { + if (isHandlerError(result.failure)) { + return yield* Effect.fail(result.failure.error) + } lastResult = result continue } @@ -788,6 +800,16 @@ const makeSecurityMiddleware = ( return middleware } +const HandlerErrorTypeId = "~effect/httpapi/HttpApiBuilder/HandlerError" as const +class HandlerError { + readonly [HandlerErrorTypeId] = HandlerErrorTypeId + readonly error: unknown + constructor(error: unknown) { + this.error = error + } +} +const isHandlerError = (value: unknown): value is HandlerError => hasProperty(value, HandlerErrorTypeId) + const $HttpServerResponse = Schema.declare(Response.isHttpServerResponse) type StreamEncoder = (response: unknown, context: Context.Context) => @@ -807,7 +829,9 @@ function makeStreamEncoder(endpoint: HttpApiEndpoint.AnyWithProps): StreamEncode if (HttpApiSchema.isStreamUint8Array(streamSchema)) { return (response, context) => { if (!Stream.isStream(response)) { - return hasBuffered ? undefined : expectedStreamResponse(response) + return hasBuffered ? undefined : new Schema.SchemaError( + new SchemaIssue.InvalidValue(Option.some(response), { message: "Expected a streaming response" }) + ) } return Effect.succeed(Response.stream( @@ -824,7 +848,9 @@ function makeStreamEncoder(endpoint: HttpApiEndpoint.AnyWithProps): StreamEncode return (response, context) => { if (!Stream.isStream(response)) { - return hasBuffered ? undefined : expectedStreamResponse(response) + return hasBuffered ? undefined : new Schema.SchemaError( + new SchemaIssue.InvalidValue(Option.some(response), { message: "Expected a streaming response" }) + ) } return Effect.succeed(Response.stream( @@ -852,16 +878,6 @@ function hasBufferedSuccess(endpoint: HttpApiEndpoint.AnyWithProps): boolean { return endpoint.success.size === 0 } -function expectedStreamResponse(response: unknown) { - return Effect.fail( - makeSchemaError( - new SchemaIssue.InvalidValue(Option.some(response), { - message: "Expected a streaming response" - }) - ) - ) -} - interface SseStreamEncoder { readonly sseMode: HttpApiSchema.StreamSseMode readonly encodeEvents: ( @@ -870,7 +886,7 @@ interface SseStreamEncoder { readonly encodeCause: (input: unknown) => Effect.Effect } -function makeSseEncoder( +function makeSseEncoder( streamSchema: HttpApiSchema.StreamSse ): SseStreamEncoder { const CauseSchema = Schema.toCodecJson(Schema.Cause(streamSchema.error, Schema.Defect())) @@ -922,19 +938,19 @@ function renderSseEvent(event: Sse.EventEncoded) { }) } -function makeSchemaError(issue: SchemaIssue.Issue): Schema.SchemaError { - return new Schema.SchemaError(issue) -} - const toResponseSuccessSchema = toResponseSchema(HttpApiSchema.getStatusSuccess) const toResponseErrorSchema = toResponseSchema(HttpApiSchema.getStatusError) -function makeSuccessSchema(endpoint: HttpApiEndpoint.AnyWithProps): Schema.Encoder { +function makeSuccessSchema( + endpoint: HttpApiEndpoint.AnyWithProps +): Schema.ConstraintEncoder { const schemas = HttpApiEndpoint.getSuccessSchemas(endpoint).map(toResponseSuccessSchema) return schemas.length === 1 ? schemas[0] : Schema.Union(schemas) } -function makeErrorSchema(endpoint: HttpApiEndpoint.AnyWithProps): Schema.Encoder { +function makeErrorSchema( + endpoint: HttpApiEndpoint.AnyWithProps +): Schema.ConstraintEncoder { const schemas = HttpApiEndpoint.getErrorSchemas(endpoint).map(toResponseErrorSchema) if (schemas.length === 0) return Schema.Never return schemas.length === 1 ? schemas[0] : Schema.Union(schemas) @@ -943,7 +959,7 @@ function makeErrorSchema(endpoint: HttpApiEndpoint.AnyWithProps): Schema.Encoder function toResponseSchema(getStatus: (ast: SchemaAST.AST) => number) { const cache = new WeakMap() - return (schema: Schema.Top): Schema.Encoder => { + return (schema: Schema.Constraint): Schema.ConstraintEncoder => { const cached = cache.get(schema.ast) if (cached !== undefined) { return cached as any @@ -958,7 +974,7 @@ function toResponseSchema(getStatus: (ast: SchemaAST.AST) => number) { function getResponseTransformation( getStatus: (ast: SchemaAST.AST) => number, - schema: Schema.Top + schema: Schema.Constraint ): SchemaTransformation.Transformation { const ast = schema.ast const encode = getResponseEncode( diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts index 7e0fb9c73..d1de48b83 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts @@ -80,7 +80,7 @@ type SuccessType = S extends HttpApiSchema.StreamSse< never > : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream - : S extends Schema.Top ? S["Type"] + : S extends Schema.Constraint ? S["Type"] : never type SuccessDecodingServices = S extends HttpApiSchema.StreamSse< @@ -91,7 +91,7 @@ type SuccessDecodingServices = S extends HttpApiSchema.StreamSse< | _Events["DecodingServices"] | _Error["DecodingServices"] : S extends HttpApiSchema.StreamUint8Array ? never - : S extends Schema.Top ? S["DecodingServices"] + : S extends Schema.Constraint ? S["DecodingServices"] : never /** @@ -636,10 +636,10 @@ export const urlBuilder = (api: Api, options?: { const makeUrl = compilePath(endpoint.path) const encodeParams = endpoint.params === undefined ? undefined - : Schema.encodeSync(endpoint.params as Schema.Encoder) + : Schema.encodeSync(endpoint.params as Schema.Codec) const encodeQuery = endpoint.query === undefined ? undefined - : Schema.encodeSync(endpoint.query as Schema.Encoder) + : Schema.encodeSync(endpoint.query as Schema.Codec) const endpointBuilder = (request?: { readonly params?: unknown @@ -687,7 +687,7 @@ const compilePath = (path: string) => { } } -function schemasToResponse(schemas: readonly [Schema.Top, ...Array]) { +function schemasToResponse(schemas: readonly [Schema.Constraint, ...Array]) { const codec = toCodecArrayBuffer(schemas) const decode = Schema.decodeEffect(codec) return (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap(response.arrayBuffer, decode) @@ -795,7 +795,7 @@ function streamToResponse(streamSchema: HttpApiSchema.StreamSchema) { function decodeSseStream( stream: Stream.Stream, - declaration: HttpApiSchema.StreamSse + declaration: HttpApiSchema.StreamSse ): Stream.Stream { const Event = Schema.Union([ declaration.events, @@ -893,10 +893,10 @@ const UnknownFromArrayBuffer = StringFromArrayBuffer.pipe(Schema.decodeTo( ]) )) -function toCodecArrayBuffer(schemas: readonly [Schema.Top, ...Array]): Schema.Top { +function toCodecArrayBuffer(schemas: readonly [Schema.Constraint, ...Array]): Schema.Top { return Schema.Union(schemas.map(onSchema)) - function onSchema(schema: Schema.Top) { + function onSchema(schema: Schema.Constraint) { const encoding = HttpApiSchema.getResponseEncoding(schema.ast) switch (encoding._tag) { case "Json": { @@ -938,7 +938,7 @@ const statusOrElse = (response: HttpClientResponse.HttpClientResponse) => const $HttpBody = Schema.declare(HttpBody.isHttpBody) function getEncodePayloadSchema( - schemas: readonly [Schema.Top, ...Array], + schemas: readonly [Schema.Constraint, ...Array], method: HttpMethod.HttpMethod ): Schema.Top { return Schema.Union(schemas.map((s) => getEncodePayloadSchemaFromBody(s, method))) @@ -947,7 +947,7 @@ function getEncodePayloadSchema( const bodyFromPayloadCache = new WeakMap() function getEncodePayloadSchemaFromBody( - schema: Schema.Top, + schema: Schema.Constraint, method: HttpMethod.HttpMethod ): Schema.Top { const ast = schema.ast diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts index b2efbb90d..7b148ee03 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts @@ -60,7 +60,7 @@ type SuccessType = S extends HttpApiSchema.StreamSse< infer _Value > ? Stream.Stream<_Value, _Error["Type"], never> : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream - : S extends Schema.Top ? S["Type"] + : S extends Schema.Constraint ? S["Type"] : never type SuccessEncodingServices = S extends HttpApiSchema.StreamSse< @@ -69,7 +69,7 @@ type SuccessEncodingServices = S extends HttpApiSchema.StreamSse< infer _Value > ? _Events["EncodingServices"] | _Error["EncodingServices"] : S extends HttpApiSchema.StreamUint8Array ? never - : S extends Schema.Top ? S["EncodingServices"] + : S extends Schema.Constraint ? S["EncodingServices"] : never type SuccessDecodingServices = S extends HttpApiSchema.StreamSse< @@ -78,10 +78,10 @@ type SuccessDecodingServices = S extends HttpApiSchema.StreamSse< infer _Value > ? _Events["DecodingServices"] | _Error["DecodingServices"] : S extends HttpApiSchema.StreamUint8Array ? never - : S extends Schema.Top ? S["DecodingServices"] + : S extends Schema.Constraint ? S["DecodingServices"] : never -type ExtractSuccessOrArray = S extends ReadonlyArray ? S[number] : S +type ExtractSuccessOrArray = S extends ReadonlyArray ? S[number] : S type ExtractBufferedSuccess = Exclude< Extract, Schema.Top>, @@ -262,8 +262,18 @@ export interface Any extends Pipeable { * @category models * @since 4.0.0 */ -export interface AnyWithProps - extends HttpApiEndpoint +export interface AnyWithProps extends + HttpApiEndpoint< + string, + HttpMethod, + string, + Schema.Top, + Schema.Top, + Schema.Top, + Schema.Top, + any, + any + > {} /** @@ -578,10 +588,10 @@ export type RequestRaw = Endpoint extends HttpApiEndpoint< * @since 4.0.0 */ export type ClientRequest< - Params extends Schema.Top, - Query extends Schema.Top, - Payload extends Schema.Top, - Headers extends Schema.Top, + Params extends Schema.Constraint, + Query extends Schema.Constraint, + Payload extends Schema.Constraint, + Headers extends Schema.Constraint, ResponseMode extends ClientResponseMode > = ( & ([Params["Type"]] extends [never] ? {} : { readonly params: Params["Type"] }) @@ -1027,8 +1037,8 @@ function makeProto< * @since 4.0.0 */ export type ParamsConstraint = - | Record> - | Schema.Encoder, unknown> + | Record> + | Schema.Codec, unknown, unknown> /** * Constraint for header schemas: each header must encode to `string | undefined`, @@ -1038,8 +1048,8 @@ export type ParamsConstraint = * @since 4.0.0 */ export type HeadersConstraint = - | Record> - | Schema.Encoder, unknown> + | Record> + | Schema.Codec, unknown, unknown> /** * Constraint for query schemas: each field must encode to `string`, an array of @@ -1049,8 +1059,8 @@ export type HeadersConstraint = * @since 4.0.0 */ export type QueryConstraint = - | Record | undefined, unknown>> - | Schema.Encoder | undefined>, unknown> + | Record | undefined, unknown, unknown>> + | Schema.Codec | undefined, unknown, unknown> /** * Payload schema depends on the HTTP method: @@ -1065,7 +1075,7 @@ export type QueryConstraint = */ export type PayloadConstraint = Method extends HttpMethod.NoBody ? Record< string, - Schema.Encoder | undefined, unknown> + Schema.Codec | undefined, unknown, unknown> > : Schema.Top | ReadonlyArray @@ -1101,7 +1111,7 @@ export type ErrorConstraint = Schema.Top | ReadonlyArray type ErrorNoStream = [ Extract< - S extends ReadonlyArray ? S[number] : S, + S extends ReadonlyArray ? S[number] : S, HttpApiSchema.StreamSchema > ] extends [never] ? S : never @@ -1147,7 +1157,7 @@ export const make = (method: Method): { : StringTree>, StringTree : Headers>, JsonSuccessOrArray, - Json ? Error[number] : Error> + Json ? Error[number] : Error> > < const Name extends string, @@ -1179,7 +1189,7 @@ export const make = (method: Method): { ExtractSchemaOrArray, ExtractSchemaOrArray, ExtractSuccessOrArray, - Error extends ReadonlyArray ? Error[number] : Error + Error extends ReadonlyArray ? Error[number] : Error > } => < @@ -1210,11 +1220,11 @@ export const make = (method: Method): { Params extends Schema.Struct.Fields ? Schema.Struct : Params, Query extends Schema.Struct.Fields ? Schema.Struct : Query, Payload extends Schema.Struct.Fields ? Schema.Struct - : Payload extends ReadonlyArray ? Payload[number] + : Payload extends ReadonlyArray ? Payload[number] : Payload, Headers extends Schema.Struct.Fields ? Schema.Struct : Headers, ExtractSuccessOrArray, - Error extends ReadonlyArray ? Error[number] : Error + Error extends ReadonlyArray ? Error[number] : Error > => { const disableCodecs = options?.disableCodecs ?? false const transformStringTree = disableCodecs ? identity : Schema.toCodecStringTree @@ -1233,10 +1243,10 @@ export const make = (method: Method): { }) } -type ExtractSchemaOrArray> = S extends - Schema.Struct.Fields ? Schema.Struct - : S extends ReadonlyArray ? S[number] - : S +type ExtractSchemaOrArray> = + S extends Schema.Struct.Fields ? Schema.Struct + : S extends ReadonlyArray ? S[number] + : S /** * A schema codec that decodes and encodes the schema's value type through JSON @@ -1245,7 +1255,7 @@ type ExtractSchemaOrArray +export interface Json extends Schema.Codec {} @@ -1256,7 +1266,7 @@ export interface Json * @category Codecs * @since 4.0.0 */ -export interface StringTree extends +export interface StringTree extends Schema.Codec< S["Type"], Schema.StringTree, @@ -1267,7 +1277,7 @@ export interface StringTree extends function ensureStruct( params: Schema.Struct.Fields | Schema.Top | undefined, - transform: typeof Schema.toCodecJson | typeof Schema.toCodecStringTree + transform: (schema: Schema.Top) => Schema.Top ): Schema.Top | undefined { if (params === undefined) return undefined if (Schema.isSchema(params)) return transform(params) @@ -1275,12 +1285,14 @@ function ensureStruct( } function getPayload( - payload: Schema.Top | ReadonlyArray | Schema.Struct.Fields | undefined, + payload: Schema.Constraint | ReadonlyArray | Schema.Struct.Fields | undefined, method: HttpMethod, disableCodecs: boolean ): PayloadMap { - const result: Map] }> = - new Map() + const result: Map< + string, + { encoding: HttpApiSchema.PayloadEncoding; schemas: [Schema.Top, ...Array] } + > = new Map() if (payload === undefined) return result const schemas: Array = Array.isArray(payload) ? payload @@ -1338,7 +1350,7 @@ function getErrorResponse( return new Set(disableCodecs ? schemas : schemas.map(transformResponse)) } -function validateSuccessResponse(schemas: ReadonlyArray, method: HttpMethod) { +function validateSuccessResponse(schemas: ReadonlyArray, method: HttpMethod) { const statuses = new Map diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts index 5ad946475..ee7875e82 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts @@ -45,8 +45,8 @@ export const isSecurity = (u: AnyService): u is AnyServiceSecurity => hasPropert type ErrorConstraint = Schema.Top | ReadonlyArray -type ErrorSchemaFromConstraint = E extends ReadonlyArray ? E[number] - : E extends Schema.Top ? E +type ErrorSchemaFromConstraint = E extends ReadonlyArray ? E[number] + : E extends Schema.Constraint ? E : never /** diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts index b3bc8cf48..622f08e01 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts @@ -233,7 +233,7 @@ export const Accepted: Accepted = Empty(202) * @category schemas * @since 4.0.0 */ -export interface asNoContent extends Schema.decodeTo, Schema.Void> {} +export interface asNoContent extends Schema.decodeTo, Schema.Void> {} /** * Marks a schema as a no-content response while preserving a decoded client value. @@ -249,7 +249,7 @@ export interface asNoContent extends Schema.decodeTo(options: { +export function asNoContent(options: { readonly decode: LazyArg }) { return (self: S): asNoContent => { @@ -292,19 +292,22 @@ export type StreamSseMode = "events" | "data" */ export interface StreamSse< Events extends Sse.EventCodec, - Error extends Schema.Top, + Error extends Schema.Constraint, Value = Events["Type"] > extends - Schema.Bottom< - Stream.Stream, - Stream.Stream, - Events["DecodingServices"] | Error["DecodingServices"], - Events["EncodingServices"] | Error["EncodingServices"], + Schema.BottomLazy< SchemaAST.Declaration, StreamSse > { + readonly "Type": Stream.Stream + readonly "Encoded": Stream.Stream + readonly "DecodingServices": Events["DecodingServices"] | Error["DecodingServices"] + readonly "EncodingServices": Events["EncodingServices"] | Error["EncodingServices"] readonly "Rebuild": StreamSse + readonly "~type.make.in": Stream.Stream + readonly "~type.make": Stream.Stream + readonly "Iso": Stream.Stream readonly [StreamSchemaTypeId]: typeof StreamSchemaTypeId readonly _tag: "StreamSse" readonly mode: "sse" @@ -321,7 +324,7 @@ export interface StreamSse< * @category models * @since 4.0.0 */ -export interface SseEventFromData extends +export interface SseEventFromData extends Schema.Codec< { readonly id: string | undefined @@ -375,20 +378,6 @@ export interface StreamUint8Array extends */ export type StreamSchema = StreamSse | StreamUint8Array -/** @internal */ -export type StreamMetadata = - | { - readonly mode: "sse" - readonly sseMode: StreamSseMode - readonly contentType: string - readonly events: Sse.EventCodec - readonly error: Schema.Top - } - | { - readonly mode: "uint8array" - readonly contentType: string - } - const streamSchema = Schema.declare(Stream.isStream) /** @@ -398,12 +387,12 @@ const streamSchema = Schema.declare(Stream.isStream) * @since 4.0.0 */ export const StreamSse: { - (options: { + (options: { readonly contentType?: string | undefined readonly events: Events readonly error?: Error | undefined }): StreamSse - (options: { + (options: { readonly contentType?: string | undefined readonly data: Data readonly error?: Error | undefined @@ -411,8 +400,8 @@ export const StreamSse: { } = (options: { readonly contentType?: string | undefined readonly events?: Sse.EventCodec | undefined - readonly data?: Schema.Top | undefined - readonly error?: Schema.Top | undefined + readonly data?: Schema.Constraint | undefined + readonly error?: Schema.Constraint | undefined }): StreamSse => { const events = options.events ?? (options.data === undefined ? undefined : Schema.Struct({ id: Schema.UndefinedOr(Schema.String), @@ -461,22 +450,6 @@ export const isStreamSse = (u: unknown): u is StreamSse isStreamSchema(u) && u._tag === "StreamUint8Array" -/** @internal */ -export function getStreamMetadata(self: StreamSchema): StreamMetadata { - return self._tag === "StreamSse" ? - { - mode: self.mode, - sseMode: self.sseMode, - contentType: self.contentType, - events: self.events, - error: self.error - } : - { - mode: self.mode, - contentType: self.contentType - } -} - function defaultStreamContentType(mode: StreamMode): string { switch (mode) { case "sse": diff --git a/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts b/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts index 704494373..1b8dc2e5b 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts @@ -454,7 +454,7 @@ export function fromApi( } } - function processParameters(schema: Schema.Top | undefined, i: OpenAPISpecParameter["in"]) { + function processParameters(schema: Schema.Constraint | undefined, i: OpenAPISpecParameter["in"]) { if (schema) { const ast = SchemaAST.getLastEncoding(schema.ast) if (SchemaAST.isObjects(ast)) { @@ -636,7 +636,7 @@ function extractSuccessResponseBodies(endpoint: HttpApiEndpoint.AnyWithProps): R } function extractResponseBodies( - schemas: Array, + schemas: Array, getStatus: (ast: SchemaAST.AST) => number, getDescription: (ast: SchemaAST.AST) => string | undefined ): ResponseBodies { @@ -650,7 +650,7 @@ function extractResponseBodies( return map - function process(schema: Schema.Top) { + function process(schema: Schema.Constraint) { if (HttpApiSchema.isStreamSchema(schema)) { addStreamContent(schema) return @@ -679,7 +679,7 @@ function extractResponseBodies( } } - function addContent(schema: Schema.Top, status: number, encoding: HttpApiSchema.Encoding) { + function addContent(schema: Schema.Constraint, status: number, encoding: HttpApiSchema.Encoding) { const description = getDescription(schema.ast) const statusMap = map.get(status) const { _tag, contentType } = encoding @@ -738,7 +738,7 @@ type Content = Map< HttpApiSchema.Encoding["_tag"], Map< string, // contentType - Set + Set > > diff --git a/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts b/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts index 8410fe9a9..9a2b515b8 100644 --- a/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts +++ b/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts @@ -284,7 +284,9 @@ const makeOtlpSpan = (self: SpanImpl): OtlpSpan => { value: { boolValue: true } }) } else { - const errors = Cause.prettyErrors(status.exit.cause) + const errors = Cause.prettyErrors(status.exit.cause, { + includeCauseInStack: true + }) otelStatus = { code: StatusCode.Error } diff --git a/.context/effect/packages/effect/src/unstable/observability/internal/otlpProtobuf.ts b/.context/effect/packages/effect/src/unstable/observability/internal/otlpProtobuf.ts index cf12ae71a..a068668f9 100644 --- a/.context/effect/packages/effect/src/unstable/observability/internal/otlpProtobuf.ts +++ b/.context/effect/packages/effect/src/unstable/observability/internal/otlpProtobuf.ts @@ -138,33 +138,6 @@ export const encodeResource = (resource: Resource): Uint8Array => : new Uint8Array(0) ) -// Trace types (opentelemetry.proto.trace.v1) - -/** - * Status code enum - * - * @internal - */ -export const StatusCode = { - Unset: 0, - Ok: 1, - Error: 2 -} as const - -/** - * SpanKind enum - * - * @internal - */ -export const SpanKind = { - Unspecified: 0, - Internal: 1, - Server: 2, - Client: 3, - Producer: 4, - Consumer: 5 -} as const - /** * Encodes a Status message. * @@ -376,19 +349,6 @@ export const encodeResourceSpans = (resourceSpans: ResourceSpan): Uint8Array => export const encodeTracesData = (tracesData: TraceData): Uint8Array => Proto.repeatedField(1, tracesData.resourceSpans, encodeResourceSpans) -// Metrics types (opentelemetry.proto.metrics.v1) - -/** - * AggregationTemporality enum - * - * @internal - */ -export const AggregationTemporality = { - Unspecified: 0, - Delta: 1, - Cumulative: 2 -} as const - /** * Encodes a NumberDataPoint message. * @@ -631,41 +591,6 @@ export const encodeMetricsData = (metricsData: { readonly resourceMetrics: ReadonlyArray[0]> }): Uint8Array => Proto.repeatedField(1, metricsData.resourceMetrics, encodeResourceMetrics) -// Logs types (opentelemetry.proto.logs.v1) - -/** - * SeverityNumber enum - * - * @internal - */ -export const SeverityNumber = { - Unspecified: 0, - Trace: 1, - Trace2: 2, - Trace3: 3, - Trace4: 4, - Debug: 5, - Debug2: 6, - Debug3: 7, - Debug4: 8, - Info: 9, - Info2: 10, - Info3: 11, - Info4: 12, - Warn: 13, - Warn2: 14, - Warn3: 15, - Warn4: 16, - Error: 17, - Error2: 18, - Error3: 19, - Error4: 20, - Fatal: 21, - Fatal2: 22, - Fatal3: 23, - Fatal4: 24 -} as const - /** * Encodes a LogRecord message. * diff --git a/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts b/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts index 28ae1c754..cf6fa3972 100644 --- a/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts +++ b/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts @@ -37,17 +37,6 @@ export const encodeVarint = (value: number | bigint): Uint8Array => { return new Uint8Array(bytes) } -/** - * Encodes a signed varint using ZigZag encoding - * - * @internal - */ -export const encodeSint = (value: number | bigint): Uint8Array => { - const n = typeof value === "bigint" ? value : BigInt(value) - const zigzag = (n << BigInt(1)) ^ (n >> BigInt(63)) - return encodeVarint(zigzag) -} - /** * Encodes a 64-bit fixed value (little-endian) * @@ -133,17 +122,6 @@ export const varintField = (fieldNumber: number, value: number | bigint): Uint8A encodeVarint(value) ) -/** - * Encodes a sint field (ZigZag encoded) - * - * @internal - */ -export const sintField = (fieldNumber: number, value: number | bigint): Uint8Array => - concat( - encodeVarint(encodeTag(fieldNumber, WireType.Varint)), - encodeSint(value) - ) - /** * Encodes a bool field * @@ -231,26 +209,6 @@ export const repeatedField = ( encode: (value: T) => Uint8Array ): Uint8Array => concat(...values.map((v) => messageField(fieldNumber, encode(v)))) -/** - * Encodes repeated varint fields (not packed) - * - * @internal - */ -export const repeatedVarintField = ( - fieldNumber: number, - values: ReadonlyArray -): Uint8Array => concat(...values.map((v) => varintField(fieldNumber, v))) - -/** - * Helper to conditionally encode an optional field - * - * @internal - */ -export const optionalField = ( - value: T | undefined, - encode: (v: T) => Uint8Array -): Uint8Array => value !== undefined ? encode(value) : new Uint8Array(0) - /** * Helper to conditionally encode a string field if non-empty * diff --git a/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts b/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts index 822568e8d..061536fff 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts @@ -681,7 +681,7 @@ const SchemaStoreTypeId = "~effect/persistence/KeyValueStore/SchemaStore" as con * @category SchemaStore * @since 4.0.0 */ -export interface SchemaStore { +export interface SchemaStore { readonly [SchemaStoreTypeId]: typeof SchemaStoreTypeId /** * Returns the value of the specified key if it exists. @@ -742,7 +742,7 @@ export interface SchemaStore { * @category SchemaStore * @since 4.0.0 */ -export const toSchemaStore = (self: KeyValueStore, schema: S): SchemaStore => { +export const toSchemaStore = (self: KeyValueStore, schema: S): SchemaStore => { const serializer = Schema.toCodecJson(schema) const jsonSchema = Schema.fromJsonString(serializer) const decode = Schema.decodeEffect(jsonSchema) diff --git a/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts b/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts index 4470dc429..82a809f89 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts @@ -37,7 +37,7 @@ export const symbol = "~effect/persistence/Persistable" as const * @category models * @since 4.0.0 */ -export interface Persistable extends PrimaryKey.PrimaryKey { +export interface Persistable extends PrimaryKey.PrimaryKey { readonly [symbol]: { readonly success: A readonly error: E @@ -50,7 +50,7 @@ export interface Persistable extends * @category models * @since 4.0.0 */ -export type Any = Persistable +export type Any = Persistable /** * Extracts the success schema from a persistable request. @@ -147,8 +147,8 @@ export const Class = < >() => < const Tag extends string, - A extends Schema.Top = Schema.Void, - E extends Schema.Top = Schema.Never + A extends Schema.Constraint = Schema.Void, + E extends Schema.Constraint = Schema.Never >(tag: Tag, options: { readonly primaryKey: (payload: Config["payload"]) => string readonly success?: A | undefined @@ -205,7 +205,7 @@ export const Class = < * @category accessors * @since 4.0.0 */ -export const exitSchema = ( +export const exitSchema = ( self: Persistable ): Schema.Exit => { let schema = exitSchemaCache.get(self) @@ -224,7 +224,7 @@ const exitSchemaCache = new WeakMap, Schema.Exit( +export const serializeExit = ( self: Persistable, exit: Exit.Exit ): Effect.Effect => { @@ -239,7 +239,7 @@ export const serializeExit = ( * @category serialization * @since 4.0.0 */ -export const deserializeExit = ( +export const deserializeExit = ( self: Persistable, encoded: unknown ): Effect.Effect< diff --git a/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts b/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts index d4dc3070b..228a28382 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts @@ -104,7 +104,7 @@ export interface PersistedQueue { export class PersistedQueueFactory extends Context.Service< PersistedQueueFactory, { - readonly make: (options: { + readonly make: (options: { readonly name: string readonly schema: S }) => Effect.Effect> @@ -118,7 +118,7 @@ export class PersistedQueueFactory extends Context.Service< * @category accessors * @since 4.0.0 */ -export const make = (options: { +export const make = (options: { readonly name: string readonly schema: S }): Effect.Effect< @@ -143,7 +143,7 @@ export const makeFactory = Effect.gen(function*() { const store = yield* PersistedQueueStore return PersistedQueueFactory.of({ - make(options: { + make(options: { readonly name: string readonly schema: S }) { diff --git a/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts b/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts index a374b100a..975aea833 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts @@ -67,28 +67,28 @@ export class Persistence extends Context.Service( + readonly get: ( key: Persistable.Persistable ) => Effect.Effect< Exit.Exit | undefined, PersistenceError | Schema.SchemaError, A["DecodingServices"] | E["DecodingServices"] > - readonly getMany: ( + readonly getMany: ( keys: Iterable> ) => Effect.Effect< Array | undefined>, PersistenceError | Schema.SchemaError, A["DecodingServices"] | E["DecodingServices"] > - readonly set: ( + readonly set: ( key: Persistable.Persistable, value: Exit.Exit ) => Effect.Effect - readonly setMany: ( + readonly setMany: ( entries: Iterable, Exit.Exit]> ) => Effect.Effect - readonly remove: ( + readonly remove: ( key: Persistable.Persistable ) => Effect.Effect readonly clear: Effect.Effect diff --git a/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts b/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts index 575330fe1..15984d9d1 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts @@ -53,6 +53,10 @@ export interface RateLimiter { readonly key: string readonly tokens?: number | undefined }) => Effect.Effect + + readonly adaptiveConsume: (options: AdaptiveConsumeOptions) => Effect.Effect + + readonly adaptiveFeedback: (options: AdaptiveFeedbackOptions) => Effect.Effect } /** @@ -88,6 +92,8 @@ export const make: Effect.Effect< return identity({ [TypeId]: TypeId, + adaptiveConsume: store.adaptiveConsume, + adaptiveFeedback: store.adaptiveFeedback, consume(options) { const tokens = options.tokens ?? 1 const onExceeded = options.onExceeded ?? "fail" @@ -470,12 +476,104 @@ export interface ConsumeResult { } /** - * Defines the low-level backing store for fixed-window counters and token-bucket state. + * Phase of adaptive rate limiting driven by server feedback. + * + * @category models + * @since 4.0.0 + */ +export type AdaptivePhase = "inactive" | "cooldown" | "learning" | "learned" + +/** + * Options for consuming tokens from the adaptive rate limiter store. + * + * @category models + * @since 4.0.0 + */ +export interface AdaptiveConsumeOptions { + /** + * The rate-limit key. + */ + readonly key: string + + /** + * The number of tokens to consume. + */ + readonly tokens: number + + /** + * The fallback limit configured for the regular rate limiter. + */ + readonly fallbackLimit: number + + /** + * The fallback window configured for the regular rate limiter. + */ + readonly fallbackWindow: Duration.Duration +} + +/** + * Metadata returned after consuming tokens from the adaptive rate limiter store. + * + * @category models + * @since 4.0.0 + */ +export interface AdaptiveConsumeResult { + /** + * The amount of delay to wait before making the request. + */ + readonly delay: Duration.Duration + + /** + * The adaptive state epoch used to correlate later response feedback. + */ + readonly epoch: number + + /** + * The adaptive phase observed by this consume operation. + */ + readonly phase: AdaptivePhase +} + +/** + * Options for reporting response feedback to the adaptive rate limiter store. + * + * @category models + * @since 4.0.0 + */ +export interface AdaptiveFeedbackOptions { + /** + * The rate-limit key. + */ + readonly key: string + + /** + * The adaptive state epoch returned by `adaptiveConsume`. + */ + readonly epoch: number + + /** + * The number of tokens consumed by the request. + */ + readonly tokens: number + + /** + * The HTTP response status code. + */ + readonly status: number + + /** + * The parsed `Retry-After` delay, when present. + */ + readonly retryAfter: Duration.Duration | undefined +} + +/** + * Defines the low-level backing store for rate-limit state. * * **When to use** * - * Use to provide the shared counter storage used by persistent rate-limit - * checks. + * Use to provide the shared counter storage and adaptive feedback state used by + * persistent rate-limit checks. * * @category store * @since 4.0.0 @@ -517,9 +615,43 @@ export class RateLimiterStore extends Context.Service< readonly refillRate: Duration.Duration readonly allowOverflow: boolean }) => Effect.Effect + + /** + * Consumes tokens from the adaptive rate-limit state for the `key`. + * + * When the store has no adaptive state for the `key`, implementations + * should return a zero delay with the inactive phase. + */ + readonly adaptiveConsume: ( + options: AdaptiveConsumeOptions + ) => Effect.Effect + + /** + * Records response feedback for the adaptive rate-limit state. + */ + readonly adaptiveFeedback: (options: AdaptiveFeedbackOptions) => Effect.Effect } >()("effect/persistence/RateLimiter/RateLimiterStore") {} +const adaptiveStateTtlGraceMillis = 60_000 +const adaptiveStateMaxWindowMillis = 60 * 60 * 1_000 + +const clampAdaptiveDurationMillis = (millis: number): number => { + if (Number.isNaN(millis) || millis <= 0) return 1 + return Math.min(millis, adaptiveStateMaxWindowMillis) +} + +interface AdaptiveState { + phase: AdaptivePhase + epoch: number + cooldownUntil: number + learningStartedAt: number + observedTokens: number + learnedLimit: number + learnedWindowMillis: number + expiresAt: number +} + /** * Provides a process-local in-memory `RateLimiterStore`. * @@ -531,6 +663,25 @@ export const layerStoreMemory: Layer.Layer< > = Layer.sync(RateLimiterStore, () => { const fixedCounters = new Map() const tokenBuckets = new Map() + const adaptiveStates = new Map() + + const getAdaptiveState = (key: string, now: number): AdaptiveState | undefined => { + const state = adaptiveStates.get(key) + if (!state) return undefined + if (state.expiresAt <= now) { + adaptiveStates.delete(key) + return undefined + } + return state + } + + const cooldownExpiresAt = (cooldownUntil: number): number => cooldownUntil + adaptiveStateTtlGraceMillis + + const learningExpiresAt = (now: number, fallbackWindow: Duration.Duration): number => + now + Duration.toMillis(fallbackWindow) + adaptiveStateTtlGraceMillis + + const learnedExpiresAt = (now: number, learnedWindowMillis: number): number => + now + learnedWindowMillis + adaptiveStateTtlGraceMillis return RateLimiterStore.of({ fixedWindow: (options) => @@ -575,6 +726,146 @@ export const layerStoreMemory: Layer.Layer< } return newTokenCount }) + ), + adaptiveConsume: (options) => + Effect.clockWith((clock) => + Effect.sync(() => { + const now = clock.currentTimeMillisUnsafe() + const state = getAdaptiveState(options.key, now) + if (!state) { + return { + delay: Duration.zero, + epoch: 0, + phase: "inactive" + } + } + + if (state.phase === "cooldown") { + if (state.cooldownUntil > now) { + return { + delay: Duration.millis(state.cooldownUntil - now), + epoch: state.epoch, + phase: "cooldown" + } + } + + state.phase = "learning" + state.epoch += 1 + state.learningStartedAt = now + state.observedTokens = options.tokens + state.expiresAt = learningExpiresAt(now, options.fallbackWindow) + return { + delay: Duration.zero, + epoch: state.epoch, + phase: "learning" + } + } + + if (state.phase === "learning") { + state.observedTokens += options.tokens + return { + delay: Duration.zero, + epoch: state.epoch, + phase: state.phase + } + } + + if (state.phase === "learned") { + const refillRateMillis = state.learnedWindowMillis / state.learnedLimit + if (state.cooldownUntil <= now) { + state.observedTokens = 0 + state.cooldownUntil = now + } + state.observedTokens += options.tokens + state.cooldownUntil += refillRateMillis * options.tokens + + const ttl = state.cooldownUntil - now + const ttlTotal = state.observedTokens * refillRateMillis + const elapsed = ttlTotal - ttl + const windowNumber = Math.floor((state.observedTokens - 1) / state.learnedLimit) + const remaining = (windowNumber * state.learnedWindowMillis) - elapsed + + return { + delay: remaining <= 0 ? Duration.zero : Duration.millis(remaining), + epoch: state.epoch, + phase: state.phase + } + } + + return { + delay: Duration.zero, + epoch: state.epoch, + phase: state.phase + } + }) + ), + adaptiveFeedback: (options) => + Effect.clockWith((clock) => + Effect.sync(() => { + if (options.status !== 429 || options.retryAfter === undefined) return + + const retryAfterMillis = clampAdaptiveDurationMillis(Duration.toMillis(options.retryAfter)) + + const now = clock.currentTimeMillisUnsafe() + const cooldownUntil = now + retryAfterMillis + const state = getAdaptiveState(options.key, now) + if (!state) { + if (options.epoch !== 0) return + adaptiveStates.set(options.key, { + phase: "cooldown", + epoch: 0, + cooldownUntil, + learningStartedAt: 0, + observedTokens: 0, + learnedLimit: 0, + learnedWindowMillis: 0, + expiresAt: cooldownExpiresAt(cooldownUntil) + }) + return + } + + if (state.epoch !== options.epoch) return + + if (state.phase === "cooldown") { + state.cooldownUntil = Math.max(state.cooldownUntil, cooldownUntil) + state.expiresAt = cooldownExpiresAt(state.cooldownUntil) + return + } + + if (state.phase === "learning") { + const acceptedTokens = state.observedTokens - options.tokens + if (acceptedTokens <= 0) { + state.phase = "cooldown" + state.cooldownUntil = cooldownUntil + state.learningStartedAt = 0 + state.observedTokens = 0 + state.learnedLimit = 0 + state.learnedWindowMillis = 0 + state.expiresAt = cooldownExpiresAt(cooldownUntil) + return + } + + const learnedWindowMillis = clampAdaptiveDurationMillis((now - state.learningStartedAt) + retryAfterMillis) + state.phase = "learned" + state.epoch += 1 + state.cooldownUntil = state.learningStartedAt + learnedWindowMillis + state.observedTokens = acceptedTokens + state.learnedLimit = acceptedTokens + state.learnedWindowMillis = learnedWindowMillis + state.expiresAt = learnedExpiresAt(now, learnedWindowMillis) + return + } + + if (state.phase === "learned") { + state.phase = "cooldown" + state.cooldownUntil = cooldownUntil + state.learningStartedAt = 0 + state.observedTokens = 0 + state.learnedLimit = 0 + state.learnedWindowMillis = 0 + state.expiresAt = cooldownExpiresAt(cooldownUntil) + } + }) ) }) }) @@ -596,6 +887,8 @@ export const makeStoreRedis = Effect.fnUntraced(function*( const fixedWindow = redis.eval(fixedWindowScript) const tokenBucket = redis.eval(tokenBucketScript) + const adaptiveConsume = redis.eval(adaptiveConsumeScript) + const adaptiveFeedback = redis.eval(adaptiveFeedbackScript) return RateLimiterStore.of({ fixedWindow(options) { @@ -636,6 +929,55 @@ export const makeStoreRedis = Effect.fnUntraced(function*( }) ) ) + }, + adaptiveConsume(options) { + const key = `${prefix}${options.key}:adaptive` + return Effect.map( + Effect.mapError( + adaptiveConsume( + key, + options.tokens, + Duration.toMillis(options.fallbackWindow), + adaptiveStateTtlGraceMillis + ), + (cause) => + new RateLimiterError({ + reason: new RateLimitStoreError({ + message: `Failed to execute adaptiveConsume rate limiting command`, + cause: cause.cause + }) + }) + ), + ([delayMillis, epoch, phase]) => ({ + delay: delayMillis <= 0 ? Duration.zero : Duration.millis(delayMillis), + epoch, + phase + }) + ) + }, + adaptiveFeedback(options) { + if (options.status !== 429 || options.retryAfter === undefined) return Effect.void + const retryAfterMillis = clampAdaptiveDurationMillis(Duration.toMillis(options.retryAfter)) + const key = `${prefix}${options.key}:adaptive` + return Effect.asVoid( + Effect.mapError( + adaptiveFeedback( + key, + options.epoch, + options.tokens, + retryAfterMillis, + adaptiveStateTtlGraceMillis, + adaptiveStateMaxWindowMillis + ), + (cause) => + new RateLimiterError({ + reason: new RateLimitStoreError({ + message: `Failed to execute adaptiveFeedback rate limiting command`, + cause: cause.cause + }) + }) + ) + ) } }) }) @@ -652,7 +994,7 @@ local limit = tonumber(ARGV[3]) local current = tonumber(redis.call("GET", key)) if not current then - local nextpttl = refillms * tokens + local nextpttl = math.max(1, math.ceil(refillms * tokens)) redis.call("SET", key, tokens, "PX", nextpttl) return { tokens, nextpttl } end @@ -663,7 +1005,7 @@ if limit and next > limit then return { next, currentpttl } end -local nextpttl = currentpttl + (refillms * tokens) +local nextpttl = math.max(1, math.ceil(currentpttl + (refillms * tokens))) redis.call("SET", key, next, "PX", nextpttl) return { next, nextpttl } ` @@ -718,6 +1060,258 @@ return next } ).withReturnType() +const adaptiveConsumeScript = Redis.script( + (key: string, tokens: number, fallbackWindowMillis: number, ttlGraceMillis: number) => [ + key, + tokens, + fallbackWindowMillis, + ttlGraceMillis + ], + { + numberOfKeys: 1, + lua: ` +local key = KEYS[1] +local tokens = tonumber(ARGV[1]) +local fallback_window_ms = tonumber(ARGV[2]) +local ttl_grace_ms = tonumber(ARGV[3]) + +local time = redis.call("TIME") +local now = (tonumber(time[1]) * 1000) + math.floor(tonumber(time[2]) / 1000) + +local phase = redis.call("HGET", key, "phase") +if not phase then + return { 0, 0, "inactive" } +end + +local epoch = tonumber(redis.call("HGET", key, "epoch") or "0") + +if phase == "cooldown" then + local cooldown_until = tonumber(redis.call("HGET", key, "cooldownUntil") or "0") + if cooldown_until > now then + return { math.ceil(cooldown_until - now), epoch, "cooldown" } + end + + epoch = epoch + 1 + redis.call( + "HSET", + key, + "phase", + "learning", + "epoch", + epoch, + "cooldownUntil", + 0, + "learningStartedAt", + now, + "observedTokens", + tokens, + "learnedLimit", + 0, + "learnedWindowMillis", + 0 + ) + redis.call("PEXPIRE", key, math.max(1, math.ceil(fallback_window_ms + ttl_grace_ms))) + return { 0, epoch, "learning" } +end + +if phase == "learning" then + local observed_tokens = tonumber(redis.call("HGET", key, "observedTokens") or "0") + tokens + redis.call("HSET", key, "observedTokens", observed_tokens) + return { 0, epoch, "learning" } +end + +if phase == "learned" then + local cooldown_until = tonumber(redis.call("HGET", key, "cooldownUntil") or "0") + local observed_tokens = tonumber(redis.call("HGET", key, "observedTokens") or "0") + local learned_limit = tonumber(redis.call("HGET", key, "learnedLimit") or "0") + local learned_window_ms = tonumber(redis.call("HGET", key, "learnedWindowMillis") or "0") + local refill_rate_ms = learned_window_ms / learned_limit + + if cooldown_until <= now then + observed_tokens = 0 + cooldown_until = now + end + + observed_tokens = observed_tokens + tokens + cooldown_until = cooldown_until + (refill_rate_ms * tokens) + + local ttl = cooldown_until - now + local ttl_total = observed_tokens * refill_rate_ms + local elapsed = ttl_total - ttl + local window_number = math.floor((observed_tokens - 1) / learned_limit) + local remaining = (window_number * learned_window_ms) - elapsed + + redis.call("HSET", key, "observedTokens", observed_tokens, "cooldownUntil", cooldown_until) + + if remaining <= 0 then + return { 0, epoch, "learned" } + end + return { math.ceil(remaining), epoch, "learned" } +end + +return { 0, epoch, phase } +` + } +).withReturnType() + +const adaptiveFeedbackScript = Redis.script( + ( + key: string, + epoch: number, + tokens: number, + retryAfterMillis: number, + ttlGraceMillis: number, + maxWindowMillis: number + ) => [ + key, + epoch, + tokens, + retryAfterMillis, + ttlGraceMillis, + maxWindowMillis + ], + { + numberOfKeys: 1, + lua: ` +local key = KEYS[1] +local feedback_epoch = tonumber(ARGV[1]) +local tokens = tonumber(ARGV[2]) +local retry_after_ms = tonumber(ARGV[3]) +local ttl_grace_ms = tonumber(ARGV[4]) +local max_window_ms = tonumber(ARGV[5]) + +if retry_after_ms <= 0 then + retry_after_ms = 1 +end +if retry_after_ms > max_window_ms then + retry_after_ms = max_window_ms +end + +local time = redis.call("TIME") +local now = (tonumber(time[1]) * 1000) + math.floor(tonumber(time[2]) / 1000) +local cooldown_until = now + retry_after_ms + +local phase = redis.call("HGET", key, "phase") +if not phase then + if feedback_epoch ~= 0 then + return 0 + end + redis.call( + "HSET", + key, + "phase", + "cooldown", + "epoch", + 0, + "cooldownUntil", + cooldown_until, + "learningStartedAt", + 0, + "observedTokens", + 0, + "learnedLimit", + 0, + "learnedWindowMillis", + 0 + ) + redis.call("PEXPIRE", key, math.max(1, math.ceil(retry_after_ms + ttl_grace_ms))) + return 1 +end + +local epoch = tonumber(redis.call("HGET", key, "epoch") or "0") +if epoch ~= feedback_epoch then + return 0 +end + +if phase == "cooldown" then + local current_cooldown_until = tonumber(redis.call("HGET", key, "cooldownUntil") or "0") + if current_cooldown_until > cooldown_until then + cooldown_until = current_cooldown_until + end + redis.call("HSET", key, "cooldownUntil", cooldown_until) + redis.call("PEXPIRE", key, math.max(1, math.ceil((cooldown_until - now) + ttl_grace_ms))) + return 1 +end + +if phase == "learning" then + local observed_tokens = tonumber(redis.call("HGET", key, "observedTokens") or "0") + local accepted_tokens = observed_tokens - tokens + if accepted_tokens <= 0 then + redis.call( + "HSET", + key, + "phase", + "cooldown", + "cooldownUntil", + cooldown_until, + "learningStartedAt", + 0, + "observedTokens", + 0, + "learnedLimit", + 0, + "learnedWindowMillis", + 0 + ) + redis.call("PEXPIRE", key, math.max(1, math.ceil(retry_after_ms + ttl_grace_ms))) + return 1 + end + + local learning_started_at = tonumber(redis.call("HGET", key, "learningStartedAt") or now) + local learned_window_ms = (now - learning_started_at) + retry_after_ms + if learned_window_ms <= 0 then + learned_window_ms = 1 + end + if learned_window_ms > max_window_ms then + learned_window_ms = max_window_ms + end + local next_epoch = epoch + 1 + redis.call( + "HSET", + key, + "phase", + "learned", + "epoch", + next_epoch, + "cooldownUntil", + learning_started_at + learned_window_ms, + "observedTokens", + accepted_tokens, + "learnedLimit", + accepted_tokens, + "learnedWindowMillis", + learned_window_ms + ) + redis.call("PEXPIRE", key, math.max(1, math.ceil(learned_window_ms + ttl_grace_ms))) + return 1 +end + +if phase == "learned" then + redis.call( + "HSET", + key, + "phase", + "cooldown", + "cooldownUntil", + cooldown_until, + "learningStartedAt", + 0, + "observedTokens", + 0, + "learnedLimit", + 0, + "learnedWindowMillis", + 0 + ) + redis.call("PEXPIRE", key, math.max(1, math.ceil(retry_after_ms + ttl_grace_ms))) + return 1 +end + +return 0 +` + } +).withReturnType() + /** * Provides a Redis-backed `RateLimiterStore` using `makeStoreRedis`. * diff --git a/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts b/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts index db0b1bb25..ec28b9d1d 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts @@ -923,8 +923,8 @@ class BuilderImpl { * @since 4.0.0 */ export interface Schema< - Success extends Schema_.Top, - Error extends Schema_.Top + Success extends Schema_.Constraint, + Error extends Schema_.Constraint > extends Schema_.declareConstructor< AsyncResult, @@ -943,8 +943,8 @@ export interface Schema< * @since 4.0.0 */ export const Schema = < - A extends Schema_.Top = Schema_.Never, - E extends Schema_.Top = Schema_.Never + A extends Schema_.Constraint = Schema_.Never, + E extends Schema_.Constraint = Schema_.Never >( options: { readonly success?: A | undefined diff --git a/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts b/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts index 2ab1835a8..a7b5478c8 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts @@ -2409,7 +2409,7 @@ export type SerializableTypeId = "~effect-atom/atom/Atom/Serializable" * @category Serializable * @since 4.0.0 */ -export interface Serializable { +export interface Serializable { readonly [SerializableTypeId]: { readonly key: string readonly encode: (value: S["Type"]) => S["Encoded"] diff --git a/.context/effect/packages/effect/src/unstable/reactivity/AtomHttpApi.ts b/.context/effect/packages/effect/src/unstable/reactivity/AtomHttpApi.ts index 2eff966a6..ae11886f9 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/AtomHttpApi.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/AtomHttpApi.ts @@ -343,7 +343,7 @@ type ResponseByMode = diff --git a/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts b/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts index 95e586941..a5ce20a04 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts @@ -51,7 +51,6 @@ export const isRpc = (u: unknown): u is Rpc => Predicate.hasPrope * @since 4.0.0 */ export interface DefectSchema extends Schema.Top { - readonly Type: unknown make(input: null, options?: Schema.MakeOptions): unknown make(input: undefined, options?: Schema.MakeOptions): unknown make(input: {}, options?: Schema.MakeOptions): unknown @@ -807,7 +806,7 @@ const Proto = { middlewares: this.middlewares }) }, - setPayload(this: AnyWithProps, payloadSchema: Schema.Struct | Schema.Struct.Fields) { + setPayload(this: AnyWithProps, payloadSchema: Schema.Struct | Schema.Struct.Fields) { return makeProto({ _tag: this._tag, payloadSchema: Schema.isSchema(payloadSchema) ? payloadSchema as any : Schema.Struct(payloadSchema as any), @@ -876,7 +875,7 @@ const makeProto = < readonly payloadSchema: Payload readonly successSchema: Success readonly errorSchema: Error - readonly defectSchema: Schema.Top + readonly defectSchema: Schema.Constraint readonly annotations: Context.Context readonly middlewares: ReadonlySet }): Rpc => { @@ -971,7 +970,7 @@ export const make = < * } * * // The type definition for the transformed success schema. - * export interface Paginated extends + * export interface Paginated extends * Schema.Struct<{ * readonly offset: Schema.Number * readonly total: Schema.Number @@ -1071,8 +1070,8 @@ export declare namespace Custom { * @since 4.0.0 */ export interface Out< - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint > { readonly success: Success readonly error: Error @@ -1097,8 +1096,8 @@ export declare namespace Custom { */ export type Kind< Def extends Custom, - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint > = (Def & { readonly success: Success readonly error: Error diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts index 46918223e..e240028b6 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts @@ -43,7 +43,7 @@ import * as Rpc from "./Rpc.ts" import { RpcClientDefect, RpcClientError } from "./RpcClientError.ts" import type * as RpcGroup from "./RpcGroup.ts" import type { FromClient, FromClientEncoded, FromServer, FromServerEncoded, Request } from "./RpcMessage.ts" -import { constPing, RequestId } from "./RpcMessage.ts" +import { constPing, isTerminalResponse, RequestId } from "./RpcMessage.ts" import type * as RpcMiddleware from "./RpcMiddleware.ts" import * as RpcSchema from "./RpcSchema.ts" import * as RpcSerialization from "./RpcSerialization.ts" @@ -887,6 +887,8 @@ export const makeProtocolHttp = (client: HttpClient.HttpClient): Effect.Effect< }) const emptyResponseError = (request: FromClientEncoded) => protocolDefect("Received empty HTTP response from RPC server", request) + const incompleteResponseError = (request: FromClientEncoded) => + protocolDefect("HTTP response ended before RPC request completed", request) const send = Effect.fnUntraced(function*(clientId: number, request: FromClientEncoded) { if (request._tag !== "Request") { @@ -914,15 +916,27 @@ export const makeProtocolHttp = (client: HttpClient.HttpClient): Effect.Effect< if (responses.length === 0) { return yield* emptyResponseError(request) } + let completed = false let i = 0 - return yield* Effect.whileLoop({ + yield* Effect.whileLoop({ while: () => i < responses.length, - body: () => writeResponse(clientId, responses[i++]), + body: () => { + const response = responses[i++] + if (isTerminalResponse(response)) { + completed = true + } + return writeResponse(clientId, response) + }, step: constVoid }) + if (!completed) { + return yield* incompleteResponseError(request) + } + return } let hasResponse = false + let completed = false yield* Stream.runForEachArray(response.stream, (chunk) => Effect.try({ try: () => chunk.flatMap(parser.decode) as Array, @@ -934,7 +948,13 @@ export const makeProtocolHttp = (client: HttpClient.HttpClient): Effect.Effect< let i = 0 return Effect.whileLoop({ while: () => i < responses.length, - body: () => writeResponse(clientId, responses[i++]), + body: () => { + const response = responses[i++] + if (isTerminalResponse(response)) { + completed = true + } + return writeResponse(clientId, response) + }, step: constVoid }) }) @@ -943,6 +963,8 @@ export const makeProtocolHttp = (client: HttpClient.HttpClient): Effect.Effect< ) if (!hasResponse) { return yield* emptyResponseError(request) + } else if (!completed) { + return yield* incompleteResponseError(request) } }) diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts index 182d72137..6dc98fa02 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts @@ -400,3 +400,22 @@ export interface Pong { * @since 4.0.0 */ export const constPong: Pong = { _tag: "Pong" } + +/** + * Checks if the response type is terminal. + * + * @category guards + * @since 4.0.0 + */ +export const isTerminalResponse = (response: FromServerEncoded): boolean => { + switch (response._tag) { + case "Exit": + case "Defect": + case "ClientProtocolError": { + return true + } + default: { + return false + } + } +} diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts index bdd1b3b2f..56773544b 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts @@ -142,7 +142,7 @@ export interface ServiceClass< Self, Name extends string, Provides, - E extends Schema.Top, + E extends Schema.Constraint, ClientError, Requires, RequiredForClient extends boolean @@ -193,7 +193,7 @@ export type ApplyServices = Exclude> | Requires * @since 4.0.0 */ export type ErrorSchema = A extends { readonly [TypeId]: { readonly error: infer E } } - ? E extends Schema.Top ? E : never + ? E extends Schema.Constraint ? E : never : never /** diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts index 517fba177..5b306dacc 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts @@ -26,12 +26,12 @@ const StreamSchemaTypeId = "~effect/rpc/RpcSchema/StreamSchema" * @category streams * @since 4.0.0 */ -export function isStreamSchema(schema: Schema.Top): schema is Stream { +export function isStreamSchema(schema: Schema.Constraint): schema is Stream { return Predicate.hasProperty(schema, StreamSchemaTypeId) } /** @internal */ -export function getStreamSchemas(schema: Schema.Top): Option.Option<{ +export function getStreamSchemas(schema: Schema.Constraint): Option.Option<{ readonly success: Schema.Top readonly error: Schema.Top }> { @@ -50,17 +50,20 @@ export function getStreamSchemas(schema: Schema.Top): Option.Option<{ * @category streams * @since 4.0.0 */ -export interface Stream extends - Schema.Bottom< - Stream_.Stream, - Stream_.Stream, - A["DecodingServices"] | E["DecodingServices"], - A["EncodingServices"] | E["EncodingServices"], +export interface Stream extends + Schema.BottomLazy< SchemaAST.Declaration, Stream > { + readonly "Type": Stream_.Stream + readonly "Encoded": Stream_.Stream + readonly "DecodingServices": A["DecodingServices"] | E["DecodingServices"] + readonly "EncodingServices": A["EncodingServices"] | E["EncodingServices"] readonly "Rebuild": Stream + readonly "~type.make.in": Stream_.Stream + readonly "~type.make": Stream_.Stream + readonly "Iso": Stream_.Stream readonly [StreamSchemaTypeId]: typeof StreamSchemaTypeId readonly success: A readonly error: E @@ -75,7 +78,7 @@ const schema = Schema.declare(Stream_.isStream) * @category streams * @since 4.0.0 */ -export function Stream(success: A, error: E): Stream { +export function Stream(success: A, error: E): Stream { return Schema.make(schema.ast, { [StreamSchemaTypeId]: StreamSchemaTypeId, success, error }) } diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts index b45c87119..66bde66d2 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts @@ -683,10 +683,6 @@ export const make: ( switch (request._tag) { case "Request": { const tag = Predicate.hasProperty(request, "tag") ? (request.tag as string) : "" - const rpc = group.requests.get(tag) - if (!rpc) { - return sendDefect(client, `Unknown request tag: ${tag}`) - } let requestId: RequestId switch (typeof request.id) { case "bigint": @@ -698,6 +694,10 @@ export const make: ( return sendDefect(client, `Invalid request id: ${request.id}`) } } + const rpc = group.requests.get(tag) + if (!rpc) { + return sendRequestDefect(client, requestId, (defect) => Effect.succeed(defect), `Unknown request tag: ${tag}`) + } const schemas = getSchemas(rpc as any) return Effect.matchEffect( Effect.provideContext(schemas.decode(request.payload), schemas.context), diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts index f798d1696..50e0b9f13 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts @@ -64,7 +64,7 @@ const ProtocolTag = Context.Service( * @category initial message * @since 4.0.0 */ -export const makeInitialMessage = ( +export const makeInitialMessage = ( schema: S, effect: Effect.Effect ): Effect.Effect< @@ -89,7 +89,7 @@ export const makeInitialMessage = ( * @category initial message * @since 4.0.0 */ -export const layerInitialMessage = ( +export const layerInitialMessage = ( schema: S, build: Effect.Effect ): Layer.Layer => @@ -108,7 +108,7 @@ export const layerInitialMessage = ( * @category initial message * @since 4.0.0 */ -export const initialMessage = ( +export const initialMessage = ( schema: S ): Effect.Effect => ProtocolTag.pipe( diff --git a/.context/effect/packages/effect/src/unstable/schema/Model.ts b/.context/effect/packages/effect/src/unstable/schema/Model.ts index 6354ce788..effd6423b 100644 --- a/.context/effect/packages/effect/src/unstable/schema/Model.ts +++ b/.context/effect/packages/effect/src/unstable/schema/Model.ts @@ -298,7 +298,7 @@ export const Sensitive = (schema: S): Sensitive => * @category optional * @since 4.0.0 */ -export interface optionalOption +export interface optionalOption extends Schema.decodeTo>, Schema.optionalKey>> {} @@ -309,7 +309,7 @@ export interface optionalOption * @category optional * @since 4.0.0 */ -export const optionalOption = (schema: S): optionalOption => +export const optionalOption = (schema: S): optionalOption => Schema.optionalKey(Schema.NullOr(schema)).pipe( Schema.decodeTo( Schema.Option(Schema.toType(schema)), diff --git a/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts b/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts index f0df2a937..060671283 100644 --- a/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts +++ b/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts @@ -262,17 +262,10 @@ export interface Class< readonly fields: Schema.Struct.Fields } > extends - Schema.Bottom< - Self, - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], + Schema.BottomLazy< SchemaAST.Declaration, Schema.decodeTo, S>, - S["~type.make.in"], - S["Iso"], readonly [S], - Self, S["~type.mutability"], S["~type.optionality"], S["~type.constructor.default"], @@ -281,6 +274,14 @@ export interface Class< >, Struct> { + readonly "Type": Self + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": Self + readonly "Iso": S["Iso"] + new( props: S["~type.make.in"], options?: { @@ -528,24 +529,25 @@ export const Override = (value: A): A & Brand<"Override"> => value as any * @since 4.0.0 */ export interface Overrideable extends - Schema.Bottom< - S["Type"] & Brand<"Override">, - S["Encoded"], - S["DecodingServices"], - S["EncodingServices"], + Schema.BottomLazy< S["ast"], Overrideable, - S["~type.make.in"], - (S["Type"] & Brand<"Override">) | undefined, S["~type.parameters"], - (S["Type"] & Brand<"Override">) | undefined, S["~type.mutability"], "required", "with-default", S["~encoded.mutability"], S["~encoded.optionality"] > -{} +{ + readonly "Type": S["Type"] & Brand<"Override"> + readonly "Encoded": S["Encoded"] + readonly "DecodingServices": S["DecodingServices"] + readonly "EncodingServices": S["EncodingServices"] + readonly "~type.make.in": S["~type.make.in"] + readonly "~type.make": (S["Type"] & Brand<"Override">) | undefined + readonly "Iso": (S["Type"] & Brand<"Override">) | undefined +} /** * Wraps a schema with an effectful constructor default while allowing explicit diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlConnection.ts b/.context/effect/packages/effect/src/unstable/sql/SqlConnection.ts index 5a45bbf47..d299f6c5c 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlConnection.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlConnection.ts @@ -50,6 +50,11 @@ export interface Connection { params: ReadonlyArray ) => Effect>, SqlError> + readonly executeValuesUnprepared: ( + sql: string, + params: ReadonlyArray + ) => Effect>, SqlError> + readonly executeUnprepared: ( sql: string, params: ReadonlyArray, diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts b/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts index 3814f1c8b..fd31502f1 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts @@ -77,7 +77,7 @@ export const makeRepository = < > => Effect.gen(function*() { const sql = yield* SqlClient - const idSchema = Model.fields[options.idColumn] as Schema.Top + const idSchema = Model.fields[options.idColumn] const idColumn = options.idColumn as string const softDeleteColumn = options.softDeleteColumn as string | undefined const withSoftDeleteFilter = (where: any) => @@ -274,7 +274,7 @@ export const makeResolvers = < > => Effect.gen(function*() { const sql = yield* SqlClient - const idSchema = Model.fields[options.idColumn] as Schema.Top + const idSchema = Model.fields[options.idColumn] const idColumn = options.idColumn as string const softDeleteColumn = options.softDeleteColumn as string | undefined const withSoftDeleteFilter = (where: any) => diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts b/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts index 98598797b..385117a08 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts @@ -98,7 +98,7 @@ export const SqlRequest = (payload: In): SqlRequest => * @category resolvers * @since 4.0.0 */ -export const ordered = ( +export const ordered = ( options: { readonly Request: Req readonly Result: Res @@ -146,7 +146,7 @@ export const ordered = * @category resolvers * @since 4.0.0 */ -export const grouped = ( +export const grouped = ( options: { readonly Request: Req readonly RequestGroupKey: (request: Req["Type"]) => K @@ -215,7 +215,7 @@ export const grouped = ( +export const findById = ( options: { readonly Id: Id readonly Result: Res @@ -272,7 +272,7 @@ export const findById = ( +const void_ = ( options: { readonly Request: Req readonly execute: ( @@ -322,7 +322,7 @@ const constNoSuchElement = Exit.fail(new Cause.NoSuchElementError()) const partitionRequests = function*( requests: Arr.NonEmptyArray>>, - schema: Schema.Codec + schema: Schema.ConstraintCodec ) { const len = requests.length const inputs = Arr.empty() @@ -347,7 +347,7 @@ const partitionRequests = function*( const partitionRequestsById = function*( requests: ReadonlyArray>>, - schema: Schema.Codec + schema: Schema.ConstraintCodec ) { const len = requests.length const inputs = Arr.empty() diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlSchema.ts b/.context/effect/packages/effect/src/unstable/sql/SqlSchema.ts index 0119f6edc..d0e06c8d8 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlSchema.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlSchema.ts @@ -30,7 +30,7 @@ import * as Schema from "../../Schema.ts" * @category constructors * @since 4.0.0 */ -export const findAll = ( +export const findAll = ( options: { readonly Request: Req readonly Result: Res @@ -62,7 +62,7 @@ export const findAll = ( * @category constructors * @since 4.0.0 */ -export const findNonEmpty = ( +export const findNonEmpty = ( options: { readonly Request: Req readonly Result: Res @@ -83,7 +83,7 @@ export const findNonEmpty = ( +const void_ = ( options: { readonly Request: Req readonly execute: (request: Req["Encoded"]) => Effect.Effect @@ -112,7 +112,7 @@ export { * @category constructors * @since 4.0.0 */ -export const findOne = ( +export const findOne = ( options: { readonly Request: Req readonly Result: Res @@ -145,7 +145,7 @@ export const findOne = ( * @category constructors * @since 4.0.0 */ -export const findOneOption = ( +export const findOneOption = ( options: { readonly Request: Req readonly Result: Res diff --git a/.context/effect/packages/effect/src/unstable/sql/Statement.ts b/.context/effect/packages/effect/src/unstable/sql/Statement.ts index 9cb0dd69b..261034c6a 100644 --- a/.context/effect/packages/effect/src/unstable/sql/Statement.ts +++ b/.context/effect/packages/effect/src/unstable/sql/Statement.ts @@ -72,6 +72,7 @@ export interface Statement extends Fragment, Effect.Effect, readonly withoutTransform: Effect.Effect, SqlError> readonly stream: Stream.Stream readonly values: Effect.Effect>, SqlError> + readonly valuesUnprepared: Effect.Effect>, SqlError> readonly unprepared: Effect.Effect, SqlError> readonly compile: (withoutTransform?: boolean | undefined) => readonly [ sql: string, @@ -1354,6 +1355,16 @@ const StatementProto: Omit< return this.withConnection("executeValues", (connection, sql, params) => connection.executeValues(sql, params)) }, + get valuesUnprepared(): Effect.Effect< + ReadonlyArray>, + SqlError + > { + return this.withConnection( + "executeValuesUnprepared", + (connection, sql, params) => connection.executeValuesUnprepared(sql, params) + ) + }, + get unprepared(): Effect.Effect, SqlError> { const self = this as StatementImpl return self.withConnection( diff --git a/.context/effect/packages/effect/src/unstable/workflow/Activity.ts b/.context/effect/packages/effect/src/unstable/workflow/Activity.ts index 91c9c58d9..79d5605f1 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/Activity.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/Activity.ts @@ -34,8 +34,8 @@ const TypeId = "~effect/workflow/Activity" * @since 4.0.0 */ export interface Activity< - Success extends Schema.Top = Schema.Void, - Error extends Schema.Top = Schema.Never, + Success extends Schema.Constraint = Schema.Void, + Error extends Schema.Constraint = Schema.Never, R = never > extends Effect.Effect< @@ -122,8 +122,8 @@ export interface AnyWithProps { */ export const make = < R, - Success extends Schema.Top = Schema.Void, - Error extends Schema.Top = Schema.Never + Success extends Schema.Constraint = Schema.Void, + Error extends Schema.Constraint = Schema.Never >(options: { readonly name: string readonly success?: Success | undefined @@ -303,8 +303,8 @@ const InstanceTag = Context.Service(activity: Activity) { const engine = yield* EngineTag const instance = yield* InstanceTag diff --git a/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts b/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts index 963c2c331..c943680b8 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts @@ -36,8 +36,8 @@ const TypeId = "~effect/workflow/DurableDeferred" * @since 4.0.0 */ export interface DurableDeferred< - Success extends Schema.Top, - Error extends Schema.Top = Schema.Never + Success extends Schema.Constraint, + Error extends Schema.Constraint = Schema.Never > { readonly [TypeId]: typeof TypeId readonly name: string @@ -82,8 +82,8 @@ export interface AnyWithProps { * @since 4.0.0 */ export const make = < - Success extends Schema.Top = Schema.Void, - Error extends Schema.Top = Schema.Never + Success extends Schema.Constraint = Schema.Void, + Error extends Schema.Constraint = Schema.Never >( name: string, options?: { @@ -129,7 +129,7 @@ const CurrentAttempt = Context.Reference( { defaultValue: () => 1 } ) -const await_: ( +const await_: ( self: DurableDeferred ) => Effect.Effect< Success["Type"], @@ -139,8 +139,8 @@ const await_: ( | Success["DecodingServices"] | Error["DecodingServices"] > = Effect.fnUntraced(function*< - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint >(self: DurableDeferred) { const engine = yield* EngineTag const instance = yield* InstanceTag @@ -173,7 +173,7 @@ export { * @since 4.0.0 */ export const into: { - ( + ( self: DurableDeferred ): ( effect: Effect.Effect @@ -186,7 +186,7 @@ export const into: { | Success["DecodingServices"] | Error["DecodingServices"] > - ( + ( effect: Effect.Effect, self: DurableDeferred ): Effect.Effect< @@ -200,7 +200,7 @@ export const into: { > } = dual( 2, - ( + ( effect: Effect.Effect, self: DurableDeferred ): Effect.Effect< @@ -404,10 +404,10 @@ export class TokenParsed extends Schema.Class( * @category token * @since 4.0.0 */ -export const token: ( +export const token: ( self: DurableDeferred ) => Effect.Effect = Effect.fnUntraced( - function*( + function*( self: DurableDeferred ) { const instance = yield* InstanceTag @@ -426,16 +426,16 @@ export const tokenFromExecutionId: { (options: { readonly workflow: Workflow.Any readonly executionId: string - }): ( + }): ( self: DurableDeferred ) => Token - ( + ( self: DurableDeferred, options: { readonly workflow: Workflow.Any; readonly executionId: string } ): Token } = dual( 2, - ( + ( self: DurableDeferred, options: { readonly workflow: Workflow.Any @@ -460,12 +460,12 @@ export const tokenFromPayload: { (options: { readonly workflow: W readonly payload: Workflow.PayloadSchema["~type.make.in"] - }): ( + }): ( self: DurableDeferred ) => Effect.Effect < - Success extends Schema.Top, - Error extends Schema.Top, + Success extends Schema.Constraint, + Error extends Schema.Constraint, W extends Workflow.Any >( self: DurableDeferred, @@ -477,8 +477,8 @@ export const tokenFromPayload: { } = dual( 2, < - Success extends Schema.Top, - Error extends Schema.Top, + Success extends Schema.Constraint, + Error extends Schema.Constraint, W extends Workflow.Any >( self: DurableDeferred, @@ -502,7 +502,7 @@ export const tokenFromPayload: { * @since 4.0.0 */ export const done: { - (options: { + (options: { readonly token: Token readonly exit: Exit.Exit }): ( @@ -512,7 +512,7 @@ export const done: { never, WorkflowEngine | Success["EncodingServices"] | Error["EncodingServices"] > - ( + ( self: DurableDeferred, options: { readonly token: Token @@ -526,8 +526,8 @@ export const done: { } = dual( 2, Effect.fnUntraced(function*< - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint >( self: DurableDeferred, options: { @@ -554,13 +554,13 @@ export const done: { * @since 4.0.0 */ export const succeed: { - (options: { + (options: { readonly token: Token readonly value: Success["Type"] }): ( self: DurableDeferred ) => Effect.Effect - ( + ( self: DurableDeferred, options: { readonly token: Token @@ -569,7 +569,7 @@ export const succeed: { ): Effect.Effect } = dual( 2, - ( + ( self: DurableDeferred, options: { readonly token: Token @@ -589,13 +589,13 @@ export const succeed: { * @since 4.0.0 */ export const fail: { - (options: { + (options: { readonly token: Token readonly error: Error["Type"] }): ( self: DurableDeferred ) => Effect.Effect - ( + ( self: DurableDeferred, options: { readonly token: Token @@ -604,7 +604,7 @@ export const fail: { ): Effect.Effect } = dual( 2, - ( + ( self: DurableDeferred, options: { readonly token: Token @@ -624,13 +624,13 @@ export const fail: { * @since 4.0.0 */ export const failCause: { - (options: { + (options: { readonly token: Token readonly cause: Cause.Cause }): ( self: DurableDeferred ) => Effect.Effect - ( + ( self: DurableDeferred, options: { readonly token: Token @@ -639,7 +639,7 @@ export const failCause: { ): Effect.Effect } = dual( 2, - ( + ( self: DurableDeferred, options: { readonly token: Token diff --git a/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts b/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts index 1718693f6..eea4a1f01 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts @@ -511,8 +511,8 @@ export interface CompleteEncoded { * @since 4.0.0 */ export interface CompleteSchema< - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint > extends Schema.declareConstructor< Complete, @@ -545,7 +545,7 @@ export class Complete extends Data.TaggedClass("Complete")<{ * * @since 4.0.0 */ - static Schema(options: { + static Schema(options: { readonly success: Success readonly error: Error }): CompleteSchema { @@ -624,8 +624,8 @@ export class Suspended extends Schema.Class( * @since 4.0.0 */ export const Result = < - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint >(options: { readonly success: Success readonly error: Error diff --git a/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts b/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts index 76fa51a69..0c0a1265c 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts @@ -144,8 +144,8 @@ export class WorkflowEngine extends Context.Service< * Execute an activity from a workflow. */ readonly activityExecute: < - Success extends Schema.Top, - Error extends Schema.Top, + Success extends Schema.Constraint, + Error extends Schema.Constraint, R >( activity: Activity.Activity, @@ -163,8 +163,8 @@ export class WorkflowEngine extends Context.Service< * Try to retrieve the result of an DurableDeferred */ readonly deferredResult: < - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint >( deferred: DurableDeferred.DurableDeferred ) => Effect.Effect< @@ -178,8 +178,8 @@ export class WorkflowEngine extends Context.Service< * workflows. */ readonly deferredDone: < - Success extends Schema.Top, - Error extends Schema.Top + Success extends Schema.Constraint, + Error extends Schema.Constraint >( deferred: DurableDeferred.DurableDeferred, options: { @@ -469,8 +469,8 @@ export const makeUnsafe = (options: Encoded): WorkflowEngine["Service"] => interruptUnsafe: options.interruptUnsafe, resume: options.resume, activityExecute: Effect.fnUntraced(function*< - Success extends Schema.Top, - Error extends Schema.Top, + Success extends Schema.Constraint, + Error extends Schema.Constraint, R >(activity: Activity.Activity, attempt: number) { const result = yield* options.activityExecute(activity, attempt) @@ -483,7 +483,7 @@ export const makeUnsafe = (options: Encoded): WorkflowEngine["Service"] => return new Workflow.Complete({ exit }) }), deferredResult: Effect.fnUntraced( - function*( + function*( deferred: DurableDeferred.DurableDeferred ) { const instance = yield* WorkflowInstance @@ -509,7 +509,7 @@ export const makeUnsafe = (options: Encoded): WorkflowEngine["Service"] => ) ), deferredDone: Effect.fnUntraced( - function*( + function*( deferred: DurableDeferred.DurableDeferred, opts: { readonly workflowName: string diff --git a/.context/effect/packages/effect/test/Config.test.ts b/.context/effect/packages/effect/test/Config.test.ts index 54919a0bd..5c0174681 100644 --- a/.context/effect/packages/effect/test/Config.test.ts +++ b/.context/effect/packages/effect/test/Config.test.ts @@ -376,6 +376,60 @@ describe("Config", () => { at ["a"]` ) }) + + it("array", async () => { + const config = Config.schema(Schema.Array(Schema.String), "a").pipe(Config.withDefault(["default"])) + + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "value" } }), ["value"]) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "" } }), []) + await assertSuccess(config, ConfigProvider.fromEnv({ env: {} }), ["default"]) + }) + + it("schema containers", async () => { + const provider = ConfigProvider.fromEnv({ env: {} }) + + await assertSuccess( + Config.schema(Schema.Struct({ value: Schema.String }), "a").pipe(Config.withDefault({ value: "default" })), + provider, + { value: "default" } + ) + await assertSuccess( + Config.schema(Schema.Struct({ value: Schema.optionalKey(Schema.String) }), "a").pipe( + Config.withDefault({ value: "default" }) + ), + provider, + { value: "default" } + ) + await assertSuccess( + Config.schema(Schema.Struct({}), "a").pipe(Config.withDefault({ value: "default" })), + provider, + { value: "default" } + ) + await assertSuccess( + Config.schema(Schema.Record(Schema.String, Schema.String), "a").pipe( + Config.withDefault({ value: "default" }) + ), + provider, + { value: "default" } + ) + await assertSuccess( + Config.schema(Schema.Tuple([Schema.String]), "a").pipe(Config.withDefault(["default"])), + provider, + ["default"] + ) + await assertSuccess( + Config.schema(Schema.ReadonlySet(Schema.String), "a").pipe(Config.withDefault(new Set(["default"]))), + provider, + new Set(["default"]) + ) + await assertSuccess( + Config.schema(Schema.ReadonlyMap(Schema.String, Schema.String), "a").pipe( + Config.withDefault(new Map([["default", "value"]])) + ), + provider, + new Map([["default", "value"]]) + ) + }) }) describe("option", () => { @@ -930,6 +984,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "abc" await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1" } }), { a: [1] }) await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1", a_1: "2" } }), { a: [1, 2] }) await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", a_0: "2" } }), { a: [1] }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: {} }), + `Missing key + at ["a"]` + ) }) }) diff --git a/.context/effect/packages/effect/test/ConfigProvider.test.ts b/.context/effect/packages/effect/test/ConfigProvider.test.ts index 940b00b5b..cc22ff2c3 100644 --- a/.context/effect/packages/effect/test/ConfigProvider.test.ts +++ b/.context/effect/packages/effect/test/ConfigProvider.test.ts @@ -107,6 +107,17 @@ describe("ConfigProvider", () => { await assertSuccess(provider, ["constant.case"], ConfigProvider.makeValue("value1")) }) + it("constantCase uses config casing for numeric word groups", async () => { + const provider = ConfigProvider.constantCase(ConfigProvider.fromEnv({ + env: { + "API_V2_XML": "value1", + "FIELD2_VALUE": "value2" + } + })) + await assertSuccess(provider, ["api-v2 xml"], ConfigProvider.makeValue("value1")) + await assertSuccess(provider, ["field2Value"], ConfigProvider.makeValue("value2")) + }) + describe("mapInput", () => { it("two mappings", async () => { const appendA = ConfigProvider.mapInput((path) => path.map((sn) => typeof sn === "string" ? sn + "_A" : sn)) diff --git a/.context/effect/packages/effect/test/Cron.test.ts b/.context/effect/packages/effect/test/Cron.test.ts index 55a69a830..ba553c9f4 100644 --- a/.context/effect/packages/effect/test/Cron.test.ts +++ b/.context/effect/packages/effect/test/Cron.test.ts @@ -169,6 +169,21 @@ describe("Cron", () => { deepStrictEqual(next(Cron.parseUnsafe("5 0 8 2 *", london), after), DateTime.toDateUtc(amsterdamTime)) }) + it("next does not skip earlier days when the upcoming day is missing from the month", () => { + const tz = DateTime.zoneMakeNamedUnsafe("UTC") + const cron = Cron.parseUnsafe("0 0 1,16,31 * *", tz) + // From Feb 18 the next matching day in February would be the 31st, which does + // not exist. Rolling onto it must not overshoot past March 1 (also a match). + deepStrictEqual(next(cron, new Date("2020-02-18T00:00:00.000Z")), new Date("2020-03-01T00:00:00.000Z")) + // `*/15` expands to days [1, 16, 31] and exhibits the same wrap. + deepStrictEqual( + next(Cron.parseUnsafe("0 0 */15 * *", tz), new Date("2020-02-18T00:00:00.000Z")), + new Date("2020-03-01T00:00:00.000Z") + ) + // 30-day month: from the 20th the next day is the 31st (missing) -> July 1. + deepStrictEqual(next(cron, new Date("2024-06-20T00:00:00.000Z")), new Date("2024-07-01T00:00:00.000Z")) + }) + it("prev", () => { const utc = DateTime.zoneMakeNamedUnsafe("UTC") const before = new Date("2024-01-04T16:21:00Z") diff --git a/.context/effect/packages/effect/test/Effect.test.ts b/.context/effect/packages/effect/test/Effect.test.ts index f1c8af2be..9a0372dad 100644 --- a/.context/effect/packages/effect/test/Effect.test.ts +++ b/.context/effect/packages/effect/test/Effect.test.ts @@ -175,6 +175,23 @@ describe("Effect", () => { Effect.tap((error) => Effect.sync(() => assert.ok(error instanceof Cause.NoSuchElementError))), Effect.runPromise )) + + it.effect("from a none with a custom error", () => + Effect.gen(function*() { + const error = new Error("Missing value") + const cause = yield* Effect.fromOption(Option.none(), () => error).pipe(Effect.flip) + assert.strictEqual(cause, error) + })) + + it.effect("from a none with a custom error data-last", () => + Effect.gen(function*() { + const error = new Error("Missing value") + const cause = yield* Option.none().pipe( + Effect.fromOption(() => error), + Effect.flip + ) + assert.strictEqual(cause, error) + })) }) describe("transposeOption", () => { diff --git a/.context/effect/packages/effect/test/Latch.test.ts b/.context/effect/packages/effect/test/Latch.test.ts index 11e54889a..e59b4d810 100644 --- a/.context/effect/packages/effect/test/Latch.test.ts +++ b/.context/effect/packages/effect/test/Latch.test.ts @@ -18,4 +18,16 @@ describe("Latch", () => { assert.isDefined(waiter.pollUnsafe()) })) + + it.effect("isOpen reflects the state of the latch", () => + Effect.gen(function*() { + const latch = yield* Latch.make(false) + + assert.isFalse(latch.isOpen()) + + yield* latch.open + yield* Effect.yieldNow + + assert.isTrue(latch.isOpen()) + })) }) diff --git a/.context/effect/packages/effect/test/Request.test.ts b/.context/effect/packages/effect/test/Request.test.ts index b0b4508bc..58c9b177b 100644 --- a/.context/effect/packages/effect/test/Request.test.ts +++ b/.context/effect/packages/effect/test/Request.test.ts @@ -1,5 +1,5 @@ import { assert, describe, expect, it } from "@effect/vitest" -import { Array, Context, Data, Fiber } from "effect" +import { Array, Context, Data, Equal, Fiber, Hash } from "effect" import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" @@ -133,6 +133,16 @@ const provideEnv = flow( ) describe.sequential("Request", () => { + it("compares StructuralProto values when hashes collide", () => { + class Req extends Request.Class<{ id: string; account: string }, string> {} + + const a = new Req({ id: "id-8", account: "acct-2811" }) + const b = new Req({ id: "id-14", account: "acct-755" }) + + assert.strictEqual(Hash.hash(a), Hash.hash(b)) + assert.strictEqual(Equal.equals(a, b), false) + }) + it.effect( "requests are executed correctly", Effect.fnUntraced(function*() { @@ -212,6 +222,34 @@ describe.sequential("Request", () => { ) ) + it.effect( + "grouped requests can be interrupted before execution", + Effect.fnUntraced(function*() { + let resolverExecuted = false + + const resolver = Resolver.make(Effect.fnUntraced(function*(entries) { + resolverExecuted = true + for (const entry of entries) { + entry.completeUnsafe(Exit.succeed(userNames.get(entry.request.id)!)) + } + })).pipe( + Resolver.grouped(({ request }) => request.id), + Resolver.setDelayEffect(Effect.never) + ) + + const fiber = yield* Effect.forkChild(Effect.request(new GetNameById({ id: userIds[0] }), resolver)) + yield* Effect.yieldNow + yield* Fiber.interrupt(fiber) + const exit = yield* Fiber.await(fiber) + + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assert.strictEqual(Cause.hasInterruptsOnly(exit.cause), true) + } + assert.strictEqual(resolverExecuted, false) + }) + ) + it.effect( "batching preserves individual & identical requests", Effect.fnUntraced(function*() { diff --git a/.context/effect/packages/effect/test/Schedule.test.ts b/.context/effect/packages/effect/test/Schedule.test.ts index bce3bc0d9..c0e32db17 100644 --- a/.context/effect/packages/effect/test/Schedule.test.ts +++ b/.context/effect/packages/effect/test/Schedule.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "@effect/vitest" -import { Array, Duration, Effect, Fiber, Pull, Random, Schedule } from "effect" +import { assert, describe, expect, it } from "@effect/vitest" +import { Array, Duration, Effect, Fiber, Pull, Random, Result, Schedule } from "effect" import { constant, constUndefined } from "effect/Function" import { TestClock } from "effect/testing" @@ -79,7 +79,7 @@ describe("Schedule", () => { ]) })) - it.effect("andThenResult - executes schedules sequentially to completion", () => + it.effect("andThenResult - sequences self then other when collecting delays", () => Effect.gen(function*() { const left = Schedule.fixed("500 millis").pipe( Schedule.while(({ attempt }) => Effect.succeed(attempt <= 3)) @@ -98,7 +98,7 @@ describe("Schedule", () => { ]) })) - it.effect("andThenResult - emits right completion when right schedule is finite", () => + it.effect("andThenResult - includes finite other completion when collecting delays", () => Effect.gen(function*() { const left = Schedule.fixed("500 millis").pipe( Schedule.while(({ attempt }) => Effect.succeed(attempt <= 2)) @@ -114,6 +114,23 @@ describe("Schedule", () => { Duration.zero ]) })) + + it.effect("andThenResult - wraps self outputs as Failure and other outputs as Success", () => + Effect.gen(function*() { + const left = Schedule.identity().pipe(Schedule.take(2)) + const right = Schedule.identity() + const step = yield* Schedule.toStep(Schedule.andThenResult(left, right)) + + const first = yield* step(0, "left-1") + const second = yield* step(0, "left-2") + const third = yield* step(0, "right-1") + + assert.deepStrictEqual([first, second, third], [ + [Result.fail("left-1"), Duration.zero], + [Result.fail("left-2"), Duration.zero], + [Result.succeed("right-1"), Duration.zero] + ]) + })) }) describe("cron", () => { diff --git a/.context/effect/packages/effect/test/String.test.ts b/.context/effect/packages/effect/test/String.test.ts index b3b063afa..b0612ba9d 100644 --- a/.context/effect/packages/effect/test/String.test.ts +++ b/.context/effect/packages/effect/test/String.test.ts @@ -638,6 +638,11 @@ describe("String", () => { strictEqual(S.noCase("hello_world"), "hello world") strictEqual(S.noCase("hello-world"), "hello world") }) + + it("splits digit-letter boundaries", () => { + strictEqual(S.noCase("field2value"), "field 2 value") + strictEqual(S.noCase("field2Value"), "field 2 value") + }) }) describe("pascalCase", () => { @@ -646,6 +651,11 @@ describe("String", () => { strictEqual(S.pascalCase("hello_world"), "HelloWorld") strictEqual(S.pascalCase("helloWorld"), "HelloWorld") }) + + it("does not prefix numeric segments with underscores", () => { + strictEqual(S.pascalCase("foo 2 bar"), "Foo2Bar") + strictEqual(S.pascalCase("api-v2 xml"), "ApiV2Xml") + }) }) describe("camelCase", () => { @@ -654,12 +664,27 @@ describe("String", () => { strictEqual(S.camelCase("hello_world"), "helloWorld") strictEqual(S.camelCase("HelloWorld"), "helloWorld") }) + + it("does not prefix numeric segments with underscores", () => { + strictEqual(S.camelCase("foo 2 bar"), "foo2Bar") + strictEqual(S.camelCase("api-v2 xml"), "apiV2Xml") + }) }) describe("constantCase", () => { it("converts to CONSTANT_CASE", () => { strictEqual(S.constantCase("hello world"), "HELLO_WORLD") strictEqual(S.constantCase("helloWorld"), "HELLO_WORLD") + strictEqual(S.constantCase("api-v2 xml"), "API_V_2_XML") + }) + }) + + describe("configCase", () => { + it("converts to CONFIG_CASE", () => { + strictEqual(S.configCase("hello world"), "HELLO_WORLD") + strictEqual(S.configCase("helloWorld"), "HELLO_WORLD") + strictEqual(S.configCase("api-v2 xml"), "API_V2_XML") + strictEqual(S.configCase("field2Value"), "FIELD2_VALUE") }) }) diff --git a/.context/effect/packages/effect/test/rpc/RpcClient.test.ts b/.context/effect/packages/effect/test/rpc/RpcClient.test.ts index 546844b28..40338d361 100644 --- a/.context/effect/packages/effect/test/rpc/RpcClient.test.ts +++ b/.context/effect/packages/effect/test/rpc/RpcClient.test.ts @@ -1,12 +1,13 @@ import { assert, describe, it } from "@effect/vitest" -import { Cause, Effect, Layer, Schema } from "effect" +import { Cause, Effect, Layer, Schema, Stream } from "effect" import * as HttpClient from "effect/unstable/http/HttpClient" import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" -import { Rpc, RpcClient, RpcGroup, RpcSerialization } from "effect/unstable/rpc" +import { Rpc, RpcClient, RpcGroup, RpcMessage, RpcSchema, RpcSerialization } from "effect/unstable/rpc" import { RpcClientError } from "effect/unstable/rpc/RpcClientError" const TestGroup = RpcGroup.make( - Rpc.make("Ping", { success: Schema.String }) + Rpc.make("Ping", { success: Schema.String }), + Rpc.make("Events", { success: RpcSchema.Stream(Schema.String, Schema.Never) }) ) const makeHttpClient = (body: string): HttpClient.HttpClient => @@ -55,4 +56,28 @@ describe("RpcClient", () => { it.effect("fails request on empty HTTP response for framed serialization", () => assertEmptyResponseFailsRequest(RpcSerialization.layerNdjson, "")) + + it.effect("defects request when framed HTTP response closes before request completes", () => + Effect.gen(function*() { + const client = yield* RpcClient.make(TestGroup, { + generateRequestId: () => RpcMessage.RequestId(BigInt(0)) + }).pipe( + Effect.provide(makeProtocolLayer( + RpcSerialization.layerNdjson, + JSON.stringify({ _tag: "Chunk", requestId: "0", values: ["event"] }) + "\n" + )) + ) + + const cause = yield* client.Events().pipe( + Stream.runDrain, + Effect.timeout("1 second"), + Effect.sandbox, + Effect.flip + ) + + const error = Cause.squash(cause) + assert.instanceOf(error, RpcClientError) + assert.strictEqual(error.reason._tag, "RpcClientDefect") + assert.strictEqual(error.reason.message, "HTTP response ended before RPC request completed") + })) }) diff --git a/.context/effect/packages/effect/test/schema/Schema.test.ts b/.context/effect/packages/effect/test/schema/Schema.test.ts index f01f93c0b..9d128e809 100644 --- a/.context/effect/packages/effect/test/schema/Schema.test.ts +++ b/.context/effect/packages/effect/test/schema/Schema.test.ts @@ -111,6 +111,31 @@ Expected an integer, got -1.2` at ["b"]["c"]` ) }) + + it("should not read parseOptions from encodingChecks", async () => { + const schema = Schema.Struct({ + a: Schema.String, + b: Schema.String + }).pipe( + Schema.flip, + Schema.check(Schema.isMaxProperties(1)), + Schema.annotate({ parseOptions: { errors: "first" } }), + Schema.flip + ) + assertTrue(SchemaAST.isObjects(schema.ast)) + strictEqual(schema.ast.checks, undefined) + strictEqual(schema.ast.encodingChecks?.length, 1) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) + await decoding.fail( + {}, + `Missing key + at ["a"] +Missing key + at ["b"]` + ) + }) }) describe("parse options", () => { @@ -409,17 +434,28 @@ Expected an integer, got -1.2` const schema = Schema.Void const asserts = new TestSchema.Asserts(schema) + // The public make input stays typed as void; these callbacks exercise runtime parser behavior. + let fn: () => void + const make = asserts.make() + await make.succeed() await make.succeed(undefined) - await make.fail(null, `Expected void, got null`) + fn = () => undefined + await make.succeed(fn()) + fn = () => null + await make.succeed(fn(), undefined) + fn = () => "a" + await make.succeed(fn(), undefined) const decoding = asserts.decoding() await decoding.succeed(undefined) - await decoding.fail(null, `Expected void, got null`) + await decoding.succeed(null, undefined) + await decoding.succeed("a", undefined) const encoding = asserts.encoding() await encoding.succeed(undefined) - await encoding.fail("1", `Expected void, got "1"`) + await encoding.succeed(null, undefined) + await encoding.succeed("1", undefined) }) it("ObjectKeyword", async () => { @@ -2441,6 +2477,182 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed("123", 123) }) + it("Struct & flip & check & flip should apply the check to the encoded side", async () => { + const schema = Schema.Struct({ a: Schema.String }).pipe( + Schema.flip, + Schema.check(Schema.makeFilter((o) => o.a.length > 1, { expected: "a length > 1" })), + Schema.flip + ) + assertTrue(SchemaAST.isObjects(schema.ast)) + strictEqual(schema.ast.checks, undefined) + strictEqual(schema.ast.encodingChecks?.length, 1) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail( + { a: "a" }, + `Expected a length > 1, got {"a":"a"}` + ) + await decoding.succeed({ a: "aa" }) + + const encoding = asserts.encoding() + await encoding.fail( + { a: "a" }, + `Expected a length > 1, got {"a":"a"}` + ) + await encoding.succeed({ a: "aa" }) + }) + + it("Tuple & flip & check & flip should apply the check to the encoded side", async () => { + const schema = Schema.Tuple([Schema.String]).pipe( + Schema.flip, + Schema.check(Schema.makeFilter((tuple) => tuple[0].length > 1, { expected: "head length > 1" })), + Schema.flip + ) + assertTrue(SchemaAST.isArrays(schema.ast)) + strictEqual(schema.ast.checks, undefined) + strictEqual(schema.ast.encodingChecks?.length, 1) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail( + ["a"], + `Expected head length > 1, got ["a"]` + ) + await decoding.succeed(["aa"]) + + const encoding = asserts.encoding() + await encoding.fail( + ["a"], + `Expected head length > 1, got ["a"]` + ) + await encoding.succeed(["aa"]) + }) + + it("Union & flip & check & flip should apply the check to the encoded side", async () => { + const schema = Schema.Union([Schema.Literal("a"), Schema.Literal("aa")]).pipe( + Schema.flip, + Schema.check(Schema.makeFilter((s) => s === "aa", { expected: `"aa"` })), + Schema.flip + ) + assertTrue(SchemaAST.isUnion(schema.ast)) + strictEqual(schema.ast.checks, undefined) + strictEqual(schema.ast.encodingChecks?.length, 1) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail( + "a", + `Expected "aa", got "a"` + ) + await decoding.succeed("aa") + + const encoding = asserts.encoding() + await encoding.fail( + "a", + `Expected "aa", got "a"` + ) + await encoding.succeed("aa") + }) + + it("Declaration & flip & check & flip should apply the check to the encoded side", async () => { + const schema = Schema.declare( + (u): u is string => typeof u === "string", + { expected: "string declaration" } + ).pipe( + Schema.flip, + Schema.check(Schema.makeFilter((s) => s.length > 1, { expected: "a length > 1" })), + Schema.flip + ) + assertTrue(SchemaAST.isDeclaration(schema.ast)) + strictEqual(schema.ast.checks, undefined) + strictEqual(schema.ast.encodingChecks?.length, 1) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail( + "a", + `Expected a length > 1, got "a"` + ) + await decoding.succeed("aa") + + const encoding = asserts.encoding() + await encoding.fail( + "a", + `Expected a length > 1, got "a"` + ) + await encoding.succeed("aa") + }) + + it("Struct & flip & check & flip with encoding chain should check the local value", async () => { + const local = Schema.Struct({ a: Schema.String }).pipe( + Schema.flip, + Schema.check(Schema.makeFilter((o) => typeof o.a === "string" && o.a.length > 1, { + expected: "a length > 1" + })), + Schema.flip + ) + const schema = Schema.Struct({ b: Schema.String }).pipe( + Schema.decodeTo(local, { + decode: SchemaGetter.transform<{ readonly a: string }, { readonly b: string }>((o) => ({ a: o.b })), + encode: SchemaGetter.transform<{ readonly b: string }, { readonly a: string }>((o) => ({ b: o.a })) + }) + ) + assertTrue(SchemaAST.isObjects(schema.ast)) + strictEqual(schema.ast.encoding?.length, 1) + strictEqual(schema.ast.encodingChecks?.length, 1) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail( + { b: "a" }, + `Expected a length > 1, got {"a":"a"}` + ) + await decoding.succeed({ b: "aa" }, { a: "aa" }) + + const encoding = asserts.encoding() + await encoding.fail( + { a: "a" }, + `Expected a length > 1, got {"a":"a"}` + ) + await encoding.succeed({ a: "aa" }, { b: "aa" }) + }) + + it(`Struct & encoding chain & structural checks should check the local value with errors: "all"`, async () => { + const local = Schema.Struct({ a: Schema.Finite }).check(Schema.isMaxProperties(1)) + const schema = Schema.Struct({ b: Schema.Number, c: Schema.String }).pipe( + Schema.decodeTo(local, { + decode: SchemaGetter.transform< + { readonly a: number }, + { readonly b: number; readonly c: string } + >((o) => ({ a: o.b })), + encode: SchemaGetter.transform< + { readonly b: number; readonly c: string }, + { readonly a: number } + >((o) => ({ b: o.a, c: "" })) + }) + ) + assertTrue(SchemaAST.isObjects(schema.ast)) + strictEqual(schema.ast.encoding?.length, 1) + strictEqual(schema.ast.checks?.length, 1) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) + await decoding.fail( + { b: NaN, c: "extra" }, + `Expected a finite number, got NaN + at ["a"]` + ) + + const encoding = asserts.encoding({ parseOptions: { errors: "all" } }) + await encoding.fail( + { a: NaN }, + `Expected a finite number, got NaN + at ["a"]` + ) + await encoding.succeed({ a: 1 }, { b: 1, c: "" }) + }) + it("should work with withConstructorDefault", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) @@ -3808,6 +4020,27 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail(null, `Expected string, got null`) }) + it(`Void`, async () => { + const schema = Schema.Union([Schema.Void]) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.succeed(undefined) + await decoding.succeed(null, undefined) + await decoding.succeed("a", undefined) + await decoding.succeed(1, undefined) + await decoding.succeed({}, undefined) + await decoding.succeed([], undefined) + }) + + it(`Void | String`, async () => { + const schema = Schema.Union([Schema.Void, Schema.String]) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.succeed("a", undefined) + }) + it(`String | Number`, async () => { const schema = Schema.Union([Schema.String, Schema.Number]) const asserts = new TestSchema.Asserts(schema) @@ -3868,6 +4101,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) }) + it(`mode: "oneOf" with Void`, async () => { + const schema = Schema.Union([Schema.Void, Schema.String], { mode: "oneOf" }) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail("a", `Expected exactly one member to match the input "a"`) + }) + it("{} & Literal", async () => { const schema = Schema.Union([ Schema.Struct({}), @@ -6029,6 +6270,42 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) }) + it("constructor ignores excess properties by default", () => { + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + + const instance = new A({ a: "a", extra: "extra" } as any) + + strictEqual(instance.a, "a") + assertFalse("extra" in instance) + }) + + it("constructor preserves excess properties when requested", () => { + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + + const instance = new A({ a: "a", extra: "extra" } as any, { + parseOptions: { onExcessProperty: "preserve" } + }) + + strictEqual(instance.a, "a") + strictEqual((instance as any).extra, "extra") + }) + + it("constructor rejects excess properties when requested", () => { + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + + throws(() => + new A({ a: "a", extra: "extra" } as any, { + parseOptions: { onExcessProperty: "error" } + }) + ) + }) + it("annotate", async () => { class A extends Schema.Class("A")({ a: Schema.String @@ -6078,6 +6355,60 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 })) }) + it("constructor preserves subclass fields while ignoring excess properties by default", () => { + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + class B extends A.extend("B")({ + b: Schema.Number + }) {} + + const instance = new B({ a: "a", b: 2, extra: "extra" } as any) + + strictEqual(instance.a, "a") + strictEqual(instance.b, 2) + assertFalse("extra" in instance) + }) + + it("constructor preserves subclass fields and excess properties when requested", () => { + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + class B extends A.extend("B")({ + b: Schema.Number + }) {} + + const instance = new B({ a: "a", b: 2, extra: "extra" } as any, { + parseOptions: { onExcessProperty: "preserve" } + }) + + strictEqual(instance.a, "a") + strictEqual(instance.b, 2) + strictEqual((instance as any).extra, "extra") + }) + + it("constructor does not treat subclass fields as excess properties", () => { + class A extends Schema.Class("A")({ + a: Schema.String + }) {} + class B extends A.extend("B")({ + b: Schema.Number + }) {} + + const instance = new B({ a: "a", b: 2 }, { + parseOptions: { onExcessProperty: "error" } + }) + + strictEqual(instance.a, "a") + strictEqual(instance.b, 2) + + throws(() => + new B({ a: "a", b: 2, extra: "extra" } as any, { + parseOptions: { onExcessProperty: "error" } + }) + ) + }) + it("Struct argument", async () => { class A extends Schema.Class("A")( Schema.Struct({ @@ -6152,6 +6483,18 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(new A({ a: "a" }), { _tag: "A", a: "a" }) }) + it("constructor ignores excess properties by default", () => { + class A extends Schema.TaggedClass()("A", { + a: Schema.String + }) {} + + const instance = new A({ a: "a", extra: "extra" } as any) + + strictEqual(instance._tag, "A") + strictEqual(instance.a, "a") + assertFalse("extra" in instance) + }) + it("Struct argument", async () => { class A extends Schema.TaggedClass()( "A", @@ -6175,6 +6518,36 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) await decoding.fail({ _tag: "A", a: "" }, `Expected "a" being longer than 0, got {"_tag":"A","a":""}`) }) + + it("extended constructor does not treat subclass fields as excess properties", () => { + class A extends Schema.TaggedClass()("A", { + a: Schema.String + }) {} + class B extends A.extend("B")({ + b: Schema.Number + }) {} + + const instance = new B({ a: "a", b: 2, extra: "extra" } as any) + + strictEqual(instance._tag, "A") + strictEqual(instance.a, "a") + strictEqual(instance.b, 2) + assertFalse("extra" in instance) + + const strictInstance = new B({ a: "a", b: 2 }, { + parseOptions: { onExcessProperty: "error" } + }) + + strictEqual(strictInstance._tag, "A") + strictEqual(strictInstance.a, "a") + strictEqual(strictInstance.b, 2) + + throws(() => + new B({ a: "a", b: 2, extra: "extra" } as any, { + parseOptions: { onExcessProperty: "error" } + }) + ) + }) }) describe("ErrorClass", () => { @@ -6202,6 +6575,37 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await make.succeed({ id: 1 }, new E({ id: 1 })) }) + it("constructor ignores excess properties by default", () => { + class E extends Schema.ErrorClass("E")({ + message: Schema.String, + cause: Schema.optionalKey(Schema.Unknown), + code: Schema.Number + }) {} + const cause = new Error("cause") + + const err = new E({ message: "boom", cause, code: 1, extra: "extra" } as any) + + strictEqual(err.message, "boom") + strictEqual(err.cause, cause) + strictEqual(err.code, 1) + assertFalse("extra" in err) + }) + + it("constructor preserves excess properties when requested", () => { + class E extends Schema.ErrorClass("E")({ + message: Schema.String, + code: Schema.Number + }) {} + + const err = new E({ message: "boom", code: 1, extra: "extra" } as any, { + parseOptions: { onExcessProperty: "preserve" } + }) + + strictEqual(err.message, "boom") + strictEqual(err.code, 1) + strictEqual((err as any).extra, "extra") + }) + it("Struct argument", async () => { class E extends Schema.ErrorClass("E")(Schema.Struct({ id: Schema.Number @@ -6259,6 +6663,37 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 })) }) + it("extended constructor ignores excess properties by default", () => { + class A extends Schema.ErrorClass("A")({ + message: Schema.String + }) {} + class B extends A.extend("B")({ + code: Schema.Number + }) {} + + const err = new B({ message: "boom", code: 1, extra: "extra" } as any) + + strictEqual(err.message, "boom") + strictEqual(err.code, 1) + assertFalse("extra" in err) + }) + + it("extended constructor does not treat subclass fields as excess properties", () => { + class A extends Schema.ErrorClass("A")({ + message: Schema.String + }) {} + class B extends A.extend("B")({ + code: Schema.Number + }) {} + + const err = new B({ message: "boom", code: 1 }, { + parseOptions: { onExcessProperty: "error" } + }) + + strictEqual(err.message, "boom") + strictEqual(err.code, 1) + }) + it("`toString` to match native `Error` output format", async () => { class E extends Schema.ErrorClass("E")({ message: Schema.String @@ -6298,6 +6733,18 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(new E({ id: 1 }), { _tag: "E", id: 1 }) }) + it("constructor ignores excess properties by default", () => { + class E extends Schema.TaggedErrorClass()("E", { + id: Schema.Number + }) {} + + const err = new E({ id: 1, extra: "extra" } as any) + + strictEqual(err._tag, "E") + strictEqual(err.id, 1) + assertFalse("extra" in err) + }) + it("Struct argument", async () => { class E extends Schema.TaggedErrorClass()( "E", @@ -6368,6 +6815,39 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` assertTrue(instance instanceof A) assertTrue(instance instanceof B) }) + + it("extended constructor ignores excess properties by default", () => { + class A extends Schema.TaggedErrorClass()("A", { + a: Schema.String + }) {} + class B extends A.extend("B")({ + b: Schema.Number + }) {} + + const instance = new B({ a: "a", b: 2, extra: "extra" } as any) + + strictEqual(instance._tag, "A") + strictEqual(instance.a, "a") + strictEqual(instance.b, 2) + assertFalse("extra" in instance) + }) + + it("extended constructor does not treat subclass fields as excess properties", () => { + class A extends Schema.TaggedErrorClass()("A", { + a: Schema.String + }) {} + class B extends A.extend("B")({ + b: Schema.Number + }) {} + + const instance = new B({ a: "a", b: 2 }, { + parseOptions: { onExcessProperty: "error" } + }) + + strictEqual(instance._tag, "A") + strictEqual(instance.a, "a") + strictEqual(instance.b, 2) + }) }) describe("Enum", () => { diff --git a/.context/effect/packages/effect/test/schema/SchemaAST.test.ts b/.context/effect/packages/effect/test/schema/SchemaAST.test.ts index 6ef883789..5ee3eda3c 100644 --- a/.context/effect/packages/effect/test/schema/SchemaAST.test.ts +++ b/.context/effect/packages/effect/test/schema/SchemaAST.test.ts @@ -68,6 +68,36 @@ describe("SchemaAST", () => { strictEqual(SchemaAST.isStringTree(circular), false) }) + describe("toType", () => { + it("promotes encodingChecks when contained type shape is preserved", () => { + const schema = Schema.Struct({ a: Schema.String }).pipe( + Schema.flip, + Schema.check(Schema.makeFilter((o) => o.a.length > 1)), + Schema.flip + ) + + const ast = SchemaAST.toType(schema.ast) + + strictEqual(SchemaAST.isObjects(ast), true) + strictEqual(ast.checks?.length, 1) + strictEqual(ast.encodingChecks, undefined) + }) + + it("drops encodingChecks when contained type shape changes", () => { + const schema = Schema.Struct({ a: Schema.FiniteFromString }).pipe( + Schema.flip, + Schema.check(Schema.makeFilter((o) => o.a.length > 1)), + Schema.flip + ) + + const ast = SchemaAST.toType(schema.ast) + + strictEqual(SchemaAST.isObjects(ast), true) + strictEqual(ast.checks, undefined) + strictEqual(ast.encodingChecks, undefined) + }) + }) + describe("collectSentinels", () => { describe("Declaration", () => { it("~sentinels", () => { diff --git a/.context/effect/packages/effect/test/schema/representation/fromASTs.test.ts b/.context/effect/packages/effect/test/schema/representation/fromASTs.test.ts index cbca96e42..47ad4a860 100644 --- a/.context/effect/packages/effect/test/schema/representation/fromASTs.test.ts +++ b/.context/effect/packages/effect/test/schema/representation/fromASTs.test.ts @@ -3,7 +3,7 @@ import { describe, it } from "vitest" import { deepStrictEqual } from "../../utils/assert.ts" describe("fromASTs", () => { - function assertFromASTs(schemas: readonly [Schema.Top, ...Array], expected: { + function assertFromASTs(schemas: readonly [Schema.Constraint, ...Array], expected: { readonly representations: readonly [ SchemaRepresentation.Representation, ...Array @@ -58,7 +58,7 @@ describe("fromASTs", () => { }) describe("fromAST", () => { - function assertFromAST(schema: Schema.Top, expected: { + function assertFromAST(schema: Schema.Constraint, expected: { readonly representation: SchemaRepresentation.Representation readonly references?: SchemaRepresentation.References }) { @@ -246,6 +246,295 @@ describe("fromAST", () => { }) }) + describe("node kinds", () => { + it("primitive nodes", () => { + assertFromAST( + Schema.Tuple([ + Schema.Null, + Schema.Undefined, + Schema.Void, + Schema.Never, + Schema.Unknown, + Schema.Any, + Schema.Boolean, + Schema.BigInt, + Schema.Symbol, + Schema.ObjectKeyword + ]), + { + representation: { + _tag: "Arrays", + elements: [ + { isOptional: false, type: { _tag: "Null" } }, + { isOptional: false, type: { _tag: "Undefined" } }, + { isOptional: false, type: { _tag: "Void" } }, + { isOptional: false, type: { _tag: "Never" } }, + { isOptional: false, type: { _tag: "Unknown" } }, + { isOptional: false, type: { _tag: "Any" } }, + { isOptional: false, type: { _tag: "Boolean" } }, + { isOptional: false, type: { _tag: "BigInt", checks: [] } }, + { isOptional: false, type: { _tag: "Symbol" } }, + { isOptional: false, type: { _tag: "ObjectKeyword" } } + ], + rest: [], + checks: [] + } + } + ) + }) + + it("literal-like nodes", () => { + const symbol = Symbol.for("a") + assertFromAST( + Schema.Tuple([ + Schema.Literal("a"), + Schema.UniqueSymbol(symbol), + Schema.Enum({ A: "a", B: "b", One: 1 }), + Schema.TemplateLiteral(["a", Schema.String, Schema.Number]) + ]), + { + representation: { + _tag: "Arrays", + elements: [ + { isOptional: false, type: { _tag: "Literal", literal: "a" } }, + { isOptional: false, type: { _tag: "UniqueSymbol", symbol } }, + { + isOptional: false, + type: { + _tag: "Enum", + enums: [["A", "a"], ["B", "b"], ["One", 1]] + } + }, + { + isOptional: false, + type: { + _tag: "TemplateLiteral", + parts: [ + { _tag: "Literal", literal: "a" }, + { _tag: "String", checks: [] }, + { _tag: "Number", checks: [] } + ] + } + } + ], + rest: [], + checks: [] + } + } + ) + }) + + it("string content schema", () => { + const document = SchemaRepresentation.fromAST( + Schema.fromJsonString(Schema.Struct({ a: Schema.String })).ast + ) + const representation = document.representation as SchemaRepresentation.String + deepStrictEqual(representation.contentSchema, { + _tag: "Objects", + propertySignatures: [ + { + name: "a", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + } + ], + indexSignatures: [], + checks: [] + }) + deepStrictEqual(representation.annotations?.expected, "a string that will be decoded as JSON") + deepStrictEqual(representation.annotations?.contentMediaType, "application/json") + }) + + it("tuple rest and mutable properties", () => { + assertFromAST( + Schema.Tuple([ + Schema.TupleWithRest(Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)]), [Schema.Boolean]), + Schema.Struct({ a: Schema.mutableKey(Schema.String) }) + ]), + { + representation: { + _tag: "Arrays", + elements: [ + { + isOptional: false, + type: { + _tag: "Arrays", + elements: [ + { isOptional: false, type: { _tag: "String", checks: [] } }, + { isOptional: true, type: { _tag: "Number", checks: [] } } + ], + rest: [{ _tag: "Boolean" }], + checks: [] + } + }, + { + isOptional: false, + type: { + _tag: "Objects", + propertySignatures: [ + { + name: "a", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: true + } + ], + indexSignatures: [], + checks: [] + } + } + ], + rest: [], + checks: [] + } + } + ) + }) + + it("declaration without an encoded schema", () => { + assertFromAST( + Schema.declare((u): u is string => typeof u === "string", { expected: "string declaration" }), + { + representation: { + _tag: "Declaration", + typeParameters: [], + encodedSchema: { _tag: "Null" }, + checks: [], + annotations: { expected: "string declaration" } + } + } + ) + }) + }) + + describe("checks", () => { + it("array and object checks", () => { + assertFromAST( + Schema.Tuple([ + Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isUnique()), + Schema.Record(Schema.String, Schema.Number).check( + Schema.isMinProperties(1), + Schema.isMaxProperties(2) + ) + ]), + { + representation: { + _tag: "Arrays", + elements: [ + { + isOptional: false, + type: { + _tag: "Arrays", + elements: [], + rest: [{ _tag: "String", checks: [] }], + checks: [ + { + _tag: "Filter", + meta: { _tag: "isMinLength", minLength: 1 }, + annotations: { expected: "a value with a length of at least 1" } + }, + { + _tag: "Filter", + meta: { _tag: "isUnique" }, + annotations: { expected: "an array with unique items" } + } + ] + } + }, + { + isOptional: false, + type: { + _tag: "Objects", + propertySignatures: [], + indexSignatures: [ + { + parameter: { _tag: "String", checks: [] }, + type: { _tag: "Number", checks: [] } + } + ], + checks: [ + { + _tag: "Filter", + meta: { _tag: "isMinProperties", minProperties: 1 }, + annotations: { expected: "a value with at least 1 entry" } + }, + { + _tag: "Filter", + meta: { _tag: "isMaxProperties", maxProperties: 2 }, + annotations: { expected: "a value with at most 2 entries" } + } + ] + } + } + ], + rest: [], + checks: [] + } + } + ) + }) + + it("filter groups", () => { + assertFromAST( + Schema.String.check( + Schema.makeFilterGroup([ + Schema.isMinLength(1), + Schema.isMaxLength(2) + ], { description: "range" }) + ), + { + representation: { + _tag: "String", + checks: [ + { + _tag: "FilterGroup", + checks: [ + { + _tag: "Filter", + meta: { _tag: "isMinLength", minLength: 1 }, + annotations: { expected: "a value with a length of at least 1" } + }, + { + _tag: "Filter", + meta: { _tag: "isMaxLength", maxLength: 2 }, + annotations: { expected: "a value with a length of at most 2" } + } + ], + annotations: { description: "range" } + } + ] + } + } + ) + }) + + it("drops checks without representation metadata", () => { + assertFromAST( + Schema.String.check(Schema.makeFilter((s) => s.length > 0, { expected: "custom" })), + { + representation: { + _tag: "String", + checks: [] + } + } + ) + assertFromAST( + Schema.String.check( + Schema.makeFilterGroup([ + Schema.makeFilter((s) => s.length > 0, { expected: "custom" }) + ], { description: "group" }) + ), + { + representation: { + _tag: "String", + checks: [] + } + } + ) + }) + }) + describe("Record", () => { describe("checks", () => { it("isPropertyNames", () => { diff --git a/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts index 28feea270..d10615207 100644 --- a/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts +++ b/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts @@ -35,7 +35,7 @@ describe("toCodeDocument", () => { } function assertSchema(input: { - readonly schema: Schema.Top + readonly schema: Schema.Constraint readonly reviver?: SchemaRepresentation.Reviver | undefined }, expected: Expected) { const multiDocument = SchemaRepresentation.fromASTs([input.schema.ast]) diff --git a/.context/effect/packages/effect/test/schema/toArbitrary.test.ts b/.context/effect/packages/effect/test/schema/toArbitrary.test.ts index d2e321a8a..cb54d4e4a 100644 --- a/.context/effect/packages/effect/test/schema/toArbitrary.test.ts +++ b/.context/effect/packages/effect/test/schema/toArbitrary.test.ts @@ -3,7 +3,7 @@ import { FastCheck, TestSchema } from "effect/testing" import { describe, it } from "vitest" import { assertInclude, assertInstanceOf, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" -function assertUnsupportedSchema(schema: Schema.Top, message: string) { +function assertUnsupportedSchema(schema: Schema.Constraint, message: string) { throws(() => Schema.toArbitrary(schema), message) } @@ -18,11 +18,11 @@ function verifyGeneration boolean, numRuns = 200) { +function assertInvariant(schema: Schema.Constraint, predicate: (value: any) => boolean, numRuns = 200) { FastCheck.assert(FastCheck.property(Schema.toArbitrary(schema), predicate), { numRuns }) } -function assertRecursiveNoFiniteGenerationPath(schema: Schema.Top) { +function assertRecursiveNoFiniteGenerationPath(schema: Schema.Constraint) { throws( () => Schema.toArbitrary(schema), (e) => { @@ -46,7 +46,7 @@ function minSizeOne(size: (a: A) => number) { }) } -function CustomArray( +function CustomArray( value: A, toArbitrary: Schema.Annotations.ToArbitrary.Declaration, readonly [A]> ) { @@ -129,7 +129,7 @@ describe("Arbitrary generation", () => { }) it("should pass the constraint to the override annotation", () => { - let constraint: Schema.Annotations.ToArbitrary.Constraint | undefined + let constraint: Schema.Annotations.ToArbitrary.GenerationConstraint | undefined const schema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })).annotate({ toArbitrary: () => (fc, ctx) => { constraint = ctx.constraint @@ -187,7 +187,7 @@ describe("Arbitrary generation", () => { describe("report and candidates", () => { it("should use filter candidates with the merged constraint context", () => { - let constraint: Schema.Annotations.ToArbitrary.Constraint | undefined + let constraint: Schema.Annotations.ToArbitrary.GenerationConstraint | undefined const schema = Schema.String.check( Schema.isMinLength(9), Schema.makeFilter((s: string) => s === "candidate", { diff --git a/.context/effect/packages/effect/test/schema/toCodec.test.ts b/.context/effect/packages/effect/test/schema/toCodec.test.ts index 1b49cc20e..8db97cdda 100644 --- a/.context/effect/packages/effect/test/schema/toCodec.test.ts +++ b/.context/effect/packages/effect/test/schema/toCodec.test.ts @@ -29,6 +29,12 @@ const FiniteFromDate = Schema.Date.pipe(Schema.decodeTo( describe("Serializers", () => { describe("toCodecJson", () => { + it("exposes the source schema", () => { + const schema = Schema.FiniteFromString + const serializer = Schema.toCodecJson(schema) + strictEqual(serializer.schema, schema) + }) + it("should reorder the types in the Union based on the encoded side", async () => { const schema = Schema.Union([ Schema.String, @@ -1568,6 +1574,12 @@ describe("Serializers", () => { }) describe("toCodecStringTree", () => { + it("exposes the source schema", () => { + const schema = Schema.FiniteFromString + const serializer = Schema.toCodecStringTree(schema) + strictEqual(serializer.schema, schema) + }) + it("should reorder the types in the Union based on the encoded side", async () => { const schema = Schema.Union([ Schema.String, @@ -1583,56 +1595,6 @@ describe("Serializers", () => { await decoding.succeed("1", "1a") }) - describe("keepDeclarations: true", () => { - describe("Unsupported schemas", () => { - it("Struct with Symbol property name", () => { - const a = Symbol.for("a") - const schema = Schema.Struct({ - [a]: Schema.String - }) - throws( - () => Schema.toCodecStringTree(schema, { keepDeclarations: true }), - "Objects property names must be strings" - ) - }) - }) - - it("should reorder the types in the Union based on the encoded side", async () => { - const schema = Schema.Union([ - Schema.String, - Schema.String.pipe(Schema.encodeTo(Schema.BigInt, { - decode: SchemaGetter.transform((n: bigint) => String(n) + "a"), - encode: SchemaGetter.transform(() => 0n) - })) - ]) - const serializer = Schema.toCodecStringTree(schema, { keepDeclarations: true }) - const asserts = new TestSchema.Asserts(Schema.toCodecJson(serializer)) - - const decoding = asserts.decoding() - await decoding.succeed("1", "1a") - }) - - it("should passthrough the schema if it's a declaration without an annotation", async () => { - const schema = Schema.Struct({ - a: Schema.instanceOf(URL), - b: Schema.Number - }) - const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(schema, { keepDeclarations: true })) - - const encoding = asserts.encoding() - await encoding.succeed({ a: new URL("https://effect.website"), b: 1 }, { - a: new URL("https://effect.website"), - b: "1" - }) - - const decoding = asserts.decoding() - await decoding.succeed({ - a: new URL("https://effect.website"), - b: "1" - }, { a: new URL("https://effect.website"), b: 1 }) - }) - }) - describe("should return the same reference if nothing changed", () => { it("String", async () => { const schema = Schema.String @@ -2292,6 +2254,20 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` ) }) + it("Array(Finite) preserves the top-level AST", async () => { + const serializer = Schema.toCodecStringTree(Schema.Array(Schema.Finite)) + strictEqual(serializer.ast._tag, "Arrays") + + const asserts = new TestSchema.Asserts(serializer) + + const encoding = asserts.encoding() + await encoding.succeed([1, 2], ["1", "2"]) + + const decoding = asserts.decoding() + await decoding.fail("1,2", `Expected array, got "1,2"`) + await decoding.succeed(["1", "2"], [1, 2]) + }) + describe("Union", () => { it("NullOr(Date)", async () => { const schema = Schema.NullOr(Schema.String) @@ -2628,11 +2604,79 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const serializer = Schema.toCodecStringTree(schema) const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(serializer)) + const decoding = asserts.decoding() + await decoding.succeed({}) + await decoding.succeed({ a: ["a"] }) + await decoding.fail({ a: "a" }, `Expected array, got "a"\n at ["a"]`) + }) + }) + + describe("toCodecArrayFromSingle", () => { + it("accepts string and array inputs for a top-level array", async () => { + const serializer = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Array(Schema.Finite))) + strictEqual(serializer.ast._tag, "Arrays") + + const asserts = new TestSchema.Asserts(serializer) + + const encoding = asserts.encoding() + await encoding.succeed([1, 2], ["1", "2"]) + await encoding.fail(1 as any, "Expected array, got 1") + + const decoding = asserts.decoding() + await decoding.succeed("1", [1]) + await decoding.fail("1,2", `Expected a string representing a finite number, got "1,2"\n at [0]`) + await decoding.succeed(["1", "2"], [1, 2]) + }) + + it("accepts string and array inputs for required array fields", async () => { + const schema = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Struct({ + a: Schema.Array(Schema.Finite) + }))) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.succeed({ a: "1" }, { a: [1] }) + await decoding.succeed({ a: ["1", "2"] }, { a: [1, 2] }) + }) + + it("accepts string and array inputs for nested optional array fields", async () => { + const schema = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Struct({ + a: Schema.optionalKey(Schema.NonEmptyArray(Schema.String)) + }))) + const asserts = new TestSchema.Asserts(schema) + const decoding = asserts.decoding() await decoding.succeed({}) await decoding.succeed({ a: ["a"] }) await decoding.succeed({ a: "a" }, { a: ["a"] }) }) + + it("accepts string and array inputs for tuples with optional elements", async () => { + const schema = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Tuple([ + Schema.optionalKey(Schema.String) + ]))) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.succeed([]) + await decoding.succeed("a", ["a"]) + await decoding.succeed(["a"]) + }) + + it("applies recursively to nested arrays", async () => { + const schema = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Array(Schema.Array(Schema.Finite)))) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.succeed("1", [[1]]) + await decoding.succeed(["1", "2"], [[1], [2]]) + await decoding.succeed([["1", "2"]], [[1, 2]]) + }) + + it("is idempotent", () => { + const schema = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Array(Schema.Finite))) + strictEqual(schema.ast, Schema.toCodecArrayFromSingle(schema).ast) + }) }) describe("toEncoderXml", () => { diff --git a/.context/effect/packages/effect/test/schema/toFormatter.test.ts b/.context/effect/packages/effect/test/schema/toFormatter.test.ts index 368eaed90..8bdedc69f 100644 --- a/.context/effect/packages/effect/test/schema/toFormatter.test.ts +++ b/.context/effect/packages/effect/test/schema/toFormatter.test.ts @@ -558,7 +558,7 @@ describe("toFormatter", () => { }) it("should allow for ast-level overrides", () => { - const toFormatter = (schema: S) => + const toFormatter = (schema: S) => Schema.toFormatter(schema, { onBefore: (ast) => { if (ast._tag === "Boolean") { diff --git a/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts b/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts index 192c818f3..dc8e145d8 100644 --- a/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts +++ b/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts @@ -17,7 +17,7 @@ const baseAjvOptions: AjvOptions = { const ajvDraft2020_12 = new Ajv2020.default(baseAjvOptions) function assertUnsupportedSchema( - schema: Schema.Top, + schema: Schema.Constraint, message: string, options?: Schema.ToJsonSchemaOptions ) { @@ -3552,6 +3552,81 @@ describe("toJsonSchemaDocument", () => { ) }) + it("preserves the content schema identifier", () => { + const MyEvent = Schema.Struct({ + value: Schema.String + }).annotate({ identifier: "MyEvent" }) + + assertJsonSchemaDocument( + Schema.fromJsonString(MyEvent), + { + schema: { + "$ref": "#/$defs/MyEventJsonString" + }, + definitions: { + "MyEvent": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "MyEventJsonString": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/MyEvent" + } + } + } + } + ) + }) + + it("respects an explicit encoded-side identifier", () => { + const MyEvent = Schema.Struct({ + value: Schema.String + }).annotate({ identifier: "MyEvent" }) + const MyWireEvent = Schema.flip( + Schema.flip(Schema.fromJsonString(MyEvent)).annotate({ identifier: "MyWireEvent" }) + ) + + assertJsonSchemaDocument( + MyWireEvent, + { + schema: { + "$ref": "#/$defs/MyWireEvent" + }, + definitions: { + "MyEvent": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "additionalProperties": false + }, + "MyWireEvent": { + "type": "string", + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/MyEvent" + } + } + } + } + ) + }) + it("nested fromJsonString", () => { assertJsonSchemaDocument( Schema.fromJsonString(Schema.Struct({ diff --git a/.context/effect/packages/effect/test/schema/v3-v4.test.ts b/.context/effect/packages/effect/test/schema/v3-v4.test.ts index 78d06f47c..2e1a6085d 100644 --- a/.context/effect/packages/effect/test/schema/v3-v4.test.ts +++ b/.context/effect/packages/effect/test/schema/v3-v4.test.ts @@ -9,7 +9,7 @@ describe("v3 -> v4 migration tests", () => { // a: Schema.optionalWith(Schema.NumberFromString, { default: () => "default value" }) // }) - function f(schema: S, defaultValue: S["Type"]) { + function f(schema: S, defaultValue: S["Type"]) { return Schema.Struct({ a: Schema.optional(schema).pipe( Schema.decodeTo(Schema.toType(schema), { @@ -48,7 +48,7 @@ describe("v3 -> v4 migration tests", () => { // a: Schema.optionalWith(Schema.NumberFromString, { default: () => "default value", exact: true }) // }) - function f(schema: S, defaultValue: S["Type"]) { + function f(schema: S, defaultValue: S["Type"]) { return Schema.Struct({ a: Schema.optionalKey(schema).pipe( Schema.decodeTo(Schema.toType(schema), { @@ -91,7 +91,7 @@ describe("v3 -> v4 migration tests", () => { // a: Schema.optionalWith(Schema.String, { nullable: true }) // }) - function f(schema: S) { + function f(schema: S) { return Schema.Struct({ a: Schema.optional(Schema.NullOr(schema)).pipe( Schema.decodeTo(Schema.optional(Schema.toType(schema)), { @@ -128,7 +128,7 @@ describe("v3 -> v4 migration tests", () => { // a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, exact: true }) // }) - function f(schema: S) { + function f(schema: S) { return Schema.Struct({ a: Schema.optionalKey(Schema.NullOr(schema)).pipe( Schema.decodeTo(Schema.optionalKey(Schema.toType(schema)), { @@ -168,7 +168,7 @@ describe("v3 -> v4 migration tests", () => { // a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, default: () => "default value" }) // }) - function f(schema: S, defaultValue: () => S["Type"]) { + function f(schema: S, defaultValue: () => S["Type"]) { return Schema.Struct({ a: Schema.optional(Schema.NullOr(schema)).pipe( Schema.decodeTo(Schema.UndefinedOr(Schema.toType(schema)), { @@ -206,7 +206,7 @@ describe("v3 -> v4 migration tests", () => { // a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, default: () => "default value", exact: true }) // }) - function f(schema: S, defaultValue: () => S["Type"]) { + function f(schema: S, defaultValue: () => S["Type"]) { return Schema.Struct({ a: Schema.optionalKey(Schema.NullOr(schema)).pipe( Schema.decodeTo(Schema.toType(schema), { diff --git a/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts b/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts index 14e1b30d7..85be5842f 100644 --- a/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts @@ -4,11 +4,11 @@ import { TestSchema } from "effect/testing" import { toCodecAnthropic } from "effect/unstable/ai/AnthropicStructuredOutput" import * as Tool from "effect/unstable/ai/Tool" -function assertJsonSchema(schema: Schema.Top, expected: JsonSchema.JsonSchema) { +function assertJsonSchema(schema: Schema.Constraint, expected: JsonSchema.JsonSchema) { assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, expected) } -function assertError(schema: Schema.Top, message: string) { +function assertError(schema: Schema.Constraint, message: string) { assert.throws(() => toCodecAnthropic(schema), message) } diff --git a/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts b/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts index e8c7bdf66..6e0ab6e11 100644 --- a/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts @@ -4,11 +4,11 @@ import { TestSchema } from "effect/testing" import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput" import * as Tool from "effect/unstable/ai/Tool" -function assertJsonSchema(schema: Schema.Top, expected: JsonSchema.JsonSchema) { +function assertJsonSchema(schema: Schema.Constraint, expected: JsonSchema.JsonSchema) { assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, expected) } -function assertError(schema: Schema.Top, message: string) { +function assertError(schema: Schema.Constraint, message: string) { assert.throws(() => toCodecOpenAI(schema), message) } diff --git a/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts b/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts index 29b16436b..7314bf375 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts @@ -1,5 +1,5 @@ import { assert, describe, expect, it } from "@effect/vitest" -import { Effect, FileSystem, Layer, Option, Path, PlatformError, Ref, Result } from "effect" +import { Effect, FileSystem, Layer, Option, Path, PlatformError, Ref, Result, Stdio } from "effect" import { TestConsole } from "effect/testing" import { Argument, CliOutput, Command, Flag } from "effect/unstable/cli" import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" @@ -32,6 +32,11 @@ const CliOutputLayer = CliOutput.layer( colors: false }) ) +const StdioLayer = Stdio.layerTest({}) +const ChildProcessSpawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.die("Not implemented")) +) const TestLayer = Layer.mergeAll( ConsoleLayer, @@ -39,7 +44,8 @@ const TestLayer = Layer.mergeAll( PathLayer, TerminalLayer, CliOutputLayer, - Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("Not implemented"))) + StdioLayer, + ChildProcessSpawnerLayer ) describe("Command arguments", () => { diff --git a/.context/effect/packages/effect/test/unstable/cli/Param.test.ts b/.context/effect/packages/effect/test/unstable/cli/Param.test.ts index 42f38f3b7..9e6990074 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Param.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Param.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref } from "effect" +import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref, Stdio } from "effect" import { TestConsole } from "effect/testing" import { Argument, CliError, Flag, Prompt } from "effect/unstable/cli" import { ChildProcessSpawner } from "effect/unstable/process" @@ -9,13 +9,19 @@ const ConsoleLayer = TestConsole.layer const FileSystemLayer = FileSystem.layerNoop({}) const PathLayer = Path.layer const TerminalLayer = MockTerminal.layer +const StdioLayer = Stdio.layerTest({}) +const ChildProcessSpawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.die("Not implemented")) +) const TestLayer = Layer.mergeAll( ConsoleLayer, FileSystemLayer, PathLayer, TerminalLayer, - Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("Not implemented"))) + StdioLayer, + ChildProcessSpawnerLayer ) describe("Param", () => { diff --git a/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts b/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts index ddbc8e9eb..0b83b1ee6 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts @@ -1,13 +1,27 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, FileSystem, Layer, Path, PlatformError, Redacted } from "effect" +import { Effect, FileSystem, Layer, Path, PlatformError, Redacted, Stdio } from "effect" +import { TestConsole } from "effect/testing/index" import { Primitive } from "effect/unstable/cli" +import { ChildProcessSpawner } from "effect/unstable/process" +import * as MockTerminal from "./services/MockTerminal.ts" +const ConsoleLayer = TestConsole.layer const FileSystemLayer = FileSystem.layerNoop({}) const PathLayer = Path.layer +const TerminalLayer = MockTerminal.layer +const StdioLayer = Stdio.layerTest({}) +const ChildProcessSpawnerLayer = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.die("Not implemented")) +) const TestLayer = Layer.mergeAll( + ConsoleLayer, FileSystemLayer, - PathLayer + PathLayer, + TerminalLayer, + StdioLayer, + ChildProcessSpawnerLayer ) // Helper functions to reduce repetition diff --git a/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts index a466b07ce..4660a42cf 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest" import { strictEqual } from "@effect/vitest/utils" -import { Effect, Fiber, Layer, Ref, Stream } from "effect" +import { Clock, Duration, Effect, Fiber, Layer, Ref, Stream } from "effect" import { TestClock } from "effect/testing" import { HttpClient, HttpClientResponse } from "effect/unstable/http" import { RateLimiter } from "effect/unstable/persistence" @@ -302,5 +302,446 @@ describe("HttpClient", () => { strictEqual(response.status, 200) strictEqual(yield* Ref.get(attempts), 2) }).pipe(Effect.provide(RateLimiterTestLayer))) + + it.effect("applies adaptive cooldown to requests delayed by the configured limiter", () => + Effect.gen(function*() { + const attempts = yield* Ref.make>([]) + const limiter = yield* RateLimiter.RateLimiter + let previous: number | undefined + const client = HttpClient.make((request) => + Effect.gen(function*() { + const now = yield* Clock.currentTimeMillis + const status = previous !== undefined && now - previous < 2_000 ? 429 : 200 + previous = now + yield* Ref.update(attempts, (statuses) => [...statuses, status]) + return HttpClientResponse.fromWeb( + request, + status === 429 + ? new Response(null, { status, headers: { "retry-after": "2" } }) + : new Response(null, { status }) + ) + }) + ).pipe( + HttpClient.withRateLimiter({ + limiter, + key: "adaptive-delayed", + limit: 1, + window: "1 second" + }) + ) + + const fiber = yield* Effect.all([ + client.get("http://test/1"), + client.get("http://test/2"), + client.get("http://test/3"), + client.get("http://test/4"), + client.get("http://test/5") + ], { concurrency: "unbounded" }).pipe(Effect.forkChild({ startImmediately: true })) + + strictEqual((yield* Ref.get(attempts)).length, 1) + + yield* TestClock.adjust("1 second") + strictEqual((yield* Ref.get(attempts)).length, 2) + + yield* TestClock.adjust("1 second") + strictEqual((yield* Ref.get(attempts)).length, 2) + + yield* TestClock.adjust("1 second") + assert.deepStrictEqual(yield* Ref.get(attempts), [200, 429, 200, 429]) + + yield* TestClock.adjust("6 seconds") + yield* Fiber.join(fiber) + + const statuses = yield* Ref.get(attempts) + assert.strictEqual(statuses.filter((status) => status === 429).length, 2) + }).pipe(Effect.provide(RateLimiterTestLayer))) + + it.effect("coordinates Retry-After cooldown across clients sharing a memory store", () => + Effect.gen(function*() { + const attemptsA = yield* Ref.make(0) + const attemptsB = yield* Ref.make(0) + const limiterA = yield* RateLimiter.make + const limiterB = yield* RateLimiter.make + const clientA = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attemptsA, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 429, + headers: { "retry-after": "10" } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: limiterA, + key: "shared", + limit: 100, + window: "1 minute" + }) + ) + const clientB = HttpClient.make((request) => + Effect.as( + Ref.update(attemptsB, (n) => n + 1), + HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: limiterB, + key: "shared", + limit: 100, + window: "1 minute" + }) + ) + + const fiberA = yield* clientA.get("http://test/a").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attemptsA), 1) + + const fiberB = yield* clientB.get("http://test/b").pipe(Effect.forkChild({ startImmediately: true })) + yield* TestClock.adjust("9 seconds") + strictEqual(yield* Ref.get(attemptsB), 0) + + yield* TestClock.adjust("1 second") + yield* Fiber.join(fiberA) + yield* Fiber.join(fiberB) + + strictEqual(yield* Ref.get(attemptsA), 2) + strictEqual(yield* Ref.get(attemptsB), 1) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + + it.effect("learns adaptive pacing from Retry-After feedback", () => + Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const limiter = yield* RateLimiter.make + const client = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 || attempt === 4 + ? new Response(null, { + status: 429, + headers: { "retry-after": "10" } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter, + key: "learned", + limit: 100, + window: "1 minute" + }) + ) + + const first = yield* client.get("http://test/").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attempts), 1) + yield* TestClock.adjust("10 seconds") + yield* Fiber.join(first) + strictEqual(yield* Ref.get(attempts), 2) + + yield* client.get("http://test/") + strictEqual(yield* Ref.get(attempts), 3) + + const learning = yield* client.get("http://test/").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attempts), 4) + yield* TestClock.adjust("10 seconds") + yield* Fiber.join(learning) + strictEqual(yield* Ref.get(attempts), 5) + + const paced = yield* client.get("http://test/").pipe( + Effect.andThen(client.get("http://test/")), + Effect.forkChild({ startImmediately: true }) + ) + strictEqual(yield* Ref.get(attempts), 6) + + yield* TestClock.adjust("9 seconds") + strictEqual(yield* Ref.get(attempts), 6) + + yield* TestClock.adjust("1 second") + yield* Fiber.join(paced) + strictEqual(yield* Ref.get(attempts), 7) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + + it.effect("applies Retry-After feedback from HttpClientError response failures", () => + Effect.gen(function*() { + const attemptsA = yield* Ref.make(0) + const attemptsB = yield* Ref.make(0) + const limiterA = yield* RateLimiter.make + const limiterB = yield* RateLimiter.make + const clientA = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attemptsA, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 429, + headers: { "retry-after": "10" } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.filterStatusOk, + HttpClient.withRateLimiter({ + limiter: limiterA, + key: "failure", + limit: 100, + window: "1 minute" + }) + ) + const clientB = HttpClient.make((request) => + Effect.as( + Ref.update(attemptsB, (n) => n + 1), + HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: limiterB, + key: "failure", + limit: 100, + window: "1 minute" + }) + ) + + const fiberA = yield* clientA.get("http://test/a").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attemptsA), 1) + + const fiberB = yield* clientB.get("http://test/b").pipe(Effect.forkChild({ startImmediately: true })) + yield* TestClock.adjust("9 seconds") + strictEqual(yield* Ref.get(attemptsB), 0) + + yield* TestClock.adjust("1 second") + yield* Fiber.join(fiberA) + yield* Fiber.join(fiberB) + + strictEqual(yield* Ref.get(attemptsA), 2) + strictEqual(yield* Ref.get(attemptsB), 1) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + + it.effect("does not send adaptive feedback when response inspection is disabled", () => + Effect.gen(function*() { + const disabledAttempts = yield* Ref.make(0) + const enabledAttempts = yield* Ref.make(0) + const disabledLimiter = yield* RateLimiter.make + const enabledLimiter = yield* RateLimiter.make + const disabledClient = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(disabledAttempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 429, + headers: { "retry-after": "10" } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: disabledLimiter, + key: "disabled", + limit: 100, + window: "1 minute", + disableResponseInspection: true + }) + ) + const enabledClient = HttpClient.make((request) => + Effect.as( + Ref.update(enabledAttempts, (n) => n + 1), + HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: enabledLimiter, + key: "disabled", + limit: 100, + window: "1 minute" + }) + ) + + const disabledResponse = yield* disabledClient.get("http://test/disabled") + strictEqual(disabledResponse.status, 200) + strictEqual(yield* Ref.get(disabledAttempts), 2) + + const fiber = yield* enabledClient.get("http://test/enabled").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(enabledAttempts), 1) + yield* Fiber.join(fiber) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + + it.effect("can disable adaptive learning without disabling response inspection", () => + Effect.gen(function*() { + const disabledAttempts = yield* Ref.make(0) + const enabledAttempts = yield* Ref.make(0) + const disabledLimiter = yield* RateLimiter.make + const enabledLimiter = yield* RateLimiter.make + const disabledClient = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(disabledAttempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 429, + headers: { "retry-after": "10" } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: disabledLimiter, + key: "adaptive-disabled", + limit: 100, + window: "1 minute", + disableAdaptiveLearning: true + }) + ) + const enabledClient = HttpClient.make((request) => + Effect.as( + Ref.update(enabledAttempts, (n) => n + 1), + HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: enabledLimiter, + key: "adaptive-disabled", + limit: 100, + window: "1 minute" + }) + ) + + const disabled = yield* disabledClient.get("http://test/disabled").pipe( + Effect.forkChild({ startImmediately: true }) + ) + strictEqual(yield* Ref.get(disabledAttempts), 1) + + const enabled = yield* enabledClient.get("http://test/enabled").pipe( + Effect.forkChild({ startImmediately: true }) + ) + strictEqual(yield* Ref.get(enabledAttempts), 1) + yield* Fiber.join(enabled) + + yield* TestClock.adjust("9 seconds") + strictEqual(yield* Ref.get(disabledAttempts), 1) + + yield* TestClock.adjust("1 second") + yield* Fiber.join(disabled) + strictEqual(yield* Ref.get(disabledAttempts), 2) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + + it.effect("keeps adaptive Retry-After state isolated by resolved key", () => + Effect.gen(function*() { + const attemptsA = yield* Ref.make(0) + const attemptsB = yield* Ref.make(0) + const limiter = yield* RateLimiter.make + const clientA = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attemptsA, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 429, + headers: { "retry-after": "10" } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter, + key: "a", + limit: 100, + window: "1 minute" + }) + ) + const clientB = HttpClient.make((request) => + Effect.as( + Ref.update(attemptsB, (n) => n + 1), + HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter, + key: "b", + limit: 100, + window: "1 minute" + }) + ) + + const fiberA = yield* clientA.get("http://test/a").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attemptsA), 1) + + const fiberB = yield* clientB.get("http://test/b").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attemptsB), 1) + yield* Fiber.join(fiberB) + + yield* TestClock.adjust("10 seconds") + yield* Fiber.join(fiberA) + strictEqual(yield* Ref.get(attemptsA), 2) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + + it.effect("prefers explicit RateLimit reset headers over adaptive Retry-After feedback", () => + Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const limiter = yield* RateLimiter.make + const client = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 429, + headers: { + "retry-after": "60", + "x-ratelimit-limit": "1", + "x-ratelimit-reset-after": "1" + } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter, + key: "explicit", + limit: 100, + window: "1 minute" + }) + ) + + const retry = yield* client.get("http://test/").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust(Duration.millis(999)) + strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust(Duration.millis(1)) + yield* Fiber.join(retry) + strictEqual(yield* Ref.get(attempts), 2) + + const next = yield* client.get("http://test/").pipe(Effect.forkChild({ startImmediately: true })) + yield* TestClock.adjust(Duration.millis(999)) + strictEqual(yield* Ref.get(attempts), 2) + + yield* TestClock.adjust(Duration.millis(1)) + yield* Fiber.join(next) + strictEqual(yield* Ref.get(attempts), 3) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) }) }) diff --git a/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts index 544f37c7d..8de2eeaf0 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts @@ -1,7 +1,10 @@ import { assert, describe, it } from "@effect/vitest" +import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" import * as Logger from "effect/Logger" import * as References from "effect/References" +import * as Tracer from "effect/Tracer" import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware" import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest" import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" @@ -54,4 +57,42 @@ describe("HttpMiddleware", () => { assert.deepStrictEqual(spans, [["http.span"], ["http.span"]]) })) }) + + describe("tracer", () => { + it.effect("excludes the sent response from a failed stream span", () => + Effect.gen(function*() { + let serverSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + serverSpan = new Tracer.NativeSpan(options) + return serverSpan + } + }) + const request = HttpServerRequest.fromWeb(new Request("http://localhost:3000/stream")) + const response = HttpServerResponse.empty({ + status: 200, + headers: { "content-type": "text/event-stream" } + }) + const streamError = new Error("stream failed") + + // Once headers have been sent, the platform attaches the response to + // the stream failure so the server can finish the request correctly. + const app = Effect.failCause(Cause.combine(Cause.fail(streamError), Cause.die(response))) + + yield* Effect.exit( + HttpMiddleware.tracer(app).pipe( + Effect.provideService(HttpServerRequest.HttpServerRequest, request), + Effect.provideService(Tracer.Tracer, tracer) + ) + ) + yield* Effect.yieldNow + + assert(serverSpan !== undefined) + assert.strictEqual(serverSpan.attributes.get("http.response.status_code"), 200) + assert.strictEqual(serverSpan.status._tag, "Ended") + if (serverSpan.status._tag === "Ended") { + assert.deepStrictEqual(serverSpan.status.exit, Exit.fail(streamError)) + } + })) + }) }) diff --git a/.context/effect/packages/effect/test/unstable/http/Url.test.ts b/.context/effect/packages/effect/test/unstable/http/Url.test.ts index 819792c2d..303f1ead5 100644 --- a/.context/effect/packages/effect/test/unstable/http/Url.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/Url.test.ts @@ -1,8 +1,15 @@ import { describe, it } from "@effect/vitest" -import { Cause, Redacted } from "effect" +import { Cause, Redacted, Result } from "effect" import { Url, UrlParams } from "effect/unstable/http" -import { assertFailure, assertSuccess, assertTrue, deepStrictEqual, strictEqual } from "../../utils/assert.ts" +import { + assertFailure, + assertInstanceOf, + assertSuccess, + assertTrue, + deepStrictEqual, + strictEqual +} from "../../utils/assert.ts" describe("Url", () => { const testURL = new URL("https://example.com/test") @@ -13,6 +20,26 @@ describe("Url", () => { strictEqual(updatedUrl.toString(), expected) } + describe("make", () => { + it("appends query parameters and hash", () => { + assertSuccess( + Url.make( + "https://example.com/test?existing=true", + UrlParams.fromInput([["foo", "bar"], ["foo", "baz"]]), + "section" + ), + new URL("https://example.com/test?existing=true&foo=bar&foo=baz#section") + ) + }) + + it("fails when the URL cannot be constructed", () => { + const result = Url.make("http://%", UrlParams.empty, undefined) + + assertTrue(Result.isFailure(result)) + assertInstanceOf(result.failure, Url.UrlError) + }) + }) + describe("fromString", () => { it("parses absolute URLs", () => { const url = Url.fromString(testURL.toString()) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts index 724f40d23..ce5c4da96 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts @@ -1,12 +1,14 @@ import { assert, it } from "@effect/vitest" -import { Cause, Effect, FileSystem, Layer, Path, Schema, Stream } from "effect" +import { Cause, Effect, FileSystem, Layer, Path, Redacted, Schema, Stream } from "effect" import { Etag, HttpPlatform } from "effect/unstable/http" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, + HttpApiMiddleware, HttpApiSchema, + HttpApiSecurity, HttpApiTest } from "effect/unstable/httpapi" @@ -183,4 +185,59 @@ it.layer(TestServices)("HttpApiBuilder streaming success responses", (it) => { `data: {"text":"hello"}\n\n` ]) })) + + it.effect("does not try another security scheme after the handler fails", () => + Effect.gen(function*() { + class HandlerFailure extends Schema.TaggedErrorClass()("HandlerFailure", { + message: Schema.String + }, { httpApiStatus: 418 }) {} + + class M extends HttpApiMiddleware.Service()("Security/HandlerFailure", { + error: Schema.String.pipe( + HttpApiSchema.status(401), + HttpApiSchema.asText() + ), + security: { + first: HttpApiSecurity.apiKey({ + in: "header", + key: "x-first" + }), + second: HttpApiSecurity.apiKey({ + in: "header", + key: "x-second" + }) + } + }) {} + + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("protected", "/protected", { + headers: { + "x-first": Schema.String + }, + success: Schema.String, + error: HandlerFailure + }).middleware(M) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("protected", () => Effect.fail(new HandlerFailure({ message: "handler failed" }))) + ) + const MLive = Layer.succeed(M)({ + first: (effect, { credential }) => + Redacted.value(credential) === "ok" ? effect : Effect.fail("first unauthorized"), + second: (effect, { credential }) => + Redacted.value(credential) === "ok" ? effect : Effect.fail("second unauthorized") + }) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe( + Effect.provide(GroupLive), + Effect.provide(MLive) + ) + const error = yield* Effect.flip(client.test.protected({ headers: { "x-first": "ok" } })) + + assert.deepStrictEqual(error, new HandlerFailure({ message: "handler failed" })) + })) }) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts index d9b688b85..9e60a9e5e 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts @@ -2,6 +2,20 @@ import { assert, describe, it } from "@effect/vitest" import { Schema } from "effect" import { HttpApiSchema } from "effect/unstable/httpapi" +const getStreamMetadata = (self: HttpApiSchema.StreamSchema) => + self._tag === "StreamSse" ? + { + mode: self.mode, + sseMode: self.sseMode, + contentType: self.contentType, + events: self.events, + error: self.error + } : + { + mode: self.mode, + contentType: self.contentType + } + describe("HttpApiSchema", () => { describe("StreamSse", () => { it("stores default metadata", () => { @@ -22,7 +36,7 @@ describe("HttpApiSchema", () => { assert.strictEqual(stream.events, events) assert.strictEqual(stream.error, error) - const metadata = HttpApiSchema.getStreamMetadata(stream) + const metadata = getStreamMetadata(stream) assert.strictEqual(metadata.mode, "sse") assert.strictEqual(metadata.contentType, "text/event-stream") if (metadata.mode === "sse") { @@ -86,7 +100,7 @@ describe("HttpApiSchema", () => { assert.isTrue(HttpApiSchema.isStreamUint8Array(stream)) assert.strictEqual(stream.mode, "uint8array") assert.strictEqual(stream.contentType, "application/octet-stream") - assert.deepStrictEqual(HttpApiSchema.getStreamMetadata(stream), { + assert.deepStrictEqual(getStreamMetadata(stream), { mode: "uint8array", contentType: "application/octet-stream" }) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts index 5b6dd0620..7e7eb15fb 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts @@ -36,4 +36,33 @@ describe("OpenApi", () => { assert.property(streamExtension, "causeSchema") assert.property(streamExtension, "errorSchema") }) + + it("preserves the data schema identifier for SSE streams", () => { + const Event = Schema.Struct({ + kind: Schema.String, + payload: Schema.String + }).annotate({ identifier: "MyEvent" }) + + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("stream", "/stream", { + success: [HttpApiSchema.StreamSse({ data: Event })] + }) + ) + ) + + const spec = OpenApi.fromApi(Api) + const schemas = spec.components?.schemas + + // The decoded data schema keeps its identifier. + assert.deepStrictEqual(schemas?.MyEvent, { + type: "object", + properties: { + kind: { type: "string" }, + payload: { type: "string" } + }, + required: ["kind", "payload"], + additionalProperties: false + }) + }) }) diff --git a/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts b/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts index 8f1b90494..3f82770f7 100644 --- a/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts +++ b/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts @@ -142,4 +142,701 @@ describe(`RateLimiter`, () => { Effect.provide(RateLimiter.layerStoreMemory) )) }) + + describe("adaptive", () => { + it.effect("uses the inactive store path until 429 Retry-After feedback", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const consume = store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + let result = yield* consume + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "inactive") + + yield* store.adaptiveFeedback({ + key: "a", + epoch: result.epoch, + tokens: 1, + status: 200, + retryAfter: Duration.seconds(1) + }) + + result = yield* consume + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "inactive") + + yield* store.adaptiveFeedback({ + key: "a", + epoch: result.epoch, + tokens: 1, + status: 429, + retryAfter: undefined + }) + + result = yield* consume + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "inactive") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("starts cooldown on the first 429 Retry-After feedback", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(5) + }) + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.seconds(5)) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "cooldown") + + const otherKey = yield* store.adaptiveConsume({ + key: "b", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(otherKey.delay, Duration.zero) + assert.strictEqual(otherKey.epoch, 0) + assert.strictEqual(otherKey.phase, "inactive") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("clamps zero Retry-After feedback to the minimum cooldown", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.zero + }) + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.millis(1)) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "cooldown") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("bounds excessive adaptive Retry-After cooldowns", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.hours(2) + }) + + let result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.hours(1)) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "cooldown") + + yield* TestClock.adjust(Duration.hours(1)) + + result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, 1) + assert.strictEqual(result.phase, "learning") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("returns the remaining cooldown delay", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(10) + }) + yield* TestClock.adjust(Duration.seconds(4)) + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.seconds(6)) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "cooldown") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("starts learning on the first admitted post-cooldown request", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(10) + }) + yield* TestClock.adjust(Duration.seconds(10)) + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 2, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, 1) + assert.strictEqual(result.phase, "learning") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("keeps learning requests at zero delay while observing token counts", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + yield* TestClock.adjust(Duration.seconds(1)) + + const learning = yield* store.adaptiveConsume({ + key: "a", + tokens: 2, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.strictEqual(learning.phase, "learning") + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 3, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, learning.epoch) + assert.strictEqual(result.phase, "learning") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("learns the accepted token count and inferred window from learning feedback", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + yield* TestClock.adjust(Duration.seconds(1)) + + const learning = yield* store.adaptiveConsume({ + key: "a", + tokens: 2, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + yield* TestClock.adjust(Duration.seconds(4)) + const rejected = yield* store.adaptiveConsume({ + key: "a", + tokens: 3, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: rejected.epoch, + tokens: 3, + status: 429, + retryAfter: Duration.seconds(6) + }) + yield* TestClock.adjust(Duration.seconds(6)) + + assert.strictEqual(learning.epoch, rejected.epoch) + + let result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.phase, "learned") + + result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.phase, "learned") + + result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.seconds(10)) + assert.strictEqual(result.phase, "learned") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("ignores stale epoch feedback", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + yield* TestClock.adjust(Duration.seconds(1)) + + const learning = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(10) + }) + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, learning.epoch) + assert.strictEqual(result.phase, "learning") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("prevents repeated learning 429 feedback from corrupting learned state", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + yield* TestClock.adjust(Duration.seconds(1)) + + yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + const rejected = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: rejected.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(5) + }) + yield* store.adaptiveFeedback({ + key: "a", + epoch: rejected.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(30) + }) + + const learned = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(learned.delay, Duration.seconds(5)) + assert.strictEqual(learned.epoch, rejected.epoch + 1) + assert.strictEqual(learned.phase, "learned") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("same-epoch learned 429 feedback enters cooldown and can only extend it", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + yield* TestClock.adjust(Duration.seconds(1)) + + yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + const rejected = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: rejected.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(5) + }) + + const learned = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + yield* store.adaptiveFeedback({ + key: "a", + epoch: learned.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(5) + }) + yield* store.adaptiveFeedback({ + key: "a", + epoch: learned.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(10) + }) + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.seconds(10)) + assert.strictEqual(result.epoch, learned.epoch) + assert.strictEqual(result.phase, "cooldown") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("stores only cooldown when no tokens were accepted during learning", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + yield* TestClock.adjust(Duration.seconds(1)) + + const learning = yield* store.adaptiveConsume({ + key: "a", + tokens: 2, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + yield* store.adaptiveFeedback({ + key: "a", + epoch: learning.epoch, + tokens: 2, + status: 429, + retryAfter: Duration.seconds(5) + }) + + let result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.seconds(5)) + assert.strictEqual(result.epoch, learning.epoch) + assert.strictEqual(result.phase, "cooldown") + + yield* TestClock.adjust(Duration.seconds(5)) + + result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, learning.epoch + 1) + assert.strictEqual(result.phase, "learning") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("expires learned adaptive state", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + yield* TestClock.adjust(Duration.seconds(1)) + + yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + const rejected = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: rejected.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.seconds(1) + }) + + yield* TestClock.adjust(Duration.seconds(61)) + + const result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "inactive") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("bounds learned adaptive windows and expires the bounded state", () => + Effect.gen(function*() { + const store = yield* RateLimiter.RateLimiterStore + const first = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: first.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.millis(1) + }) + yield* TestClock.adjust(Duration.millis(1)) + + yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + const rejected = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + + yield* store.adaptiveFeedback({ + key: "a", + epoch: rejected.epoch, + tokens: 1, + status: 429, + retryAfter: Duration.hours(2) + }) + + let result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.hours(1)) + assert.strictEqual(result.phase, "learned") + + yield* TestClock.adjust(Duration.minutes(61)) + + result = yield* store.adaptiveConsume({ + key: "a", + tokens: 1, + fallbackLimit: 5, + fallbackWindow: Duration.minutes(1) + }) + assert.deepStrictEqual(result.delay, Duration.zero) + assert.strictEqual(result.epoch, 0) + assert.strictEqual(result.phase, "inactive") + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + }) }) diff --git a/.context/effect/packages/effect/typeperf/README.md b/.context/effect/packages/effect/typeperf/README.md new file mode 100644 index 000000000..e33ee0e82 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/README.md @@ -0,0 +1,176 @@ +# Effect Type Performance + +This harness measures TypeScript type instantiations for focused fixtures. + +Use it when a Schema type-level change needs a small regression guard for the +specific type path that was optimized. + +## Core Model + +Each suite has one shared baseline. Fixture cost is reported as: + +```txt +delta = fixture instantiations - suite baseline instantiations +``` + +The runner compiles each baseline and fixture as an isolated TypeScript program +by generating temporary `tsconfig` files under `tmp/typeperf`. + +Fixtures must use realistic package imports: + +```ts +import { Schema } from "effect" +``` + +Do not import source files directly, for example do not use +`../../src/Schema.ts`. The package self-reference should resolve the public +`effect` package entry. + +Every fixture in a suite should repeat the suite warmup expression from +`baseline.ts`. For the current Schema suite, that means: + +```ts +import { Schema } from "effect" + +Schema.String +``` + +## Commands + +From the repository root: + +```sh +pnpm typeperf +pnpm typeperf schema +pnpm typeperf schema/struct-required +pnpm typeperf --update +pnpm typeperf schema/struct-required --update +``` + +Without a target, the runner checks every suite and fixture. A target can be a +suite name (`schema`) or an individual fixture (`schema/struct-required`). + +`--update` writes exact measured deltas to threshold files. When a target is +provided, only the selected suite or fixture is updated. + +## Adding A Fixture + +Add a fixture when a change optimizes one specific constructor, helper, or +type-level path. + +1. Create one file under the suite's `fixtures/` directory. +2. Name it after the isolated behavior being measured, for example + `struct-optional-only.ts`. +3. Add a first-line comment explaining the single thing being measured. +4. Use realistic package imports. +5. Repeat the suite warmup expression. +6. Build only the schema needed for that one behavior. +7. Export type aliases that force TypeScript to instantiate the views under + test. +8. Add the fixture to `config.json`. +9. Run `pnpm typeperf / --update`. +10. Run `pnpm typeperf /`. + +Example: + +```ts +// Measures the marginal type-level cost of Schema.Struct when all fields are optional keys. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + count: Schema.optionalKey(Schema.NumberFromString) +}) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] +``` + +Then register it: + +```json +{ + "name": "struct-optional-only", + "file": "suites/schema/fixtures/struct-optional-only.ts" +} +``` + +### Fixture Rules + +- Measure one thing per fixture. Split separate paths into separate files. +- Keep inputs realistic, but not large for its own sake. +- Export the minimum type aliases needed to force the type computation being + measured. +- Do not combine multiple optimized APIs just to make the fixture look like an + application model. +- Do not add fixtures for unchanged or unrelated APIs. +- Do not add fixture files to `packages/effect/tsconfig.json`; `pnpm typeperf` + compiles them independently. +- If a fixture fails to compile, fix the fixture or source types. Do not hide + the problem by aggregating it with another fixture. + +## Adding A Suite + +Add a suite when fixtures need a different shared baseline. For example, +Schema-specific fixtures share the `schema` baseline, while a future HTTP API +suite may need a different import and warmup. + +1. Create `suites//baseline.ts`. +2. Create `suites//fixtures/`. +3. Create `suites//thresholds.json` with `{}`. +4. Add a suite entry to `config.json`. +5. Add at least one focused fixture. +6. Run `pnpm typeperf --update`. +7. Run `pnpm typeperf `. + +Example suite entry: + +```json +{ + "name": "schema", + "baseline": "suites/schema/baseline.ts", + "thresholds": "suites/schema/thresholds.json", + "fixtures": [ + { + "name": "struct-required", + "file": "suites/schema/fixtures/struct-required.ts" + } + ] +} +``` + +The baseline should contain only the import and warmup common to every fixture in +that suite. If a fixture needs extra setup that others do not need, put that +setup in the fixture, not in the shared baseline. + +## Updating Thresholds + +Use `--update` only after verifying that the measured delta is expected. + +```sh +pnpm typeperf schema/struct-required --update +pnpm typeperf schema/struct-required +``` + +`--update` writes the exact measured delta as `maxDelta`. It does not add a +margin. + +## Validation Checklist + +After adding or changing fixtures: + +```sh +pnpm typeperf / --update +pnpm typeperf / +pnpm exec dprint check package.json packages/effect/typeperf +``` + +Run the broader suite when the change touches shared runner behavior: + +```sh +pnpm typeperf +``` diff --git a/.context/effect/packages/effect/typeperf/config.json b/.context/effect/packages/effect/typeperf/config.json new file mode 100644 index 000000000..9115b501c --- /dev/null +++ b/.context/effect/packages/effect/typeperf/config.json @@ -0,0 +1,99 @@ +{ + "suites": [ + { + "name": "schema", + "baseline": "suites/schema/baseline.ts", + "thresholds": "suites/schema/thresholds.json", + "fixtures": [ + { + "name": "struct-required", + "file": "suites/schema/fixtures/struct-required.ts" + }, + { + "name": "struct-optional-only", + "file": "suites/schema/fixtures/struct-optional-only.ts" + }, + { + "name": "struct-mutable-only", + "file": "suites/schema/fixtures/struct-mutable-only.ts" + }, + { + "name": "struct-mixed", + "file": "suites/schema/fixtures/struct-mixed.ts" + }, + { + "name": "union", + "file": "suites/schema/fixtures/union.ts" + }, + { + "name": "tagged-union", + "file": "suites/schema/fixtures/tagged-union.ts" + }, + { + "name": "record", + "file": "suites/schema/fixtures/record.ts" + }, + { + "name": "struct-with-rest", + "file": "suites/schema/fixtures/struct-with-rest.ts" + }, + { + "name": "tuple", + "file": "suites/schema/fixtures/tuple.ts" + }, + { + "name": "tuple-with-rest", + "file": "suites/schema/fixtures/tuple-with-rest.ts" + }, + { + "name": "array", + "file": "suites/schema/fixtures/array.ts" + }, + { + "name": "non-empty-array", + "file": "suites/schema/fixtures/non-empty-array.ts" + }, + { + "name": "to-type", + "file": "suites/schema/fixtures/to-type.ts" + }, + { + "name": "to-encoded", + "file": "suites/schema/fixtures/to-encoded.ts" + }, + { + "name": "to-codec-json", + "file": "suites/schema/fixtures/to-codec-json.ts" + }, + { + "name": "to-codec-string-tree", + "file": "suites/schema/fixtures/to-codec-string-tree.ts" + }, + { + "name": "to-codec-array-from-single", + "file": "suites/schema/fixtures/to-codec-array-from-single.ts" + }, + { + "name": "brand", + "file": "suites/schema/fixtures/brand.ts" + }, + { + "name": "decode-unknown-effect", + "file": "suites/schema/fixtures/decode-unknown-effect.ts" + }, + { + "name": "encode-unknown-effect", + "file": "suites/schema/fixtures/encode-unknown-effect.ts" + }, + { + "name": "to-standard-json-schema", + "file": "suites/schema/fixtures/to-standard-json-schema.ts" + }, + { + "name": "exit", + "file": "suites/schema/fixtures/exit.ts" + } + ] + } + ] +} diff --git a/.context/effect/packages/effect/typeperf/run.mjs b/.context/effect/packages/effect/typeperf/run.mjs new file mode 100644 index 000000000..5a7a33a63 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/run.mjs @@ -0,0 +1,195 @@ +import { spawnSync } from "node:child_process" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { dirname, join, relative, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +const args = process.argv.slice(2) +const update = args.includes("--update") +const help = args.includes("--help") || args.includes("-h") +const targets = args.filter((arg) => arg !== "--update" && arg !== "--help" && arg !== "-h") + +if (help) { + console.log(`Usage: pnpm typeperf [suite[/fixture]] [--update] + +Options: + --update Write exact measured deltas to thresholds.json +`) + process.exit(0) +} + +if (targets.length > 1) { + console.error(`Expected at most one target, got: ${targets.join(", ")}`) + process.exit(1) +} + +const typeperfDir = dirname(fileURLToPath(import.meta.url)) +const effectDir = resolve(typeperfDir, "..") +const repoRoot = resolve(effectDir, "../..") +const tmpDir = join(repoRoot, "tmp", "typeperf", "effect") +const configPath = join(typeperfDir, "config.json") +const effectTsconfigPath = join(effectDir, "tsconfig.json") + +const readJson = (path) => JSON.parse(readFileSync(path, "utf8")) + +const writeJson = (path, value) => { + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) +} + +const toAbsolute = (path) => resolve(typeperfDir, path) + +const sanitize = (name) => name.replace(/[^a-zA-Z0-9._-]/g, "-") + +const writeTempTsconfig = (suiteName, subjectName, sourcePath) => { + mkdirSync(tmpDir, { recursive: true }) + const id = sanitize(`${suiteName}-${subjectName}`) + const tsconfigPath = join(tmpDir, `${id}.json`) + const tsBuildInfoPath = join(tmpDir, `${id}.tsbuildinfo`) + writeJson(tsconfigPath, { + extends: effectTsconfigPath, + files: [sourcePath], + include: [], + compilerOptions: { + noEmit: true, + declaration: false, + declarationMap: false, + sourceMap: false, + rootDir: effectDir, + incremental: false, + composite: false, + tsBuildInfoFile: tsBuildInfoPath + } + }) + return tsconfigPath +} + +const readInstantiations = (output, label) => { + const match = output.match(/^Instantiations:\s+(\d+)$/m) + if (!match) { + throw new Error(`Could not read Instantiations from compiler output for ${label}`) + } + return Number(match[1]) +} + +const measure = (suiteName, subjectName, sourcePath) => { + const tsconfigPath = writeTempTsconfig(suiteName, subjectName, sourcePath) + const result = spawnSync("pnpm", ["exec", "tsc", "-p", tsconfigPath, "--extendedDiagnostics", "--noEmit"], { + cwd: effectDir, + encoding: "utf8" + }) + const output = `${result.stdout ?? ""}${result.stderr ?? ""}` + if (result.error) { + throw result.error + } + if (result.status !== 0) { + process.stderr.write(output) + throw new Error(`Compiler failed for ${suiteName}/${subjectName}`) + } + return readInstantiations(output, `${suiteName}/${subjectName}`) +} + +const formatNumber = (n) => n.toLocaleString("en-US") + +const printTable = (rows) => { + const table = [ + ["suite", "fixture", "baseline", "total", "delta", "maxDelta", "status"], + ...rows.map((row) => [ + row.suite, + row.fixture, + formatNumber(row.baseline), + formatNumber(row.total), + formatNumber(row.delta), + row.maxDelta === undefined ? "-" : formatNumber(row.maxDelta), + row.status + ]) + ] + const widths = table[0].map((_, index) => Math.max(...table.map((row) => row[index].length))) + for (const [index, row] of table.entries()) { + const line = row.map((cell, cellIndex) => cell.padEnd(widths[cellIndex])).join(" ") + console.log(line) + if (index === 0) { + console.log(widths.map((width) => "-".repeat(width)).join(" ")) + } + } +} + +const config = readJson(configPath) +const selectedTarget = targets[0] +const [selectedSuiteName, selectedFixtureName] = selectedTarget === undefined ? [] : selectedTarget.split("/") + +if (selectedTarget !== undefined && (selectedSuiteName === "" || selectedFixtureName === "")) { + console.error(`Invalid target: ${selectedTarget}`) + process.exit(1) +} + +const rows = [] +let failed = false +let matchedTarget = false + +for (const suite of config.suites) { + if (selectedSuiteName !== undefined && suite.name !== selectedSuiteName) { + continue + } + matchedTarget = selectedFixtureName === undefined + const baselinePath = toAbsolute(suite.baseline) + const thresholdsPath = toAbsolute(suite.thresholds) + const thresholds = existsSync(thresholdsPath) ? readJson(thresholdsPath) : {} + const baseline = measure(suite.name, "baseline", baselinePath) + + for (const fixture of suite.fixtures) { + if (selectedFixtureName !== undefined && fixture.name !== selectedFixtureName) { + continue + } + matchedTarget = true + const fixturePath = toAbsolute(fixture.file) + const total = measure(suite.name, fixture.name, fixturePath) + const delta = total - baseline + const threshold = thresholds[fixture.name] + const maxDelta = typeof threshold?.maxDelta === "number" ? threshold.maxDelta : undefined + let status + + if (update) { + thresholds[fixture.name] = { maxDelta: delta } + status = "updated" + } else if (maxDelta === undefined) { + status = "missing" + failed = true + } else if (delta <= maxDelta) { + status = "ok" + } else { + status = "fail" + failed = true + } + + rows.push({ + suite: suite.name, + fixture: fixture.name, + baseline, + total, + delta, + maxDelta: update ? delta : maxDelta, + status + }) + } + + if (update) { + writeJson(thresholdsPath, thresholds) + console.log(`Updated ${relative(repoRoot, thresholdsPath)}`) + } +} + +if (!matchedTarget) { + const availableTargets = config.suites.flatMap((suite) => [ + suite.name, + ...suite.fixtures.map((fixture) => `${suite.name}/${fixture.name}`) + ]) + console.error(`Unknown typeperf target: ${selectedTarget}`) + console.error(`Available targets:\n${availableTargets.map((target) => ` - ${target}`).join("\n")}`) + process.exit(1) +} + +printTable(rows) + +if (failed) { + process.exit(1) +} diff --git a/.context/effect/packages/effect/typeperf/suites/schema/baseline.ts b/.context/effect/packages/effect/typeperf/suites/schema/baseline.ts new file mode 100644 index 000000000..906ab97fd --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/baseline.ts @@ -0,0 +1,4 @@ +// Shared Schema suite baseline: import the public package and touch Schema once. +import { Schema } from "effect" + +Schema.String diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/array.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/array.ts new file mode 100644 index 000000000..823d18cf0 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/array.ts @@ -0,0 +1,14 @@ +// Measures the marginal type-level cost of Schema.Array for one structured element schema. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Array(Schema.Struct({ + id: Schema.String, + count: Schema.NumberFromString +})) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/brand.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/brand.ts new file mode 100644 index 000000000..d135dc18b --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/brand.ts @@ -0,0 +1,10 @@ +// Measures the marginal type-level cost of applying Schema.brand to one schema. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.String.pipe(Schema.brand("TypeperfBrand")) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/decode-unknown-effect.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/decode-unknown-effect.ts new file mode 100644 index 000000000..89129a257 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/decode-unknown-effect.ts @@ -0,0 +1,8 @@ +// Measures the marginal type-level cost of Schema.decodeUnknownEffect. +import { Schema } from "effect" + +Schema.String + +const decode = Schema.decodeUnknownEffect(Schema.NumberFromString) + +export type Decode = typeof decode diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/encode-unknown-effect.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/encode-unknown-effect.ts new file mode 100644 index 000000000..be3f705a9 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/encode-unknown-effect.ts @@ -0,0 +1,8 @@ +// Measures the marginal type-level cost of Schema.encodeUnknownEffect. +import { Schema } from "effect" + +Schema.String + +const encode = Schema.encodeUnknownEffect(Schema.NumberFromString) + +export type Encode = typeof encode diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/exit.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/exit.ts new file mode 100644 index 000000000..03213cb67 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/exit.ts @@ -0,0 +1,14 @@ +// Measures the marginal type-level cost of Schema.Exit. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Exit( + Schema.Struct({ id: Schema.String, count: Schema.NumberFromString }), + Schema.Struct({ code: Schema.String, message: Schema.String }), + Schema.Defect() +) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/non-empty-array.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/non-empty-array.ts new file mode 100644 index 000000000..268eeea5a --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/non-empty-array.ts @@ -0,0 +1,14 @@ +// Measures the marginal type-level cost of Schema.NonEmptyArray for one structured element schema. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.NonEmptyArray(Schema.Struct({ + id: Schema.String, + count: Schema.NumberFromString +})) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/record.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/record.ts new file mode 100644 index 000000000..2559ac037 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/record.ts @@ -0,0 +1,14 @@ +// Measures the marginal type-level cost of Schema.Record with literal keys and mixed field modifiers. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Record( + Schema.Literals(["primary", "secondary", "archive"]), + Schema.mutableKey(Schema.optionalKey(Schema.NumberFromString)) +) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-mixed.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-mixed.ts new file mode 100644 index 000000000..b379327f8 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-mixed.ts @@ -0,0 +1,22 @@ +// Measures the marginal type-level cost of Schema.Struct with mixed required, optional, and mutable fields. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Struct({ + id: Schema.String, + name: Schema.optionalKey(Schema.String), + count: Schema.mutableKey(Schema.NumberFromString), + enabled: Schema.Boolean.pipe(Schema.optionalKey, Schema.mutableKey), + createdAt: Schema.DateFromString, + updatedAt: Schema.optionalKey(Schema.DateFromString), + country: Schema.mutableKey(Schema.String), + region: Schema.String.pipe(Schema.mutableKey, Schema.optionalKey), + city: Schema.String, + postalCode: Schema.optional(Schema.String) +}) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-mutable-only.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-mutable-only.ts new file mode 100644 index 000000000..586f21351 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-mutable-only.ts @@ -0,0 +1,22 @@ +// Measures the marginal type-level cost of Schema.Struct when all fields are mutable. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Struct({ + id: Schema.mutableKey(Schema.String), + name: Schema.mutableKey(Schema.String), + count: Schema.mutableKey(Schema.NumberFromString), + enabled: Schema.mutableKey(Schema.Boolean), + createdAt: Schema.mutableKey(Schema.DateFromString), + updatedAt: Schema.mutableKey(Schema.DateFromString), + country: Schema.mutableKey(Schema.String), + region: Schema.mutableKey(Schema.String), + city: Schema.mutableKey(Schema.String), + postalCode: Schema.mutableKey(Schema.String) +}) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-optional-only.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-optional-only.ts new file mode 100644 index 000000000..c519ebd02 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-optional-only.ts @@ -0,0 +1,22 @@ +// Measures the marginal type-level cost of Schema.Struct when all fields are optional keys. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Struct({ + id: Schema.optionalKey(Schema.String), + name: Schema.optionalKey(Schema.String), + count: Schema.optionalKey(Schema.NumberFromString), + enabled: Schema.optionalKey(Schema.Boolean), + createdAt: Schema.optionalKey(Schema.DateFromString), + updatedAt: Schema.optionalKey(Schema.DateFromString), + country: Schema.optionalKey(Schema.String), + region: Schema.optionalKey(Schema.String), + city: Schema.optionalKey(Schema.String), + postalCode: Schema.optionalKey(Schema.String) +}) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-required.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-required.ts new file mode 100644 index 000000000..72b779c2b --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-required.ts @@ -0,0 +1,22 @@ +// Measures the marginal type-level cost of Schema.Struct when all fields are required readonly fields. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Struct({ + id: Schema.String, + name: Schema.String, + count: Schema.NumberFromString, + enabled: Schema.Boolean, + createdAt: Schema.DateFromString, + updatedAt: Schema.DateFromString, + country: Schema.String, + region: Schema.String, + city: Schema.String, + postalCode: Schema.String +}) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-with-rest.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-with-rest.ts new file mode 100644 index 000000000..7b50a5235 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/struct-with-rest.ts @@ -0,0 +1,17 @@ +// Measures the marginal type-level cost of Schema.StructWithRest without opt-in record validation. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.StructWithRest( + Schema.Struct({ + id: Schema.String, + value: Schema.NumberFromString + }), + [Schema.Record(Schema.String, Schema.mutableKey(Schema.NumberFromString))] +) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tagged-union.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tagged-union.ts new file mode 100644 index 000000000..811320ab0 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tagged-union.ts @@ -0,0 +1,16 @@ +// Measures the marginal type-level cost of Schema.TaggedUnion with several tagged struct cases. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.TaggedUnion({ + Created: { id: Schema.String, name: Schema.String }, + Updated: { id: Schema.String, before: Schema.String, after: Schema.String }, + Counted: { id: Schema.String, count: Schema.NumberFromString }, + Flagged: { id: Schema.String, enabled: Schema.Boolean } +}) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type Cases = typeof schema.cases diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-array-from-single.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-array-from-single.ts new file mode 100644 index 000000000..b15dbae5e --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-array-from-single.ts @@ -0,0 +1,12 @@ +// Measures the marginal type-level cost of Schema.toCodecArrayFromSingle. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.toCodecArrayFromSingle( + Schema.toCodecStringTree(Schema.Array(Schema.NumberFromString)) +) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-json.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-json.ts new file mode 100644 index 000000000..00a3dae4c --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-json.ts @@ -0,0 +1,13 @@ +// Measures the marginal type-level cost of Schema.toCodecJson. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.toCodecJson(Schema.Struct({ + id: Schema.String, + count: Schema.NumberFromString +})) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-string-tree.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-string-tree.ts new file mode 100644 index 000000000..24c187a64 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-codec-string-tree.ts @@ -0,0 +1,10 @@ +// Measures the marginal type-level cost of Schema.toCodecStringTree. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.toCodecStringTree(Schema.Array(Schema.NumberFromString)) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-encoded.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-encoded.ts new file mode 100644 index 000000000..9fca5c694 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-encoded.ts @@ -0,0 +1,10 @@ +// Measures the marginal type-level cost of Schema.toEncoded. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.toEncoded(Schema.NumberFromString) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-standard-json-schema.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-standard-json-schema.ts new file mode 100644 index 000000000..c11939745 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-standard-json-schema.ts @@ -0,0 +1,11 @@ +// Measures the marginal type-level cost of Schema.toStandardJSONSchemaV1. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.toStandardJSONSchemaV1(Schema.Struct({ + id: Schema.String, + count: Schema.NumberFromString +})) + +export type StandardJsonSchema = typeof schema diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-type.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-type.ts new file mode 100644 index 000000000..e24d73b4e --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/to-type.ts @@ -0,0 +1,10 @@ +// Measures the marginal type-level cost of Schema.toType. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.toType(Schema.NumberFromString) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tuple-with-rest.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tuple-with-rest.ts new file mode 100644 index 000000000..bccbc4fe3 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tuple-with-rest.ts @@ -0,0 +1,20 @@ +// Measures the marginal type-level cost of Schema.TupleWithRest. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.TupleWithRest( + Schema.Tuple([ + Schema.String, + Schema.NumberFromString + ]), + [ + Schema.Boolean, + Schema.String + ] +) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tuple.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tuple.ts new file mode 100644 index 000000000..b22fc4d22 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/tuple.ts @@ -0,0 +1,16 @@ +// Measures the marginal type-level cost of Schema.Tuple. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Tuple([ + Schema.String, + Schema.NumberFromString, + Schema.Boolean, + Schema.Array(Schema.String) +]) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso +export type MakeIn = typeof schema["~type.make.in"] diff --git a/.context/effect/packages/effect/typeperf/suites/schema/fixtures/union.ts b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/union.ts new file mode 100644 index 000000000..80a41e302 --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/fixtures/union.ts @@ -0,0 +1,15 @@ +// Measures the marginal type-level cost of Schema.Union with several structured members. +import { Schema } from "effect" + +Schema.String + +const schema = Schema.Union([ + Schema.Struct({ type: Schema.Literal("a"), id: Schema.String, count: Schema.NumberFromString }), + Schema.Struct({ type: Schema.Literal("b"), id: Schema.String, enabled: Schema.Boolean }), + Schema.Struct({ type: Schema.Literal("c"), id: Schema.String, tags: Schema.Array(Schema.String) }), + Schema.Struct({ type: Schema.Literal("d"), id: Schema.String, nested: Schema.Struct({ value: Schema.String }) }) +]) + +export type Type = typeof schema.Type +export type Encoded = typeof schema.Encoded +export type Iso = typeof schema.Iso diff --git a/.context/effect/packages/effect/typeperf/suites/schema/thresholds.json b/.context/effect/packages/effect/typeperf/suites/schema/thresholds.json new file mode 100644 index 000000000..2c94b228f --- /dev/null +++ b/.context/effect/packages/effect/typeperf/suites/schema/thresholds.json @@ -0,0 +1,68 @@ +{ + "struct-required": { + "maxDelta": 401 + }, + "struct-optional-only": { + "maxDelta": 647 + }, + "struct-mutable-only": { + "maxDelta": 659 + }, + "struct-mixed": { + "maxDelta": 953 + }, + "union": { + "maxDelta": 907 + }, + "tagged-union": { + "maxDelta": 924 + }, + "record": { + "maxDelta": 289 + }, + "struct-with-rest": { + "maxDelta": 634 + }, + "tuple": { + "maxDelta": 417 + }, + "tuple-with-rest": { + "maxDelta": 408 + }, + "array": { + "maxDelta": 199 + }, + "non-empty-array": { + "maxDelta": 199 + }, + "to-type": { + "maxDelta": 31 + }, + "to-encoded": { + "maxDelta": 29 + }, + "to-codec-json": { + "maxDelta": 117 + }, + "to-codec-string-tree": { + "maxDelta": 59 + }, + "to-codec-array-from-single": { + "maxDelta": 96 + }, + "brand": { + "maxDelta": 82 + }, + "decode-unknown-effect": { + "maxDelta": 2 + }, + "encode-unknown-effect": { + "maxDelta": 2 + }, + "to-standard-json-schema": { + "maxDelta": 128 + }, + "exit": { + "maxDelta": 359 + } +} diff --git a/.context/effect/packages/effect/typetest/Config.tst.ts b/.context/effect/packages/effect/typetest/Config.tst.ts index 1c7410865..aa760eafa 100644 --- a/.context/effect/packages/effect/typetest/Config.tst.ts +++ b/.context/effect/packages/effect/typetest/Config.tst.ts @@ -28,4 +28,16 @@ describe("Config", () => { type S = Config.Success expect().type.toBe<"a" | "b">() }) + + it("Record", () => { + const c = Config.schema(Config.Record(Schema.String, Schema.FiniteFromString)) + + expect(c).type.toBe>() + }) + + it("Array", () => { + const c = Config.schema(Config.Array(Schema.FiniteFromString)) + + expect(c).type.toBe>>() + }) }) diff --git a/.context/effect/packages/effect/typetest/Effect.tst.ts b/.context/effect/packages/effect/typetest/Effect.tst.ts index 5102d9add..abdad1719 100644 --- a/.context/effect/packages/effect/typetest/Effect.tst.ts +++ b/.context/effect/packages/effect/typetest/Effect.tst.ts @@ -62,6 +62,7 @@ declare const channelStringOrNumber: declare const layerStringOrNumber: | Layer.Layer<"out-1", "err-1", "dep-1"> | Layer.Layer<"out-2", "err-2", "dep-2"> +declare const optionString: Option.Option declare const optionStringOrNumber: Option.Option | Option.Option declare const resultStringOrNumber: Result.Result | Result.Result declare const fiberStringOrNumber: Fiber.Fiber | Fiber.Fiber @@ -212,6 +213,31 @@ describe("Effect.transposeOption", () => { }) }) +describe("Effect.fromOption", () => { + it("uses NoSuchElementError by default", () => { + const result = Effect.fromOption(optionString) + expect(result).type.toBe>() + }) + + it("supports a custom error in data-first form", () => { + const result = Effect.fromOption(optionString, () => new SimpleError({ code: 1 })) + expect(result).type.toBe>() + }) + + it("supports a custom error in data-last form", () => { + const result = pipe( + optionString, + Effect.fromOption(() => new SimpleError({ code: 1 })) + ) + expect(result).type.toBe>() + }) + + it("supports callback usage with the default error", () => { + const result = Effect.flatMap(Effect.succeed(optionString), Effect.fromOption) + expect(result).type.toBe>() + }) +}) + describe("Effect.firstSuccessOf", () => { it("infers success, error, and requirements from the effect collection", () => { const result = Effect.firstSuccessOf([ diff --git a/.context/effect/packages/effect/typetest/schema/Array.tst.ts b/.context/effect/packages/effect/typetest/schema/Array.tst.ts index be3c87e34..ed0347a6a 100644 --- a/.context/effect/packages/effect/typetest/schema/Array.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/Array.tst.ts @@ -44,3 +44,21 @@ describe("NonEmptyArray", () => { expect(schema.schema.value).type.toBe() }) }) + +describe("ArrayEnsure", () => { + it("ArrayEnsure", () => { + const schema = Schema.ArrayEnsure(Schema.FiniteFromString) + expect(Schema.revealCodec(schema)).type.toBe, string | ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotate({})).type.toBe>() + }) +}) + +describe("UniqueArray", () => { + it("UniqueArray", () => { + const schema = Schema.UniqueArray(Schema.FiniteFromString) + expect(Schema.revealCodec(schema)).type.toBe, ReadonlyArray>>() + expect(schema).type.toBe>() + expect(schema.annotate({})).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/Schema.tst.ts b/.context/effect/packages/effect/typetest/schema/Schema.tst.ts index 7fde58c46..ff42b15ed 100644 --- a/.context/effect/packages/effect/typetest/schema/Schema.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/Schema.tst.ts @@ -463,6 +463,7 @@ describe("Schema", () => { const schema = Schema.toType(Schema.FiniteFromString) expect(Schema.revealCodec(schema)).type.toBe>() expect(schema).type.toBe>() + expect(schema.schema).type.toBe() expect(schema.annotate({})).type.toBe>() }) }) @@ -477,10 +478,58 @@ describe("Schema", () => { const schema = Schema.toEncoded(Schema.FiniteFromString) expect(Schema.revealCodec(schema)).type.toBe>() expect(schema).type.toBe>() + expect(schema.schema).type.toBe() expect(schema.annotate({})).type.toBe>() }) }) + describe("toCodecJson", () => { + it("ast type", () => { + const schema = Schema.toCodecJson(Schema.FiniteFromString) + expect(schema.ast).type.toBe() + }) + + it("revealCodec + annotate", () => { + const schema = Schema.toCodecJson(Schema.FiniteFromString) + expect(Schema.revealCodec(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.schema).type.toBe() + expect(schema.annotate({})).type.toBe>() + }) + }) + + describe("toCodecStringTree", () => { + it("ast type", () => { + const schema = Schema.toCodecStringTree(Schema.FiniteFromString) + expect(schema.ast).type.toBe() + }) + + it("Array ast type", () => { + const schema = Schema.toCodecStringTree(Schema.Array(Schema.FiniteFromString)) + expect(schema.ast).type.toBe() + }) + + it("revealCodec + annotate", () => { + const schema = Schema.toCodecStringTree(Schema.FiniteFromString) + expect(Schema.revealCodec(schema)).type.toBe>() + expect(schema).type.toBe>() + expect(schema.schema).type.toBe() + expect(schema.annotate({})).type.toBe>() + }) + }) + + describe("toCodecArrayFromSingle", () => { + it("revealCodec + annotate", () => { + const stringTree = Schema.toCodecStringTree(Schema.Array(Schema.FiniteFromString)) + const schema = Schema.toCodecArrayFromSingle(stringTree) + expect(Schema.revealCodec(schema)).type.toBe< + Schema.Codec, Schema.StringTree, never, never> + >() + expect(schema).type.toBe>() + expect(schema.annotate({})).type.toBe>() + }) + }) + describe("annotateEncoded", () => { it("non-transforming schema should return Rebuild", () => { const schema = Schema.String.pipe( @@ -952,7 +1001,7 @@ describe("Schema", () => { it("should allow partial application", () => { const f = Schema.decodeTo(Schema.String) expect(f).type.toBe< - (from: From) => Schema.compose + (from: From) => Schema.compose >() expect(f(Schema.Number)).type.toBe>() @@ -1828,6 +1877,34 @@ describe("Schema", () => { }) }) + describe("OptionFrom", () => { + it("preserves concrete wrapper types", () => { + const nullOr = Schema.OptionFromNullOr(Schema.FiniteFromString) + expect(nullOr).type.toBe>() + expect(nullOr.annotate({})).type.toBe>() + + const undefinedOr = Schema.OptionFromUndefinedOr(Schema.FiniteFromString) + expect(undefinedOr).type.toBe>() + expect(undefinedOr.annotate({})).type.toBe>() + + const nullishOr = Schema.OptionFromNullishOr(Schema.FiniteFromString) + expect(nullishOr).type.toBe>() + expect(nullishOr.annotate({})).type.toBe>() + + const optionalKey = Schema.OptionFromOptionalKey(Schema.FiniteFromString) + expect(optionalKey).type.toBe>() + expect(optionalKey.annotate({})).type.toBe>() + + const optional = Schema.OptionFromOptional(Schema.FiniteFromString) + expect(optional).type.toBe>() + expect(optional.annotate({})).type.toBe>() + + const optionalNullOr = Schema.OptionFromOptionalNullOr(Schema.FiniteFromString) + expect(optionalNullOr).type.toBe>() + expect(optionalNullOr.annotate({})).type.toBe>() + }) + }) + describe("Annotations", () => { describe("resolveAnnotations", () => { it("String", () => { diff --git a/.context/effect/packages/effect/typetest/schema/Struct.tst.ts b/.context/effect/packages/effect/typetest/schema/Struct.tst.ts index aa9adc37f..10c3790db 100644 --- a/.context/effect/packages/effect/typetest/schema/Struct.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/Struct.tst.ts @@ -150,6 +150,26 @@ describe("Struct", () => { >() }) + it("schema views remain precise", () => { + const schema = Schema.Struct({ + a: Schema.FiniteFromString + }) + const asSchema = (schema: Schema.Schema) => schema + const asCodec = (schema: Schema.Codec) => schema + + expect(schema).type.toBeAssignableTo>() + expect(schema).type.toBeAssignableTo>() + + const schemaView = asSchema(schema) + expect(schemaView.Type).type.toBe<{ readonly a: number }>() + + const codecView = asCodec(schema) + expect(codecView.Type).type.toBe<{ readonly a: number }>() + expect(codecView.Encoded).type.toBe<{ readonly a: string }>() + expect(codecView.DecodingServices).type.toBe() + expect(codecView.EncodingServices).type.toBe() + }) + describe("mapFields", () => { describe("assign", () => { it("non-overlapping fields", () => { @@ -670,32 +690,42 @@ describe("StructWithRest", () => { >() }) - it("rejects decoded fields incompatible with string index signatures", () => { - expect(Schema.StructWithRest).type.not.toBeCallableWith( - Schema.Struct({ count: Schema.NumberFromString }), - [Schema.Record(Schema.String, Schema.String)] - ) + it("reports decoded fields incompatible with string index signatures", () => { + const schema = Schema.Struct({ count: Schema.NumberFromString }) + const records = [Schema.Record(Schema.String, Schema.String)] as const + + expect(Schema.StructWithRest).type.toBeCallableWith(schema, records) + expect>().type.toBe<{ + "incompatible index signatures": "count" + }>() }) - it("rejects encoded fields incompatible with string index signatures", () => { - expect(Schema.StructWithRest).type.not.toBeCallableWith( - Schema.Struct({ count: Schema.NumberFromString }), - [Schema.Record(Schema.String, Schema.Number)] - ) + it("reports encoded fields incompatible with string index signatures", () => { + const schema = Schema.Struct({ count: Schema.NumberFromString }) + const records = [Schema.Record(Schema.String, Schema.Number)] as const + + expect(Schema.StructWithRest).type.toBeCallableWith(schema, records) + expect>().type.toBe<{ + "incompatible index signatures": "count" + }>() }) it("allows optionalKey fields compatible with string index signatures", () => { - expect(Schema.StructWithRest).type.toBeCallableWith( - Schema.Struct({ a: Schema.optionalKey(Schema.String) }), - [Schema.Record(Schema.String, Schema.String)] - ) + const schema = Schema.Struct({ a: Schema.optionalKey(Schema.String) }) + const records = [Schema.Record(Schema.String, Schema.String)] as const + + expect(Schema.StructWithRest).type.toBeCallableWith(schema, records) + expect>().type.toBe() }) - it("rejects optional fields incompatible with string index signatures", () => { - expect(Schema.StructWithRest).type.not.toBeCallableWith( - Schema.Struct({ a: Schema.optional(Schema.String) }), - [Schema.Record(Schema.String, Schema.String)] - ) + it("reports optional fields incompatible with string index signatures", () => { + const schema = Schema.Struct({ a: Schema.optional(Schema.String) }) + const records = [Schema.Record(Schema.String, Schema.String)] as const + + expect(Schema.StructWithRest).type.toBeCallableWith(schema, records) + expect>().type.toBe<{ + "incompatible index signatures": "a" + }>() }) it("records mutability and optionality", () => { diff --git a/.context/effect/packages/effect/typetest/schema/Union.tst.ts b/.context/effect/packages/effect/typetest/schema/Union.tst.ts index d8c9a59ba..02bd9fc66 100644 --- a/.context/effect/packages/effect/typetest/schema/Union.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/Union.tst.ts @@ -30,6 +30,24 @@ describe("Union", () => { expect(schema.members).type.toBe() }) + it("schema views remain precise", () => { + const schema = Schema.Union([Schema.String, Schema.FiniteFromString]) + const asSchema = (schema: Schema.Schema) => schema + const asCodec = (schema: Schema.Codec) => schema + + expect(schema).type.toBeAssignableTo>() + expect(schema).type.toBeAssignableTo>() + + const schemaView = asSchema(schema) + expect(schemaView.Type).type.toBe() + + const codecView = asCodec(schema) + expect(codecView.Type).type.toBe() + expect(codecView.Encoded).type.toBe() + expect(codecView.DecodingServices).type.toBe() + expect(codecView.EncodingServices).type.toBe() + }) + describe("mapMembers", () => { it("appendElement", () => { const schema = Schema.Union([Schema.String, Schema.Number]).mapMembers(Tuple.appendElement(Schema.Boolean)) diff --git a/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts b/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts index b036fd8fe..afa0d0175 100644 --- a/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts @@ -38,7 +38,7 @@ describe("toArbitrary", () => { it("passes recursion metadata in the arbitrary context", () => { Schema.String.annotate({ toArbitrary: () => (fc, context) => { - expect(context.constraint).type.toBe() + expect(context.constraint).type.toBe() expect(context.recursion).type.toBe() return fc.string() } diff --git a/.context/effect/packages/opentelemetry/CHANGELOG.md b/.context/effect/packages/opentelemetry/CHANGELOG.md index d86334e11..7f0be17cf 100644 --- a/.context/effect/packages/opentelemetry/CHANGELOG.md +++ b/.context/effect/packages/opentelemetry/CHANGELOG.md @@ -1,5 +1,67 @@ # @effect/opentelemetry +## 4.0.0-beta.93 + +### Patch Changes + +- [#2511](https://github.com/Effect-TS/effect-smol/pull/2511) [`ceaffb8`](https://github.com/Effect-TS/effect-smol/commit/ceaffb8e532d77a528eef0c888b9e59683514a25) Thanks @alvarosevilla95! - prefix opentelemetry modules to avoid collisions + +- [#2511](https://github.com/Effect-TS/effect-smol/pull/2511) [`ceaffb8`](https://github.com/Effect-TS/effect-smol/commit/ceaffb8e532d77a528eef0c888b9e59683514a25) Thanks @alvarosevilla95! - Add `includeCauseInStack` option to `Cause.prettyErrors` + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2451](https://github.com/Effect-TS/effect-smol/pull/2451) [`4a005d0`](https://github.com/Effect-TS/effect-smol/commit/4a005d01955ac3c714c0ab24be50c7a0885b0abe) Thanks @StarpTech! - Preserve sampling decisions when adapting generic Effect external spans to OpenTelemetry. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/opentelemetry/package.json b/.context/effect/packages/opentelemetry/package.json index 406da0cf4..38d399b1c 100644 --- a/.context/effect/packages/opentelemetry/package.json +++ b/.context/effect/packages/opentelemetry/package.json @@ -1,7 +1,7 @@ { "name": "@effect/opentelemetry", "type": "module", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "license": "MIT", "description": "OpenTelemetry integration for Effect", "homepage": "https://effect.website", diff --git a/.context/effect/packages/opentelemetry/src/NodeSdk.ts b/.context/effect/packages/opentelemetry/src/NodeSdk.ts index e455d3d09..df91ec140 100644 --- a/.context/effect/packages/opentelemetry/src/NodeSdk.ts +++ b/.context/effect/packages/opentelemetry/src/NodeSdk.ts @@ -21,10 +21,10 @@ import * as Effect from "effect/Effect" import { constant, type LazyArg } from "effect/Function" import * as Layer from "effect/Layer" import { isNonEmpty } from "./internal/utilities.ts" -import * as Logger from "./Logger.ts" -import * as Metrics from "./Metrics.ts" +import * as Logger from "./OtelLogger.ts" +import * as Metrics from "./OtelMetrics.ts" +import * as Tracer from "./OtelTracer.ts" import * as Resource from "./Resource.ts" -import * as Tracer from "./Tracer.ts" /** * Configuration for the Node OpenTelemetry layer, including optional tracing, metrics, logging, resource, and shutdown settings. diff --git a/.context/effect/packages/opentelemetry/src/Logger.ts b/.context/effect/packages/opentelemetry/src/OtelLogger.ts similarity index 100% rename from .context/effect/packages/opentelemetry/src/Logger.ts rename to .context/effect/packages/opentelemetry/src/OtelLogger.ts diff --git a/.context/effect/packages/opentelemetry/src/Metrics.ts b/.context/effect/packages/opentelemetry/src/OtelMetrics.ts similarity index 96% rename from .context/effect/packages/opentelemetry/src/Metrics.ts rename to .context/effect/packages/opentelemetry/src/OtelMetrics.ts index fc8183eff..8674844e0 100644 --- a/.context/effect/packages/opentelemetry/src/Metrics.ts +++ b/.context/effect/packages/opentelemetry/src/OtelMetrics.ts @@ -100,14 +100,14 @@ export const registerProducer = ( * **Example** (Creating a metrics layer with temporality) * * ```ts - * import { Metrics } from "@effect/opentelemetry" + * import { OtelMetrics } from "@effect/opentelemetry" * import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics" * import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http" * * const metricExporter = new OTLPMetricExporter({ url: "" }) * * // Use delta temporality for backends like Datadog or Dynatrace - * const metricsLayer = Metrics.layer( + * const metricsLayer = OtelMetrics.layer( * () => new PeriodicExportingMetricReader({ * exporter: metricExporter, * exportIntervalMillis: 10000 @@ -116,7 +116,7 @@ export const registerProducer = ( * ) * * // Use cumulative temporality for backends like Prometheus (default) - * const cumulativeLayer = Metrics.layer( + * const cumulativeLayer = OtelMetrics.layer( * () => new PeriodicExportingMetricReader({ exporter: metricExporter }), * { temporality: "cumulative" } * ) diff --git a/.context/effect/packages/opentelemetry/src/Tracer.ts b/.context/effect/packages/opentelemetry/src/OtelTracer.ts similarity index 98% rename from .context/effect/packages/opentelemetry/src/Tracer.ts rename to .context/effect/packages/opentelemetry/src/OtelTracer.ts index 9ef619f77..8e34a0abb 100644 --- a/.context/effect/packages/opentelemetry/src/Tracer.ts +++ b/.context/effect/packages/opentelemetry/src/OtelTracer.ts @@ -329,7 +329,9 @@ const makeOtelSpan = (span: Tracer.Span, clock: Clock.Clock): Otel.Span => { recordException(exception, timeInput) { const time = convertOtelTimeInput(timeInput, clock) const cause = Cause.fail(exception) - const error = Cause.prettyErrors(cause)[0] + const error = Cause.prettyErrors(cause, { + includeCauseInStack: true + })[0] span.event(error.message, time, { "exception.type": error.name, "exception.message": error.message, @@ -484,7 +486,9 @@ export class OtelSpan implements Tracer.Span { this.span.setAttribute("span.label", "⚠︎ Interrupted") this.span.setAttribute("status.interrupted", true) } else { - const errors = Cause.prettyErrors(exit.cause) + const errors = Cause.prettyErrors(exit.cause, { + includeCauseInStack: true + }) if (errors.length > 0) { for (const error of errors) { this.span.recordException(error, hrTime) @@ -555,7 +559,7 @@ const makeTraceFlags = ( traceFlags = Context.getOrUndefined(span.annotations, OtelTraceFlags) } } - return traceFlags ?? Otel.TraceFlags.SAMPLED + return traceFlags ?? (span.sampled ? Otel.TraceFlags.SAMPLED : Otel.TraceFlags.NONE) } const makeTraceState = ( diff --git a/.context/effect/packages/opentelemetry/src/WebSdk.ts b/.context/effect/packages/opentelemetry/src/WebSdk.ts index 85e248dcc..0fae7d60b 100644 --- a/.context/effect/packages/opentelemetry/src/WebSdk.ts +++ b/.context/effect/packages/opentelemetry/src/WebSdk.ts @@ -19,10 +19,10 @@ import * as Effect from "effect/Effect" import { constant, type LazyArg } from "effect/Function" import * as Layer from "effect/Layer" import { isNonEmpty } from "./internal/utilities.ts" -import * as Logger from "./Logger.ts" -import * as Metrics from "./Metrics.ts" +import * as Logger from "./OtelLogger.ts" +import * as Metrics from "./OtelMetrics.ts" +import * as Tracer from "./OtelTracer.ts" import * as Resource from "./Resource.ts" -import * as Tracer from "./Tracer.ts" /** * Configuration for the Web OpenTelemetry layer, including resource metadata and optional tracing, metrics, and logging settings. diff --git a/.context/effect/packages/opentelemetry/src/index.ts b/.context/effect/packages/opentelemetry/src/index.ts index f181a380a..0fc343dc9 100644 --- a/.context/effect/packages/opentelemetry/src/index.ts +++ b/.context/effect/packages/opentelemetry/src/index.ts @@ -7,27 +7,27 @@ /** * @since 4.0.0 */ -export * as Logger from "./Logger.ts" +export * as NodeSdk from "./NodeSdk.ts" /** * @since 4.0.0 */ -export * as Metrics from "./Metrics.ts" +export * as OtelLogger from "./OtelLogger.ts" /** * @since 4.0.0 */ -export * as NodeSdk from "./NodeSdk.ts" +export * as OtelMetrics from "./OtelMetrics.ts" /** * @since 4.0.0 */ -export * as Resource from "./Resource.ts" +export * as OtelTracer from "./OtelTracer.ts" /** * @since 4.0.0 */ -export * as Tracer from "./Tracer.ts" +export * as Resource from "./Resource.ts" /** * @since 4.0.0 diff --git a/.context/effect/packages/opentelemetry/src/internal/metrics.ts b/.context/effect/packages/opentelemetry/src/internal/metrics.ts index 13f3e4ebd..24692997d 100644 --- a/.context/effect/packages/opentelemetry/src/internal/metrics.ts +++ b/.context/effect/packages/opentelemetry/src/internal/metrics.ts @@ -13,7 +13,7 @@ import type { InstrumentDescriptor } from "@opentelemetry/sdk-metrics/build/src/ import * as Arr from "effect/Array" import type * as Context from "effect/Context" import * as Metric from "effect/Metric" -import type * as Metrics from "../Metrics.ts" +import type * as Metrics from "../OtelMetrics.ts" const sdkName = "@effect/opentelemetry/Metrics" diff --git a/.context/effect/packages/opentelemetry/test/Logger.test.ts b/.context/effect/packages/opentelemetry/test/OtelLogger.test.ts similarity index 100% rename from .context/effect/packages/opentelemetry/test/Logger.test.ts rename to .context/effect/packages/opentelemetry/test/OtelLogger.test.ts diff --git a/.context/effect/packages/opentelemetry/test/Metrics.test.ts b/.context/effect/packages/opentelemetry/test/OtelMetrics.test.ts similarity index 100% rename from .context/effect/packages/opentelemetry/test/Metrics.test.ts rename to .context/effect/packages/opentelemetry/test/OtelMetrics.test.ts diff --git a/.context/effect/packages/opentelemetry/test/Tracer.test.ts b/.context/effect/packages/opentelemetry/test/OtelTracer.test.ts similarity index 68% rename from .context/effect/packages/opentelemetry/test/Tracer.test.ts rename to .context/effect/packages/opentelemetry/test/OtelTracer.test.ts index aee5b75c1..d4f8df6f8 100644 --- a/.context/effect/packages/opentelemetry/test/Tracer.test.ts +++ b/.context/effect/packages/opentelemetry/test/OtelTracer.test.ts @@ -1,11 +1,12 @@ import * as NodeSdk from "@effect/opentelemetry/NodeSdk" -import * as Tracer from "@effect/opentelemetry/Tracer" +import * as OtelTracer from "@effect/opentelemetry/OtelTracer" import { assert, describe, it } from "@effect/vitest" import * as OtelApi from "@opentelemetry/api" import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks" import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" +import * as EffectTracer from "effect/Tracer" const TracingLive = NodeSdk.layer(Effect.sync(() => ({ resource: { @@ -23,7 +24,7 @@ describe("Tracer", () => { it.effect("withSpan", () => Effect.gen(function*() { const span = yield* Effect.currentSpan - assert.instanceOf(span, Tracer.OtelSpan) + assert.instanceOf(span, OtelTracer.OtelSpan) }).pipe( Effect.withSpan("ok"), Effect.provide(TracingLive) @@ -36,7 +37,7 @@ describe("Tracer", () => { Effect.withSpan("A"), Effect.linkSpans(linkedSpan) ) - assert.instanceOf(span, Tracer.OtelSpan) + assert.instanceOf(span, OtelTracer.OtelSpan) assert.lengthOf(span.links, 1) }).pipe( Effect.scoped, @@ -49,10 +50,10 @@ describe("Tracer", () => { Effect.withSpan("child"), Effect.withSpan("parent") ) - assert.instanceOf(child, Tracer.OtelSpan) + assert.instanceOf(child, OtelTracer.OtelSpan) assert.strictEqual(child.name, "child") assert.isDefined(child.parent) - assert.strictEqual((child.parent.valueOrUndefined! as Tracer.OtelSpan).name, "parent") + assert.strictEqual((child.parent.valueOrUndefined! as OtelTracer.OtelSpan).name, "parent") }).pipe( Effect.provide(TracingLive) )) @@ -79,13 +80,28 @@ describe("Tracer", () => { it.effect("currentOtelSpan", () => Effect.gen(function*() { const span = yield* Effect.currentSpan - const otelSpan = yield* Tracer.currentOtelSpan - assert.strictEqual((span as Tracer.OtelSpan).span, otelSpan) + const otelSpan = yield* OtelTracer.currentOtelSpan + assert.strictEqual((span as OtelTracer.OtelSpan).span, otelSpan) }).pipe( Effect.withSpan("ok"), Effect.provide(TracingLive) )) + it.effect("preserves the sampling decision of generic external spans", () => + Effect.gen(function*() { + const span = yield* Effect.currentSpan + assert.instanceOf(span, OtelTracer.OtelSpan) + assert.strictEqual(span.span.spanContext().traceFlags, OtelApi.TraceFlags.NONE) + }).pipe( + Effect.withSpan("child"), + Effect.withParentSpan(EffectTracer.externalSpan({ + traceId: "1".repeat(32), + spanId: "2".repeat(16), + sampled: false + })), + Effect.provide(TracingLive) + )) + it.effect("records every pretty error", () => Effect.gen(function*() { const exporter = new InMemorySpanExporter() @@ -115,6 +131,35 @@ describe("Tracer", () => { assert.strictEqual(spanData.status.message, "first") })) + it.effect("renders nested error causes in the stacktrace", () => + Effect.gen(function*() { + const exporter = new InMemorySpanExporter() + const spanProcessor = new SimpleSpanProcessor(exporter) + const error = new Error("outer failure", { cause: new Error("inner cause") }) + + yield* Effect.die(error).pipe( + Effect.withSpan("error-span"), + Effect.andThen(Effect.never), // keep the exporter alive + Effect.provide(NodeSdk.layer(() => ({ + resource: { + serviceName: "test" + }, + spanProcessor: [spanProcessor] + }))), + Effect.forkChild({ startImmediately: true }) + ) + + const spanData = exporter.getFinishedSpans()[0] + if (spanData === undefined) { + return yield* Effect.die("Missing span data") + } + const exceptionEvent = spanData.events.find((event) => event.name === "exception") + assert(exceptionEvent !== undefined) + const stacktrace = exceptionEvent.attributes?.["exception.stacktrace"] + assert.isString(stacktrace) + assert.include(stacktrace as string, "[cause]: Error: inner cause") + })) + it.effect("withSpanContext", () => Effect.gen(function*() { const effect = Effect.gen(function*() { @@ -135,7 +180,7 @@ describe("Tracer", () => { }, async (span) => { try { const parent = await effect.pipe( - Tracer.withSpanContext(span.spanContext()), + OtelTracer.withSpanContext(span.spanContext()), Effect.provideContext(services), Effect.runPromise ) @@ -159,7 +204,7 @@ describe("Tracer", () => { it.effect("withSpan", () => Effect.gen(function*() { const span = yield* Effect.currentSpan - assert.notInstanceOf(span, Tracer.OtelSpan) + assert.notInstanceOf(span, OtelTracer.OtelSpan) }).pipe( Effect.withSpan("ok") )) diff --git a/.context/effect/packages/platform-browser/CHANGELOG.md b/.context/effect/packages/platform-browser/CHANGELOG.md index ad1fae838..fe0326319 100644 --- a/.context/effect/packages/platform-browser/CHANGELOG.md +++ b/.context/effect/packages/platform-browser/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/platform-browser +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/platform-browser/package.json b/.context/effect/packages/platform-browser/package.json index 7471d794f..150992239 100644 --- a/.context/effect/packages/platform-browser/package.json +++ b/.context/effect/packages/platform-browser/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-browser", "type": "module", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "license": "MIT", "description": "Platform specific implementations for the browser", "homepage": "https://effect.website", diff --git a/.context/effect/packages/platform-bun/CHANGELOG.md b/.context/effect/packages/platform-bun/CHANGELOG.md index d8ce1f43e..53595c21d 100644 --- a/.context/effect/packages/platform-bun/CHANGELOG.md +++ b/.context/effect/packages/platform-bun/CHANGELOG.md @@ -1,5 +1,69 @@ # @effect/platform-bun +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + - @effect/platform-node-shared@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + - @effect/platform-node-shared@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + - @effect/platform-node-shared@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + - @effect/platform-node-shared@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + - @effect/platform-node-shared@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + - @effect/platform-node-shared@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + - @effect/platform-node-shared@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + - @effect/platform-node-shared@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/platform-bun/package.json b/.context/effect/packages/platform-bun/package.json index ffaaa1d27..18c627fd9 100644 --- a/.context/effect/packages/platform-bun/package.json +++ b/.context/effect/packages/platform-bun/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-bun", "type": "module", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "license": "MIT", "description": "Platform specific implementations for the Bun runtime", "homepage": "https://effect.website", diff --git a/.context/effect/packages/platform-node-shared/CHANGELOG.md b/.context/effect/packages/platform-node-shared/CHANGELOG.md index 88fc825cd..68079f41c 100644 --- a/.context/effect/packages/platform-node-shared/CHANGELOG.md +++ b/.context/effect/packages/platform-node-shared/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/platform-node-shared +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/platform-node-shared/package.json b/.context/effect/packages/platform-node-shared/package.json index bc1ab1445..7805e1e81 100644 --- a/.context/effect/packages/platform-node-shared/package.json +++ b/.context/effect/packages/platform-node-shared/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node-shared", "type": "module", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "license": "MIT", "description": "Unified interfaces for common platform-specific services", "homepage": "https://effect.website", diff --git a/.context/effect/packages/platform-node/CHANGELOG.md b/.context/effect/packages/platform-node/CHANGELOG.md index 57a72fbbd..f9421c254 100644 --- a/.context/effect/packages/platform-node/CHANGELOG.md +++ b/.context/effect/packages/platform-node/CHANGELOG.md @@ -1,5 +1,71 @@ # @effect/platform-node +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + - @effect/platform-node-shared@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + - @effect/platform-node-shared@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + - @effect/platform-node-shared@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + - @effect/platform-node-shared@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + - @effect/platform-node-shared@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + - @effect/platform-node-shared@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + - @effect/platform-node-shared@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2463](https://github.com/Effect-TS/effect-smol/pull/2463) [`28b4196`](https://github.com/Effect-TS/effect-smol/commit/28b4196390d3ab83be1567b65440919a9061fcc3) Thanks @tim-smart! - Update `NodeHttpServer.layerConfig`'s type to report the same provided Node services as `NodeHttpServer.layer`. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + - @effect/platform-node-shared@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/platform-node/package.json b/.context/effect/packages/platform-node/package.json index f033453ef..a3f90d088 100644 --- a/.context/effect/packages/platform-node/package.json +++ b/.context/effect/packages/platform-node/package.json @@ -1,7 +1,7 @@ { "name": "@effect/platform-node", "type": "module", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "license": "MIT", "description": "Platform specific implementations for the Node.js runtime", "homepage": "https://effect.website", diff --git a/.context/effect/packages/platform-node/src/NodeHttpServer.ts b/.context/effect/packages/platform-node/src/NodeHttpServer.ts index f6838fb32..d9f8f59e6 100644 --- a/.context/effect/packages/platform-node/src/NodeHttpServer.ts +++ b/.context/effect/packages/platform-node/src/NodeHttpServer.ts @@ -441,8 +441,9 @@ export const layer = ( ) /** - * Provides a Node `HttpServer` and HTTP support services, reading the listen - * and shutdown options from a `Config` value. + * Provides a Node `HttpServer` together with the Node HTTP platform, ETag, + * and core Node platform services, reading the listen and shutdown options from + * a `Config` value. * * @category layers * @since 4.0.0 @@ -456,7 +457,7 @@ export const layerConfig = ( } > ): Layer.Layer< - HttpServer.HttpServer | FileSystem.FileSystem | Path.Path | HttpPlatform.HttpPlatform | Etag.Generator, + HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator, ServeError | Config.ConfigError > => Layer.mergeAll( diff --git a/.context/effect/packages/platform-node/test/RpcServer.test.ts b/.context/effect/packages/platform-node/test/RpcServer.test.ts index efd522af4..b4104c316 100644 --- a/.context/effect/packages/platform-node/test/RpcServer.test.ts +++ b/.context/effect/packages/platform-node/test/RpcServer.test.ts @@ -1,9 +1,9 @@ import { NodeHttpServer, NodeSocket, NodeSocketServer } from "@effect/platform-node" import { assert, describe, it } from "@effect/vitest" -import { Cause, Deferred, Effect, Layer } from "effect" +import { Cause, Deferred, Effect, Fiber, Layer, Ref, Schedule, Schema, Stream } from "effect" import { Entity, EntityProxy, EntityProxyServer, Sharding } from "effect/unstable/cluster" import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" -import { Rpc, RpcClient, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc" +import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc" import { SocketServer } from "effect/unstable/socket" import { e2eSuite, UsersClient } from "./fixtures/rpc-e2e.ts" import { RpcLive, User } from "./fixtures/rpc-schemas.ts" @@ -210,4 +210,83 @@ describe("RpcServer", () => { yield* Deferred.await(called) })) }) + + describe("unknown-tag isolation", () => { + const Ticker = Rpc.make("Ticker", { + success: Schema.Number, + stream: true + }) + const Ghost = Rpc.make("Ghost", { + payload: { value: Schema.String }, + success: Schema.String + }) + + const serverGroup = RpcGroup.make(Ticker) + const clientGroup = RpcGroup.make(Ticker, Ghost) + + const TickerHandlers = serverGroup.toLayer({ + Ticker: () => Stream.fromSchedule(Schedule.spaced("60 millis")) + }) + + const IsolationServer = RpcServer.layer(serverGroup).pipe( + Layer.provide(TickerHandlers), + Layer.provideMerge(RpcServer.layerProtocolSocketServer), + Layer.provideMerge(NodeSocketServer.layer({ port: 0 })), + Layer.provide(RpcSerialization.layerNdjson) + ) + const IsolationClient = RpcClient.layerProtocolSocket().pipe( + Layer.provide( + Effect.gen(function*() { + const server = yield* SocketServer.SocketServer + const address = server.address as SocketServer.TcpAddress + return NodeSocket.layerNet({ port: address.port }) + }).pipe(Layer.unwrap) + ), + Layer.provide(RpcSerialization.layerNdjson) + ) + + it.live( + "an unknown request tag fails only its own request, not other in-flight streams on the same connection", + () => + Effect.gen(function*() { + const client = yield* RpcClient.make(clientGroup) + + const received = yield* Ref.make>([]) + + const tickerFiber = yield* client.Ticker().pipe( + Stream.runForEach((value) => Ref.update(received, (xs) => [...xs, value])), + Effect.forkChild + ) + + yield* Effect.retry( + Effect.flatMap( + Ref.get(received), + (xs) => xs.length >= 2 ? Effect.void : Effect.fail("not enough ticks yet") + ), + { schedule: Schedule.spaced("50 millis"), times: 200 } + ) + + const ticksBeforeGhost = (yield* Ref.get(received)).length + assert.isAtLeast(ticksBeforeGhost, 2) + + const ghostExit = yield* client.Ghost({ value: "boo" }).pipe(Effect.exit) + assert.isTrue(ghostExit._tag === "Failure", "Ghost call should fail with the routing miss") + + yield* Effect.sleep("300 millis") + + const ticksAfterGhost = (yield* Ref.get(received)).length + const tickerStatus = tickerFiber.pollUnsafe() + + yield* Fiber.interrupt(tickerFiber) + + assert.isUndefined(tickerStatus, "Ticker stream must still be running after the unknown-tag failure") + assert.isAbove( + ticksAfterGhost, + ticksBeforeGhost, + "Ticker stream must keep emitting after the unknown-tag failure" + ) + }).pipe(Effect.provide(IsolationClient.pipe(Layer.provideMerge(IsolationServer)))), + { timeout: 30_000 } + ) + }) }) diff --git a/.context/effect/packages/sql/clickhouse/CHANGELOG.md b/.context/effect/packages/sql/clickhouse/CHANGELOG.md index c8304f493..f50f5891a 100644 --- a/.context/effect/packages/sql/clickhouse/CHANGELOG.md +++ b/.context/effect/packages/sql/clickhouse/CHANGELOG.md @@ -1,5 +1,71 @@ # @effect/sql-clickhouse +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + - @effect/platform-node@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + - @effect/platform-node@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + - @effect/platform-node@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + - @effect/platform-node@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + - @effect/platform-node@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + - @effect/platform-node@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + - @effect/platform-node@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`28b4196`](https://github.com/Effect-TS/effect-smol/commit/28b4196390d3ab83be1567b65440919a9061fcc3), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + - @effect/platform-node@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/clickhouse/package.json b/.context/effect/packages/sql/clickhouse/package.json index 5eeefc4f4..906dd16fb 100644 --- a/.context/effect/packages/sql/clickhouse/package.json +++ b/.context/effect/packages/sql/clickhouse/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-clickhouse", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A Clickhouse toolkit for Effect", diff --git a/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts b/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts index ada3cc3ed..fb5883eb1 100644 --- a/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts +++ b/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts @@ -290,6 +290,9 @@ export const make = ( executeValues(sql: string, params: ReadonlyArray) { return this.run(sql, params, "JSONCompact") } + executeValuesUnprepared(sql: string, params: ReadonlyArray) { + return this.executeValues(sql, params) + } executeUnprepared(sql: string, params: ReadonlyArray, transformRows?: any) { return this.execute(sql, params, transformRows) } diff --git a/.context/effect/packages/sql/d1/CHANGELOG.md b/.context/effect/packages/sql/d1/CHANGELOG.md index 68dcf0447..93166013c 100644 --- a/.context/effect/packages/sql/d1/CHANGELOG.md +++ b/.context/effect/packages/sql/d1/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-d1 +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/d1/package.json b/.context/effect/packages/sql/d1/package.json index b4f262fe2..e11966479 100644 --- a/.context/effect/packages/sql/d1/package.json +++ b/.context/effect/packages/sql/d1/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-d1", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A Cloudflare D1 integration for Effect", diff --git a/.context/effect/packages/sql/d1/src/D1Client.ts b/.context/effect/packages/sql/d1/src/D1Client.ts index c3be41b00..50078f5bc 100644 --- a/.context/effect/packages/sql/d1/src/D1Client.ts +++ b/.context/effect/packages/sql/d1/src/D1Client.ts @@ -167,6 +167,21 @@ export const make = ( }) ) + const runValuesUncached = ( + sql: string, + params: ReadonlyArray + ) => + Effect.tryPromise({ + try: () => { + return db.prepare(sql).bind(...params).raw() as Promise< + ReadonlyArray< + ReadonlyArray + > + > + }, + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) + }) + return identity({ execute(sql, params, transformRows) { return transformRows @@ -179,6 +194,9 @@ export const make = ( executeValues(sql, params) { return runValues(sql, params) }, + executeValuesUnprepared(sql, params) { + return runValuesUncached(sql, params) + }, executeUnprepared(sql, params, transformRows) { return transformRows ? Effect.map(runUncached(sql, params), transformRows) diff --git a/.context/effect/packages/sql/libsql/CHANGELOG.md b/.context/effect/packages/sql/libsql/CHANGELOG.md index 46ce81102..3c6645620 100644 --- a/.context/effect/packages/sql/libsql/CHANGELOG.md +++ b/.context/effect/packages/sql/libsql/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-libsql +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/libsql/package.json b/.context/effect/packages/sql/libsql/package.json index c2f07d9c4..96ea35d5a 100644 --- a/.context/effect/packages/sql/libsql/package.json +++ b/.context/effect/packages/sql/libsql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-libsql", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A libSQL toolkit for Effect", diff --git a/.context/effect/packages/sql/libsql/src/LibsqlClient.ts b/.context/effect/packages/sql/libsql/src/LibsqlClient.ts index 2ef62948b..4b574e039 100644 --- a/.context/effect/packages/sql/libsql/src/LibsqlClient.ts +++ b/.context/effect/packages/sql/libsql/src/LibsqlClient.ts @@ -240,6 +240,9 @@ export const make = ( executeValues(sql: string, params: ReadonlyArray) { return Effect.map(this.run(sql, params), (rows) => rows.map((row) => Array.from(row) as Array)) } + executeValuesUnprepared(sql: string, params: ReadonlyArray) { + return this.executeValues(sql, params) + } executeUnprepared( sql: string, params: ReadonlyArray, diff --git a/.context/effect/packages/sql/mssql/CHANGELOG.md b/.context/effect/packages/sql/mssql/CHANGELOG.md index 1f84f9f14..7b980c9b9 100644 --- a/.context/effect/packages/sql/mssql/CHANGELOG.md +++ b/.context/effect/packages/sql/mssql/CHANGELOG.md @@ -1,5 +1,69 @@ # @effect/sql-mssql +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- [#2397](https://github.com/Effect-TS/effect-smol/pull/2397) [`6412955`](https://github.com/Effect-TS/effect-smol/commit/641295595394c468094b1a246aa59d352d2a8ac9) Thanks @tuttarealstep! - Added some options for the MssqlClient: + - multiSubnetFailover + - cancelTimeout + - connectionRetryInterval + - maxRetriesOnTransientErrors + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/mssql/package.json b/.context/effect/packages/sql/mssql/package.json index 3a95081ab..a43a7f71a 100644 --- a/.context/effect/packages/sql/mssql/package.json +++ b/.context/effect/packages/sql/mssql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mssql", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A Microsoft SQL Server toolkit for Effect", diff --git a/.context/effect/packages/sql/mssql/src/MssqlClient.ts b/.context/effect/packages/sql/mssql/src/MssqlClient.ts index a02d5fe15..d58c0f734 100644 --- a/.context/effect/packages/sql/mssql/src/MssqlClient.ts +++ b/.context/effect/packages/sql/mssql/src/MssqlClient.ts @@ -213,6 +213,10 @@ export interface MssqlClientConfig { readonly username?: string | undefined readonly password?: Redacted.Redacted | undefined readonly connectTimeout?: Duration.Input | undefined + readonly cancelTimeout?: Duration.Input | undefined + readonly connectionRetryInterval?: Duration.Input | undefined + readonly multiSubnetFailover?: boolean | undefined + readonly maxRetriesOnTransientErrors?: number | undefined readonly minConnections?: number | undefined readonly maxConnections?: number | undefined @@ -280,13 +284,21 @@ export const make = ( port: options.port, database: options.database, trustServerCertificate: options.trustServer ?? true, + multiSubnetFailover: options.multiSubnetFailover, connectTimeout: options.connectTimeout ? Duration.toMillis(Duration.fromInputUnsafe(options.connectTimeout)) : undefined, rowCollectionOnRequestCompletion: true, useColumnNames: false, instanceName: options.instanceName, - encrypt: options.encrypt ?? false + encrypt: options.encrypt ?? false, + cancelTimeout: options.cancelTimeout + ? Duration.toMillis(Duration.fromInputUnsafe(options.cancelTimeout)) + : undefined, + connectionRetryInterval: options.connectionRetryInterval + ? Duration.toMillis(Duration.fromInputUnsafe(options.connectionRetryInterval)) + : undefined, + maxRetriesOnTransientErrors: options.maxRetriesOnTransientErrors } as ConnectionOptions, server: options.server, authentication: { @@ -416,6 +428,9 @@ export const make = ( executeValues(sql, params) { return run(sql, params, true) }, + executeValuesUnprepared(sql, params) { + return run(sql, params, true) + }, executeUnprepared(sql, params, transformRows) { return this.execute(sql, params, transformRows) }, diff --git a/.context/effect/packages/sql/mysql2/CHANGELOG.md b/.context/effect/packages/sql/mysql2/CHANGELOG.md index 0bb905d18..02eb74778 100644 --- a/.context/effect/packages/sql/mysql2/CHANGELOG.md +++ b/.context/effect/packages/sql/mysql2/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-mysql2 +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/mysql2/package.json b/.context/effect/packages/sql/mysql2/package.json index edbd8f5db..cfc396645 100644 --- a/.context/effect/packages/sql/mysql2/package.json +++ b/.context/effect/packages/sql/mysql2/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mysql2", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A MySQL toolkit for Effect", diff --git a/.context/effect/packages/sql/mysql2/src/MysqlClient.ts b/.context/effect/packages/sql/mysql2/src/MysqlClient.ts index 9694bb5a4..928f6c725 100644 --- a/.context/effect/packages/sql/mysql2/src/MysqlClient.ts +++ b/.context/effect/packages/sql/mysql2/src/MysqlClient.ts @@ -275,6 +275,9 @@ export const make = ( executeValues(sql: string, params: ReadonlyArray) { return this.run(sql, params, true) } + executeValuesUnprepared(sql: string, params: ReadonlyArray) { + return this.run(sql, params, true, "query") + } executeUnprepared( sql: string, params: ReadonlyArray, diff --git a/.context/effect/packages/sql/pg/CHANGELOG.md b/.context/effect/packages/sql/pg/CHANGELOG.md index 5417da8f9..113343d26 100644 --- a/.context/effect/packages/sql/pg/CHANGELOG.md +++ b/.context/effect/packages/sql/pg/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-pg +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/pg/package.json b/.context/effect/packages/sql/pg/package.json index db91a4364..73a8ea715 100644 --- a/.context/effect/packages/sql/pg/package.json +++ b/.context/effect/packages/sql/pg/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pg", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A PostgreSQL toolkit for Effect", diff --git a/.context/effect/packages/sql/pg/src/PgClient.ts b/.context/effect/packages/sql/pg/src/PgClient.ts index b9ff6c091..ea5f3f543 100644 --- a/.context/effect/packages/sql/pg/src/PgClient.ts +++ b/.context/effect/packages/sql/pg/src/PgClient.ts @@ -711,6 +711,9 @@ class ConnectionImpl implements Connection { ) }) } + executeValuesUnprepared(sql: string, params: ReadonlyArray) { + return this.executeValues(sql, params) + } executeUnprepared( sql: string, params: ReadonlyArray, diff --git a/.context/effect/packages/sql/pglite/CHANGELOG.md b/.context/effect/packages/sql/pglite/CHANGELOG.md index 41296337b..306dc980a 100644 --- a/.context/effect/packages/sql/pglite/CHANGELOG.md +++ b/.context/effect/packages/sql/pglite/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-pglite +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/pglite/package.json b/.context/effect/packages/sql/pglite/package.json index 4a351edc4..29328d880 100644 --- a/.context/effect/packages/sql/pglite/package.json +++ b/.context/effect/packages/sql/pglite/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pglite", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A PGlite toolkit for Effect", diff --git a/.context/effect/packages/sql/pglite/src/PgliteClient.ts b/.context/effect/packages/sql/pglite/src/PgliteClient.ts index d7fd0c6ee..95aa38a98 100644 --- a/.context/effect/packages/sql/pglite/src/PgliteClient.ts +++ b/.context/effect/packages/sql/pglite/src/PgliteClient.ts @@ -308,6 +308,16 @@ class PgliteConnection implements Connection { (result) => result.rows as ReadonlyArray> ) } + executeValuesUnprepared(sql: string, params: ReadonlyArray) { + return Effect.map( + Effect.tryPromise({ + try: () => this.pglite.query(sql, params as Array, { rowMode: "array" }), + catch: (cause) => + new SqlError({ reason: classifyError(cause, "Failed to execute statement", "executeValuesUnprepared") }) + }), + (result) => result.rows as ReadonlyArray> + ) + } executeUnprepared( sql: string, params: ReadonlyArray, diff --git a/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md b/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md index 9b399b9b7..873f18837 100644 --- a/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md @@ -1,5 +1,65 @@ # @effect/sql-sqlite-bun +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- [#2399](https://github.com/Effect-TS/effect-smol/pull/2399) [`6cbbdf3`](https://github.com/Effect-TS/effect-smol/commit/6cbbdf3a0265136032f33beb0439ac026ec9b4a6) Thanks @truffle-dev! - Fail with a typed `SqlError` when Bun SQLite statement preparation throws (for example a missing table or a syntax error), instead of letting the driver error escape as a defect, closes [#2385](https://github.com/Effect-TS/effect-smol/issues/2385). + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-bun/package.json b/.context/effect/packages/sql/sqlite-bun/package.json index dc47e5190..51f75c820 100644 --- a/.context/effect/packages/sql/sqlite-bun/package.json +++ b/.context/effect/packages/sql/sqlite-bun/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-bun", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts index 26408adcc..1e3493e41 100644 --- a/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts @@ -126,17 +126,21 @@ export const make = ( db.run("PRAGMA journal_mode = WAL;") } + const prepare = (sql: string, useSafeIntegers: boolean) => { + const statement = db.query(sql) + // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 + statement.safeIntegers(useSafeIntegers) + return statement + } + const run = ( sql: string, params: ReadonlyArray = [] ) => Effect.withFiber, SqlError>((fiber) => { - const statement = db.query(sql) const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers) - // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 - statement.safeIntegers(useSafeIntegers) try { - return Effect.succeed((statement.all(...(params as any)) ?? []) as Array) + return Effect.succeed((prepare(sql, useSafeIntegers).all(...(params as any)) ?? []) as Array) } catch (cause) { return Effect.fail(new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })) } @@ -147,14 +151,13 @@ export const make = ( params: ReadonlyArray = [] ) => Effect.withFiber, SqlError>((fiber) => { - const statement = db.query(sql) const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers) - // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 - statement.safeIntegers(useSafeIntegers) try { - return Effect.succeed((statement.values(...(params as any)) ?? []) as Array) + return Effect.succeed((prepare(sql, useSafeIntegers).values(...(params as any)) ?? []) as Array) } catch (cause) { - return Effect.fail(new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })) + return Effect.fail( + new SqlError({ reason: classifyError(cause, "Failed to execute statement", "executeValues") }) + ) } }) @@ -170,6 +173,9 @@ export const make = ( executeValues(sql, params) { return runValues(sql, params) }, + executeValuesUnprepared(sql, params) { + return runValues(sql, params) + }, executeUnprepared(sql, params, transformRows) { return this.execute(sql, params, transformRows) }, diff --git a/.context/effect/packages/sql/sqlite-do/CHANGELOG.md b/.context/effect/packages/sql/sqlite-do/CHANGELOG.md index b712d8d53..1f4136310 100644 --- a/.context/effect/packages/sql/sqlite-do/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-do/CHANGELOG.md @@ -1,5 +1,65 @@ # @effect/sql-sqlite-do +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- [#2217](https://github.com/Effect-TS/effect-smol/pull/2217) [`fe1b2d5`](https://github.com/Effect-TS/effect-smol/commit/fe1b2d53b7374db7a690ba96cc46392789d8ffe0) Thanks @dmmulroy! - Support Cloudflare Durable Object SQLite transactions by allowing `SqliteClient` to be configured with `DurableObjectStorage` and routing `withTransaction` through `storage.transaction`. + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-do/package.json b/.context/effect/packages/sql/sqlite-do/package.json index dfcc3b05f..035d7ae73 100644 --- a/.context/effect/packages/sql/sqlite-do/package.json +++ b/.context/effect/packages/sql/sqlite-do/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-do", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts index 345e19689..9df97d269 100644 --- a/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts @@ -1,18 +1,30 @@ /** * Connects Effect SQL to SQLite storage inside Cloudflare Durable Objects. * - * This module wraps a Durable Object `SqlStorage` handle and exposes it as both - * `SqliteClient` and the generic Effect SQL client. It serializes access, - * supports normal and streaming queries, converts returned `ArrayBuffer` values - * to `Uint8Array`, and provides layers for wiring the client into an Effect - * application. `updateValues` is not supported by this driver. + * This module adapts a Durable Object `SqlStorage` handle into both the + * Durable Object-specific `SqliteClient` service and the generic Effect + * `SqlClient` service. Use it from inside a Durable Object to run local + * per-object queries, repositories, migrations, transactional read/write + * workflows, and tests that exercise Cloudflare's SQLite-backed storage API. + * + * Durable Object SQLite storage is scoped to one object id, so each object + * instance has its own database. Callers can pass the `SqlStorage` handle for + * normal queries, or the full `DurableObjectStorage` when `withTransaction` or + * migrations need Cloudflare-managed transactions. This adapter serializes + * Effect SQL access through one connection; a transaction holds that permit for + * the lifetime of its scope, so keep transactions short, avoid suspending them + * across unrelated work, and use them when multi-statement writes must commit + * atomically. `SqlStorage.exec` returns `ArrayBuffer` values + * for SQLite blobs, which this client normalizes to `Uint8Array`, and SQLite + * does not support `updateValues`. * * @since 4.0.0 */ -import type { SqlStorage } from "@cloudflare/workers-types" +import type { DurableObjectStorage, SqlStorage } from "@cloudflare/workers-types" import * as Config from "effect/Config" import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" import * as Fiber from "effect/Fiber" import { identity } from "effect/Function" import * as Layer from "effect/Layer" @@ -22,7 +34,7 @@ import * as Stream from "effect/Stream" import * as Reactivity from "effect/unstable/reactivity/Reactivity" import * as Client from "effect/unstable/sql/SqlClient" import type { Connection } from "effect/unstable/sql/SqlConnection" -import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import { classifySqliteError, SqlError, UnknownError } from "effect/unstable/sql/SqlError" import * as Statement from "effect/unstable/sql/Statement" const ATTR_DB_SYSTEM_NAME = "db.system.name" @@ -73,22 +85,90 @@ export interface SqliteClient extends Client.SqlClient { */ export const SqliteClient = Context.Service("@effect/sql-sqlite-do/SqliteClient") +const SqliteTransaction = Context.Service( + "@effect/sql-sqlite-do/SqliteClient/SqliteTransaction" +) + /** - * Configuration for a Cloudflare Durable Object SQLite client, including the `SqlStorage` handle, span attributes, and query/result name transforms. + * Configuration for a Cloudflare Durable Object SQLite client, including either a `SqlStorage` handle or the full `DurableObjectStorage` for transaction support, span attributes, and query/result name transforms. * * @category models * @since 4.0.0 */ export interface SqliteClientConfig { - readonly db: SqlStorage + readonly db?: SqlStorage | undefined + readonly storage?: DurableObjectStorage | undefined readonly spanAttributes?: Record | undefined readonly transformResultNames?: ((str: string) => string) | undefined readonly transformQueryNames?: ((str: string) => string) | undefined } +const unsupportedTransaction = (message: string, operation: string) => + new SqlError({ + reason: new UnknownError({ + cause: new Error(message), + message, + operation + }) + }) + +const makeUnsupportedWithTransaction = + (message: string): Client.SqlClient["withTransaction"] => + (_effect: Effect.Effect): Effect.Effect => + Effect.fail(unsupportedTransaction(message, "transaction")) + +const makeStorageBackedWithTransaction = ( + storage: DurableObjectStorage, + connection: Connection, + semaphore: Semaphore.Semaphore +): Client.SqlClient["withTransaction"] => +(effect: Effect.Effect): Effect.Effect => + Effect.withFiber((fiber) => { + const services = fiber.context + const connOption = Context.getOption(services, SqliteTransaction) + if (connOption._tag === "Some") { + return Effect.fail( + unsupportedTransaction( + "Nested transactions are not supported by Cloudflare Durable Object SQLite storage", + "transaction" + ) + ) + } + + const effectWithTxn = Effect.provideContext( + effect, + Context.add(services, SqliteTransaction, [connection, 0] as const) + ) + + return semaphore.withPermits(1)( + Effect.callback((resume) => { + let interrupted = false + const promise = storage.transaction((txn) => + new Promise((resolve) => { + if (interrupted) return resolve() + resume(Effect.onExit(effectWithTxn, (exit) => { + if (Exit.isFailure(exit)) { + txn.rollback() + } + resolve() + // wait for the transaction to complete + return Effect.promise(() => promise) + })) + }) + ).catch((cause) => + resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Failed transaction", "transaction") }))) + ) + return Effect.suspend(() => { + interrupted = true + return Effect.promise(() => promise) + }) + }) + ) + }) + /** - * Creates a scoped Cloudflare Durable Object SQLite client around `SqlStorage`, serializing access and converting returned `ArrayBuffer` values to `Uint8Array`. + * Creates a scoped Cloudflare Durable Object SQLite client around Durable Object SQLite storage, serializing access and converting returned `ArrayBuffer` values to `Uint8Array`. * * @category constructors * @since 4.0.0 @@ -101,15 +181,19 @@ export const make = ( const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined + const db = options.storage?.sql ?? options.db - const makeConnection = Effect.gen(function*() { - const db = options.db + if (db === undefined) { + return yield* Effect.die("SqliteClient.make requires either a Durable Object storage or sql storage") + } + const sqlStorage = db + const makeConnection = Effect.gen(function*() { function* runIterator( sql: string, params: ReadonlyArray = [] ) { - const cursor = db.exec(sql, ...params) + const cursor = sqlStorage.exec(sql, ...params) const columns = cursor.columnNames for (const result of cursor.raw()) { const obj: any = {} @@ -136,7 +220,7 @@ export const make = ( ): Effect.Effect, SqlError, never> => Effect.try({ try: () => - Array.from(db.exec(sql, ...params).raw(), (row) => { + Array.from(sqlStorage.exec(sql, ...params).raw(), (row) => { for (let i = 0; i < row.length; i++) { const value = row[i] if (value instanceof ArrayBuffer) { @@ -160,6 +244,9 @@ export const make = ( executeValues(sql, params) { return runValues(sql, params) }, + executeValuesUnprepared(sql, params) { + return runValues(sql, params) + }, executeUnprepared(sql, params, transformRows) { return transformRows ? Effect.map(runStatement(sql, params), transformRows) @@ -194,22 +281,27 @@ export const make = ( ) }) - return Object.assign( - (yield* Client.make({ - acquirer, - compiler, - transactionAcquirer, - spanAttributes: [ - ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), - [ATTR_DB_SYSTEM_NAME, "sqlite"] - ], - transformRows - })) as SqliteClient, - { - [TypeId]: TypeId as TypeId, - config: options - } - ) + const client = (yield* Client.make({ + acquirer, + compiler, + transactionAcquirer, + transactionService: SqliteTransaction, + spanAttributes: [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"] + ], + transformRows + })) as SqliteClient + + return Object.assign(client, { + [TypeId]: TypeId as TypeId, + config: options, + withTransaction: options.storage + ? makeStorageBackedWithTransaction(options.storage, connection, semaphore) + : makeUnsupportedWithTransaction( + "Transactions require Durable Object storage; pass ctx.storage as the storage option" + ) + }) }) /** diff --git a/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts b/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts index 9cc790e7c..c02c16727 100644 --- a/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts +++ b/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts @@ -2,10 +2,25 @@ * Runs database migrations for Durable Object SQLite storage that uses Effect * SQL. * - * This module re-exports the shared migration loaders and errors, then provides - * `run` and `layer` helpers that apply pending migration files with the current - * `SqlClient`. The SQL client supplies the Durable Object storage connection; - * this module only handles migration execution. + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers that execute ordered migrations through the + * current Durable Object SQLite `SqlClient`. Use it when a Durable + * Object needs to create or upgrade its local schema during construction, before + * repositories or request handlers use the object storage, or in tests that + * exercise Durable Object persistence. + * + * Migrations are recorded in `effect_sql_migrations` by default and are loaded + * using the shared `_` file or record-key convention. The underlying + * storage is scoped to a Durable Object id, so running migrations for one object + * does not update any other object instance; run the migrator against the same + * `DurableObjectStorage`-backed client that the object uses for normal queries + * so migrations can run in Cloudflare-managed transactions. These SQL + * migrations are separate from Cloudflare's Durable Object class migrations, and + * the Durable Object must already be configured with SQLite storage before this + * module can apply schema changes. Repeated startup runs are expected and are + * guarded by the migrations table, but request handling should wait until the + * migration layer has finished. This adapter does not currently write SQLite + * schema dumps for `schemaDirectory`. * * @since 4.0.0 */ diff --git a/.context/effect/packages/sql/sqlite-do/test/Client.test.ts b/.context/effect/packages/sql/sqlite-do/test/Client.test.ts index 443f33fa0..c2cce8b88 100644 --- a/.context/effect/packages/sql/sqlite-do/test/Client.test.ts +++ b/.context/effect/packages/sql/sqlite-do/test/Client.test.ts @@ -1,7 +1,182 @@ -import { SqliteClient } from "@effect/sql-sqlite-do" +import type { DurableObjectStorage, SqlStorage } from "@cloudflare/workers-types" +import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-do" import { assert, describe, it } from "@effect/vitest" -import { Effect } from "effect" +import { Deferred, Effect, Fiber } from "effect" import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as SqlClient from "effect/unstable/sql/SqlClient" + +class FakeCursor { + readonly columnNames: ReadonlyArray + readonly rows: ReadonlyArray> + + constructor(rows: ReadonlyArray>, columns?: ReadonlyArray) { + this.columnNames = columns ?? Object.keys(rows[0] ?? {}) + this.rows = rows.map((row) => this.columnNames.map((column) => row[column])) + } + + *raw() { + yield* this.rows.map((row) => [...row]) + } +} + +class FakeSqlStorage { + readonly statements: Array = [] + tables = new Map>>() + columns = new Map>() + + snapshot() { + return { + tables: new Map(Array.from(this.tables, ([table, rows]) => [table, rows.map((row) => ({ ...row }))])), + columns: new Map(Array.from(this.columns, ([table, columns]) => [table, [...columns]])) + } + } + + restore(snapshot: ReturnType) { + this.tables = snapshot.tables + this.columns = snapshot.columns + } + + exec(sql: string, ...params: ReadonlyArray) { + this.statements.push(sql) + const statement = normalizeSql(sql) + if (/^(BEGIN|COMMIT|ROLLBACK|SAVEPOINT)\b/i.test(statement)) { + throw new Error(`Unsupported transaction SQL: ${statement}`) + } + + if (/^CREATE TABLE\b/i.test(statement)) { + const match = /^CREATE TABLE(?: IF NOT EXISTS)?\s+("[^"]+"|\w+)\s*\((.*)\)$/i.exec(statement) + if (match !== null) { + const table = unquote(match[1]) + if (!this.tables.has(table)) { + this.tables.set(table, []) + this.columns.set( + table, + match[2].split(",").map((part) => unquote(part.trim().split(/\s+/)[0])).filter((column) => + column.length > 0 && column.toUpperCase() !== "PRIMARY" + ) + ) + } + } + return new FakeCursor([]) + } + + if (/^INSERT INTO\b/i.test(statement)) { + const match = /^INSERT INTO\s+("[^"]+"|\w+)\s*\(([^)]+)\)\s+VALUES\s+(.+)$/i.exec(statement) + if (match !== null) { + const table = unquote(match[1]) + const columns = match[2].split(",").map((column) => unquote(column.trim())) + const rows = this.tables.get(table) ?? [] + const tableColumns = this.columns.get(table) ?? [...columns] + if (!this.tables.has(table)) { + this.tables.set(table, rows) + this.columns.set(table, tableColumns) + } + let paramIndex = 0 + for (const group of valueGroups(match[3])) { + const values = group.split(",").map((value) => parseValue(value.trim(), params, () => paramIndex++)) + const row: Record = {} + if (tableColumns.includes("id") && !columns.includes("id")) { + row.id = rows.length + 1 + } + for (let i = 0; i < columns.length; i++) { + row[columns[i]] = values[i] + } + if (tableColumns.includes("created_at") && row.created_at === undefined) { + row.created_at = "current_timestamp" + } + rows.push(row) + } + } + return new FakeCursor([]) + } + + if (/^SELECT\b/i.test(statement)) { + const match = + /^SELECT\s+(.+)\s+FROM\s+("[^"]+"|\w+)(?:\s+WHERE\s+("[^"]+"|\w+)\s*=\s*(\?|('[^']*')|\d+))?(?:\s+ORDER BY\s+("[^"]+"|\w+)\s+DESC)?$/i + .exec(statement) + if (match !== null) { + const table = unquote(match[2]) + const selectedColumns = match[1].trim() === "*" + ? this.columns.get(table) ?? [] + : match[1].split(",").map((column) => unquote(column.trim())) + let rows = [...(this.tables.get(table) ?? [])] + if (match[3] !== undefined) { + const column = unquote(match[3]) + const value = match[4] === "?" ? params[0] : parseValue(match[4], [], () => 0) + rows = rows.filter((row) => row[column] === value) + } + if (match[6] !== undefined) { + const column = unquote(match[6]) + rows.sort((a, b) => Number(b[column]) - Number(a[column])) + } + return new FakeCursor(rows, selectedColumns) + } + } + + return new FakeCursor([]) + } +} + +class FakeDurableObjectStorage { + readonly sql = new FakeSqlStorage() + transactionCalls = 0 + rollbackCalls = 0 + + transaction(body: (txn: { rollback: () => void }) => Promise): Promise { + this.transactionCalls++ + const snapshot = this.sql.snapshot() + let rolledBack = false + const txn = { + rollback: () => { + this.rollbackCalls++ + rolledBack = true + } + } + return Promise.resolve().then(() => body(txn)).then( + (value) => { + if (rolledBack) { + this.sql.restore(snapshot) + } + return value + }, + (error) => { + this.sql.restore(snapshot) + throw error + } + ) + } +} + +const normalizeSql = (sql: string) => sql.replace(/;$/, "").replace(/\s+/g, " ").trim() + +const unquote = (value: string) => value.replace(/^"|"$/g, "") + +const valueGroups = (values: string): Array => + values.match(/\(([^)]*)\)/g)?.map((group) => group.slice(1, -1)) ?? [] + +const parseValue = ( + value: string, + params: ReadonlyArray, + nextParamIndex: () => number +): unknown => { + if (value === "?") { + return params[nextParamIndex()] + } + if (/^'.*'$/.test(value)) { + return value.slice(1, -1) + } + const number = Number(value) + return Number.isNaN(number) ? value : number +} + +const makeClient = (config: SqliteClient.SqliteClientConfig) => + SqliteClient.make(config).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + ) + +const hasForbiddenTransactionSql = (db: FakeSqlStorage) => + db.statements.some((statement) => /^(BEGIN|COMMIT|ROLLBACK|SAVEPOINT)\b/i.test(normalizeSql(statement))) describe("Client", () => { it.effect("classifies native errors without stable sqlite codes as UnknownError", () => @@ -10,15 +185,135 @@ describe("Client", () => { exec: () => { throw new Error("boom") } - } as any + } as unknown as SqlStorage - const client = yield* SqliteClient.make({ db: failingDb }) + const client = yield* makeClient({ db: failingDb }) const error = yield* Effect.flip(client`SELECT 1`) assert.strictEqual(error.reason._tag, "UnknownError") - }).pipe( - Effect.scoped, - Effect.provide(Reactivity.layer) - )) + })) + + it.effect("db-only clients support normal queries", () => + Effect.gen(function*() { + const db = new FakeSqlStorage() + const sql = yield* makeClient({ db: db as unknown as SqlStorage }) + + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql`INSERT INTO test (name) VALUES (${"hello"})` + const rows = yield* sql`SELECT * FROM test` + + assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) + })) + + it.effect("db-only transactions fail clearly without transaction SQL", () => + Effect.gen(function*() { + const db = new FakeSqlStorage() + const sql = yield* makeClient({ db: db as unknown as SqlStorage }) + + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const error = yield* Effect.flip(sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`)) + const rows = yield* sql`SELECT * FROM test` + + assert.strictEqual(error.reason._tag, "UnknownError") + assert.match(error.message, /Transactions require Durable Object storage/) + assert.deepStrictEqual(rows, []) + assert.strictEqual(hasForbiddenTransactionSql(db), false) + })) + + it.effect("storage-backed transactions use DurableObjectStorage.transaction", () => + Effect.gen(function*() { + const storage = new FakeDurableObjectStorage() + const sql = yield* makeClient({ storage: storage as unknown as DurableObjectStorage }) + + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`) + const rows = yield* sql`SELECT * FROM test` + + assert.strictEqual(storage.transactionCalls, 1) + assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) + assert.strictEqual(hasForbiddenTransactionSql(storage.sql), false) + })) + + it.effect("storage-backed failed transactions roll back and re-emit the original failure", () => + Effect.gen(function*() { + const storage = new FakeDurableObjectStorage() + const sql = yield* makeClient({ storage: storage as unknown as DurableObjectStorage }) + + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const error = yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe( + Effect.andThen(Effect.fail("boom")), + sql.withTransaction, + Effect.flip + ) + const rows = yield* sql`SELECT * FROM test` + + assert.strictEqual(error, "boom") + assert.deepStrictEqual(rows, []) + assert.strictEqual(storage.rollbackCalls, 1) + })) + + it.effect("storage-backed interrupted transactions roll back before release", () => + Effect.gen(function*() { + const storage = new FakeDurableObjectStorage() + const sql = yield* makeClient({ storage: storage as unknown as DurableObjectStorage }) + const inserted = yield* Deferred.make() + + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const fiber = yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe( + Effect.tap(() => Deferred.succeed(inserted, void 0)), + Effect.andThen(Effect.never), + sql.withTransaction, + Effect.forkChild + ) + yield* Deferred.await(inserted) + yield* Fiber.interrupt(fiber) + const rows = yield* sql`SELECT * FROM test` + + assert.deepStrictEqual(rows, []) + assert.strictEqual(storage.rollbackCalls, 1) + })) + + it.effect("nested transactions fail clearly without savepoint SQL", () => + Effect.gen(function*() { + const storage = new FakeDurableObjectStorage() + const sql = yield* makeClient({ storage: storage as unknown as DurableObjectStorage }) + + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const error = yield* sql.withTransaction( + Effect.gen(function*() { + yield* sql`INSERT INTO test (name) VALUES ('outer')` + yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('inner')`) + }) + ).pipe(Effect.flip) + const rows = yield* sql`SELECT * FROM test` + + assert.strictEqual(error.reason._tag, "UnknownError") + assert.match(error.message, /Nested transactions are not supported/) + assert.deepStrictEqual(rows, []) + assert.strictEqual(storage.rollbackCalls, 1) + assert.strictEqual(hasForbiddenTransactionSql(storage.sql), false) + })) + + it.effect("SqliteMigrator.run works with storage-backed transactions", () => + Effect.gen(function*() { + const storage = new FakeDurableObjectStorage() + const sql = yield* makeClient({ storage: storage as unknown as DurableObjectStorage }) + + const completed = yield* SqliteMigrator.run({ + loader: SqliteMigrator.fromRecord({ + "1_create": Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + yield* sql`CREATE TABLE migrated (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql`INSERT INTO migrated (name) VALUES ('ok')` + }) + }) + }).pipe(Effect.provideService(SqlClient.SqlClient, sql)) + const rows = yield* sql`SELECT * FROM migrated` + const migrations = yield* sql`SELECT * FROM effect_sql_migrations` - it.effect("should work", () => Effect.void) + assert.deepStrictEqual(completed, [[1, "create"]]) + assert.deepStrictEqual(rows, [{ id: 1, name: "ok" }]) + assert.deepStrictEqual(migrations, [{ migration_id: 1, name: "create", created_at: "current_timestamp" }]) + assert.strictEqual(storage.transactionCalls, 1) + assert.strictEqual(hasForbiddenTransactionSql(storage.sql), false) + })) }) diff --git a/.context/effect/packages/sql/sqlite-node/CHANGELOG.md b/.context/effect/packages/sql/sqlite-node/CHANGELOG.md index 3b9f52e89..6bda6b886 100644 --- a/.context/effect/packages/sql/sqlite-node/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-node/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-sqlite-node +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-node/package.json b/.context/effect/packages/sql/sqlite-node/package.json index 1ba00dd3d..c6ac1c6ee 100644 --- a/.context/effect/packages/sql/sqlite-node/package.json +++ b/.context/effect/packages/sql/sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-node", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts index 37d11ee74..518384c2d 100644 --- a/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts @@ -199,6 +199,25 @@ export const make = ( (statement) => Effect.sync(() => statement.reader && statement.raw(false)) ) + const runValuesUnprepared = ( + sql: string, + params: ReadonlyArray + ) => + Effect.try({ + try: () => { + const statement = db.prepare(sql) + if (statement.reader) { + statement.raw(true) + return statement.all(...params) as ReadonlyArray< + ReadonlyArray + > + } + statement.run(...params) + return [] + }, + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) + }) + return identity({ execute(sql, params, transformRows) { return transformRows @@ -211,6 +230,9 @@ export const make = ( executeValues(sql, params) { return runValues(sql, params) }, + executeValuesUnprepared(sql, params) { + return runValuesUnprepared(sql, params) + }, executeUnprepared(sql, params, transformRows) { const effect = runStatement(db.prepare(sql), params ?? [], false) return transformRows ? Effect.map(effect, transformRows) : effect diff --git a/.context/effect/packages/sql/sqlite-node/test/Client.test.ts b/.context/effect/packages/sql/sqlite-node/test/Client.test.ts index 1611382ea..97c758c58 100644 --- a/.context/effect/packages/sql/sqlite-node/test/Client.test.ts +++ b/.context/effect/packages/sql/sqlite-node/test/Client.test.ts @@ -23,6 +23,8 @@ describe("Client", () => { assert.deepStrictEqual(response, []) response = yield* sql`SELECT * FROM test` assert.deepStrictEqual(response, [{ id: 1, name: "hello" }]) + response = yield* sql`SELECT * FROM test`.valuesUnprepared + assert.deepStrictEqual(response, [[1, "hello"]]) response = yield* sql`INSERT INTO test (name) VALUES ('world')`.pipe(sql.withTransaction) assert.deepStrictEqual(response, []) response = yield* sql`SELECT * FROM test` diff --git a/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md b/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md index 0d4b93ad5..acb170dae 100644 --- a/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-sqlite-react-native +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-react-native/package.json b/.context/effect/packages/sql/sqlite-react-native/package.json index 66607def7..323865f09 100644 --- a/.context/effect/packages/sql/sqlite-react-native/package.json +++ b/.context/effect/packages/sql/sqlite-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-react-native", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts index b476f1676..9ea80d2a9 100644 --- a/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts @@ -178,6 +178,9 @@ export const make = ( executeValues(sql, params) { return run(sql, params, true) }, + executeValuesUnprepared(sql, params) { + return run(sql, params, true) + }, executeUnprepared(sql, params, transformRows) { return this.execute(sql, params, transformRows) }, diff --git a/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md b/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md index 6f9227065..efbc1390d 100644 --- a/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md @@ -1,5 +1,63 @@ # @effect/sql-sqlite-wasm +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2462](https://github.com/Effect-TS/effect-smol/pull/2462) [`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64) Thanks @tim-smart! - Add `Statement.valuesUnprepared` for returning unprepared SQL statement rows as arrays. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-wasm/package.json b/.context/effect/packages/sql/sqlite-wasm/package.json index 6b55b7fc0..2bdb05c9c 100644 --- a/.context/effect/packages/sql/sqlite-wasm/package.json +++ b/.context/effect/packages/sql/sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-wasm", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts index 73378c958..d68f540b8 100644 --- a/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts @@ -208,6 +208,9 @@ export const makeMemory = ( executeValues(sql, params) { return run(sql, params, "array") }, + executeValuesUnprepared(sql, params) { + return run(sql, params, "array") + }, executeUnprepared(sql, params, transformRows) { return this.execute(sql, params, transformRows) }, @@ -394,6 +397,9 @@ export const make = ( executeValues(sql, params) { return run(sql, params, "array") }, + executeValuesUnprepared(sql, params) { + return run(sql, params, "array") + }, executeUnprepared(sql, params, transformRows) { return this.execute(sql, params, transformRows) }, diff --git a/.context/effect/packages/tools/ai-docgen/src/main.ts b/.context/effect/packages/tools/ai-docgen/src/main.ts index 1824c5842..32c854482 100755 --- a/.context/effect/packages/tools/ai-docgen/src/main.ts +++ b/.context/effect/packages/tools/ai-docgen/src/main.ts @@ -60,7 +60,9 @@ Command.make("effect-ai-docgen", { directory, output, watch }).pipe( ) const directoryToMarkdown = Effect.fn("directoryToMarkdown")( - function*(directory): Effect.fn.Return { + function*( + directory: string + ): Effect.fn.Return { const pathService = yield* Path.Path const fs = yield* FileSystem.FileSystem diff --git a/.context/effect/packages/tools/bundle/README.md b/.context/effect/packages/tools/bundle/README.md new file mode 100644 index 000000000..d182b129e --- /dev/null +++ b/.context/effect/packages/tools/bundle/README.md @@ -0,0 +1,226 @@ +# Bundle Size Development Workflow + +This package contains the internal bundle-size tooling used to measure Effect +entrypoints with Rollup, minification, and gzip. + +## AI Agent Instructions + +Use this workflow when a user wants to quickly understand how a local source +change affects bundle size for one or more explicitly selected fixture files. + +Do not scan `scratchpad/` or `packages/tools/bundle/fixtures/` for this workflow. +Only measure files that the user explicitly names, or files you create +specifically for the current investigation. + +Before running a comparison, ask the user which mode they want: + +- default cleanup mode for a single check or one-off investigation; +- keep-base mode for many repeated measurements, followed by explicit cleanup at + the end. + +Use cleanup mode unless the user says they expect to run multiple trials. + +If the user asks what a generated bundle is made of, use the bundle composition +workflow instead of the size comparison workflow. + +### Analyze Bundle Composition + +Use this workflow when the user gives an explicit fixture and wants to +understand which modules make up the produced bundle. + +Agent procedure: + +1. Confirm the exact fixture path or paths to analyze. Do not scan directories. +2. Run `pnpm bundle-analyze` with only those explicit paths. +3. Read the printed Markdown table and identify the generated `*.raw-data.json` + path for each fixture. +4. Analyze `*.raw-data.json` first. Use `*.treemap.html` only when the user wants + a visual artifact to open. +5. Report the largest modules, notable dependency groups, and any surprising + inclusions. Mention the generated artifact paths in the answer. + +Prefer the repository-level wrapper: + +```sh +pnpm bundle-analyze scratchpad/my-fixture.ts +``` + +You can pass multiple explicit files: + +```sh +pnpm bundle-analyze \ + scratchpad/schema-codec.ts \ + scratchpad/schema-arbitrary.ts +``` + +The wrapper builds the current checkout with `pnpm build:tsgo` before generating +analysis artifacts, so the bundle is not produced from stale `dist` files. + +By default, artifacts are written to `tmp/bundle-analysis`. Use `--output-dir` +to choose another directory: + +```sh +pnpm bundle-analyze --output-dir tmp/schema-analysis scratchpad/my-fixture.ts +``` + +For each selected fixture, the tool writes: + +- `.min.js`: the generated Rollup output for inspection; +- `.treemap.html`: an interactive treemap for human inspection; +- `.raw-data.json`: raw visualizer data suitable for AI analysis. + +The analysis build disables identifier mangling so module and export names stay +readable in the visualizer output. Use `bundle-compare-selected` or `report` +when exact size measurement is the goal. + +After running the command, inspect the Markdown table it prints to find the exact +paths. For an AI analysis, read the `*.raw-data.json` file first and summarize +the largest modules and dependency groups. Use the `*.treemap.html` file when +the user wants to inspect the bundle visually. + +The raw data contains a tree plus module metadata. For a quick size-oriented +summary, rank module parts by `renderedLength` or `gzipLength`, then map each +part through `metaUid` to `nodeMetas[metaUid].id`. + +Example inspection command: + +```sh +node -e 'const data = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); console.log(Object.entries(data.nodeParts).map(([uid, part]) => ({ uid, id: data.nodeMetas[part.metaUid]?.id, rendered: part.renderedLength, gzip: part.gzipLength })).sort((a, b) => b.rendered - a.rendered).slice(0, 20))' tmp/bundle-analysis/my-fixture.raw-data.json +``` + +Do not treat the generated `.min.js` size from this workflow as the exact bundle +size comparison number. The analysis build keeps names readable for the +visualizer. Use `bundle-compare-selected` or `report` for exact size reporting. + +Do not use this workflow to compare against a base ref. Use +`bundle-compare-selected` for size impact and `bundle-analyze` for current bundle +composition. + +### Compare Explicit Scratchpad Fixtures + +Prefer the repository-level wrapper: + +```sh +pnpm bundle-compare-selected --base main scratchpad/my-fixture.ts +``` + +You can pass multiple explicit files: + +```sh +pnpm bundle-compare-selected --base main \ + scratchpad/schema-codec.ts \ + scratchpad/schema-arbitrary.ts +``` + +If `--base` is omitted, the script compares against `main`. + +By default, the wrapper removes `tmp/bundle-base` before exiting, including on +failure. This avoids leaving extra git worktree state after a one-off +measurement. + +For repeated measurements where the user explicitly wants a faster feedback loop, +pass `--keep-base`: + +```sh +pnpm bundle-compare-selected --base main --keep-base scratchpad/my-fixture.ts +``` + +When using `--keep-base`, clean up `tmp/bundle-base` after the last measurement. +The explicit final cleanup command is: + +```sh +git worktree remove --force tmp/bundle-base +``` + +The wrapper: + +- builds the current checkout with `pnpm build:tsgo`; +- creates or reuses `tmp/bundle-base` at the requested base ref; +- builds the base checkout when needed and writes + `tmp/bundle-base/.bundle-build-stamp` only after a successful build; +- reuses a kept base checkout only when its HEAD and `.bundle-build-stamp` + both match the requested base ref; +- copies only the selected files into the base checkout at the same relative + paths; +- invokes the internal bundle CLI to compare current vs base sizes. + +The selected files are copied into the base checkout so the report isolates +source changes instead of changes to the fixture text. + +Expected output is a Markdown table: + +```md +| File Name | Current Size | Previous Size | Difference | +| :------------------------- | :----------: | :-----------: | :---------------: | +| `scratchpad/my-fixture.ts` | 42.10 KB | 40.80 KB | +1.30 KB (+3.19%) | +``` + +### Internal CLI + +Use the internal CLI directly only when the base checkout is already prepared +and built: + +```sh +node packages/tools/bundle/src/bin.ts compare-selected \ + --base-dir tmp/bundle-base \ + scratchpad/my-fixture.ts +``` + +The `--base-dir` value must be the root of the base checkout, not the base +fixture directory. + +### Current Size Only + +If the user only asks for the current bundled size, use: + +```sh +pnpm --dir packages/tools/bundle report ../../../scratchpad/my-fixture.ts +``` + +This does not compare against a base ref. + +### Fixture Guidance + +Keep temporary fixtures in `scratchpad/`. + +Good temporary fixtures are small, focused entrypoints that import public +package APIs: + +```ts +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" + +const schema = Schema.Struct({ + name: Schema.String +}) + +Schema.decodeUnknownEffect(schema)({ name: "effect" }).pipe(Effect.runFork) +``` + +Prefer self-contained fixtures. The compare-selected workflow copies only the +explicit entry files into the base checkout. If a fixture imports local relative +helpers, either avoid that shape or make the entry fixture self-contained before +measuring. + +Do not add temporary fixtures to `packages/tools/bundle/fixtures/`. That +directory is for stable comparison fixtures used by the regular bundle-size +workflow. + +### Cleanup + +The wrapper cleans up `tmp/bundle-base` by default. If `--keep-base` was used for +a multi-run investigation, remove the worktree when the user is done. + +To remove it: + +```sh +git worktree remove --force tmp/bundle-base +``` + +To verify that cleanup succeeded: + +```sh +git worktree list +``` + +Only the main repository worktree should remain. diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-class.ts b/.context/effect/packages/tools/bundle/fixtures/schema-class.ts new file mode 100644 index 000000000..a20c480c0 --- /dev/null +++ b/.context/effect/packages/tools/bundle/fixtures/schema-class.ts @@ -0,0 +1,12 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" + +class A extends Schema.Class("A")({ + a: Schema.String, + b: Schema.optional(Schema.FiniteFromString), + c: Schema.Array(Schema.String) +}) {} + +Schema.decodeUnknownEffect(A)({ a: "a", b: 1, c: ["c"] }).pipe( + Effect.runFork +) diff --git a/.context/effect/packages/tools/bundle/src/Cli.ts b/.context/effect/packages/tools/bundle/src/Cli.ts index caebff7c5..b5a37052c 100644 --- a/.context/effect/packages/tools/bundle/src/Cli.ts +++ b/.context/effect/packages/tools/bundle/src/Cli.ts @@ -5,8 +5,10 @@ * exposes the workflows used when maintaining fixture bundle sizes. `compare` * builds the package's local fixtures and compares them with matching fixture * files from another checkout, `report` bundles an explicit list of entrypoints - * and prints a Markdown table, and `visualize` prompts for local fixtures before - * producing visualization output for inspection. + * and prints a Markdown table, `compare-selected` compares explicit entrypoints + * against a base checkout, `visualize-selected` analyzes explicit entrypoints, + * and `visualize` prompts for local fixtures before producing visualization + * output for inspection. * * Command output is intentionally split by workflow. `compare` requires an * existing `--base-dir` (`-b`) and writes its Markdown report to `--output-path` @@ -76,7 +78,8 @@ const visualize = Command.make("visualize", { outputDirectory }).pipe( })) }) - yield* reporter.visualize({ paths, outputDirectory }) + const report = yield* reporter.visualize({ paths, outputDirectory }) + yield* Console.log(report) })) ) @@ -93,6 +96,22 @@ const report = Command.make("report", { paths: reportPaths }).pipe( })) ) +const compareSelected = Command.make("compare-selected", { baseDirectory, paths: reportPaths }).pipe( + Command.withHandler(Effect.fnUntraced(function*({ baseDirectory, paths }) { + const reporter = yield* Reporter + const report = yield* reporter.reportSelectedComparison({ baseDirectory, paths }) + yield* Console.log(report) + })) +) + +const visualizeSelected = Command.make("visualize-selected", { outputDirectory, paths: reportPaths }).pipe( + Command.withHandler(Effect.fnUntraced(function*({ outputDirectory, paths }) { + const reporter = yield* Reporter + const report = yield* reporter.visualize({ outputDirectory, paths }) + yield* Console.log(report) + })) +) + /** * Bundle analysis CLI command with subcommands for comparing fixture bundle sizes, reporting selected fixtures, and generating visualizations. * @@ -100,5 +119,5 @@ const report = Command.make("report", { paths: reportPaths }).pipe( * @since 4.0.0 */ export const cli = Command.make("bundle").pipe( - Command.withSubcommands([compare, report, visualize]) + Command.withSubcommands([compare, compareSelected, report, visualize, visualizeSelected]) ) diff --git a/.context/effect/packages/tools/bundle/src/Plugins.ts b/.context/effect/packages/tools/bundle/src/Plugins.ts index dfdd1e4be..89eb748b3 100644 --- a/.context/effect/packages/tools/bundle/src/Plugins.ts +++ b/.context/effect/packages/tools/bundle/src/Plugins.ts @@ -25,9 +25,23 @@ import type * as Path from "effect/Path" import * as Predicate from "effect/Predicate" import type { Plugin } from "rollup" import esbuild from "rollup-plugin-esbuild" -import { visualizer } from "rollup-plugin-visualizer" +import { type PluginVisualizerOptions, visualizer } from "rollup-plugin-visualizer" const EFFECT_PACKAGE_REGEX = /^(@effect\/[\w-]+|effect)(\/.*)?$/ +const TYPE_SCRIPT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]) + +const toLocalDistPath = (pathService: Path.Path, packageDir: string, resolvedId: string): string => { + const srcDir = pathService.join(packageDir, "src") + const relative = pathService.relative(srcDir, resolvedId) + if (relative === "" || relative.startsWith("..") || pathService.isAbsolute(relative)) { + return resolvedId + } + const extension = pathService.extname(relative) + if (!TYPE_SCRIPT_EXTENSIONS.has(extension)) { + return resolvedId + } + return pathService.join(packageDir, "dist", relative.slice(0, -extension.length) + ".js") +} /** * Options for configuring Rollup plugins. @@ -40,6 +54,19 @@ export interface PluginOptions { readonly minify?: boolean | undefined readonly mangle?: boolean | undefined readonly visualize?: boolean | undefined + readonly visualizations?: ReadonlyArray | undefined +} + +/** + * Output generated by the Rollup visualizer plugin. + * + * @category options + * @since 4.0.0 + */ +export interface VisualizationOutput { + readonly filename: string + readonly template: NonNullable + readonly title?: string | undefined } interface ResolvedPluginOptions { @@ -47,13 +74,15 @@ interface ResolvedPluginOptions { readonly minify: boolean readonly mangle: boolean readonly visualize: boolean + readonly visualizations: ReadonlyArray } const defaultPluginOptions: ResolvedPluginOptions = { nodeTarget: "node20", minify: true, mangle: true, - visualize: false + visualize: false, + visualizations: [] } /** @@ -63,7 +92,8 @@ const resolvePluginOptions = (options: PluginOptions): ResolvedPluginOptions => nodeTarget: options.nodeTarget ?? defaultPluginOptions.nodeTarget, minify: options.minify ?? defaultPluginOptions.minify, mangle: options.mangle ?? defaultPluginOptions.mangle, - visualize: options.visualize ?? defaultPluginOptions.visualize + visualize: options.visualize ?? defaultPluginOptions.visualize, + visualizations: options.visualizations ?? defaultPluginOptions.visualizations }) /** @@ -79,13 +109,15 @@ export const createResolveLocalPackageImports = (pathService: Path.Path): Plugin const match = source.match(EFFECT_PACKAGE_REGEX) if (Predicate.isNotNull(match)) { const packageName = match[1] - const subpath = match[2] - const resolved = await this.resolve(`${packageName}/package.json`, importer, { skipSelf: true }) + const packageJson = await this.resolve(`${packageName}/package.json`, importer, { skipSelf: true }) + if (packageJson === null) return null + const resolved = await this.resolve(source, importer, { skipSelf: true }) if (resolved === null) return null - const packageDir = pathService.dirname(resolved.id) - const modulePath = subpath ? subpath.slice(1) : "index" - const distPath = pathService.join(packageDir, "dist", `${modulePath}.js`) - return { id: distPath, external: false } + return { + ...resolved, + id: toLocalDistPath(pathService, pathService.dirname(packageJson.id), resolved.id), + external: false + } } return null } @@ -120,7 +152,20 @@ export const createPlugins = (pathService: Path.Path, options: PluginOptions = { }) ] - if (resolved.visualize) { + if (resolved.visualizations.length > 0) { + for (const output of resolved.visualizations) { + const visualizerOptions: PluginVisualizerOptions = { + filename: output.filename, + gzipSize: true, + open: false, + template: output.template + } + if (output.title !== undefined) { + visualizerOptions.title = output.title + } + plugins.push(visualizer(visualizerOptions)) + } + } else if (resolved.visualize) { plugins.push(visualizer({ open: true, gzipSize: true diff --git a/.context/effect/packages/tools/bundle/src/Reporter.ts b/.context/effect/packages/tools/bundle/src/Reporter.ts index c78057a66..cfde4812b 100644 --- a/.context/effect/packages/tools/bundle/src/Reporter.ts +++ b/.context/effect/packages/tools/bundle/src/Reporter.ts @@ -23,6 +23,7 @@ import * as FileSystem from "effect/FileSystem" import { constFalse } from "effect/Function" import * as Layer from "effect/Layer" import * as Path from "effect/Path" +import { fileURLToPath } from "node:url" import { Fixtures } from "./Fixtures.ts" import type { BundleStats } from "./Rollup.ts" import { Rollup } from "./Rollup.ts" @@ -68,6 +69,17 @@ export interface ReportSelectedOptions { readonly paths: ReadonlyArray } +/** + * Options for generating a bundle size comparison report for explicit entry files against a base checkout. + * + * @category options + * @since 4.0.0 + */ +export interface ReportSelectedComparisonOptions { + readonly baseDirectory: string + readonly paths: ReadonlyArray +} + /** * Context service for producing bundle size reports and visualizations from Rollup-generated fixture stats. * @@ -82,6 +94,7 @@ export class Reporter extends Context.Service()( const path = yield* Path.Path const { fixtures, fixturesDir } = yield* Fixtures const rollup = yield* Rollup + const currentDirectory = path.resolve(fileURLToPath(new URL("../../../../", import.meta.url))) const calculateDifference = (current: BundleStats, previous: BundleStats) => { const currSize = current.sizeInBytes @@ -102,28 +115,49 @@ export class Reporter extends Context.Service()( } } - const createReport = (curr: ReadonlyArray, prev: ReadonlyArray): string => { + const createComparisonReport = ( + entries: ReadonlyArray<{ + readonly current: BundleStats + readonly previous: BundleStats + readonly filename: string + }> + ): string => { const lines: Array = [ "| File Name | Current Size | Previous Size | Difference |", "|:----------|:------------:|:-------------:|:----------:|" ] - for (const current of curr) { - const previous = prev.find((previous) => { - return path.basename(previous.path) === path.basename(current.path) - }) ?? current + for (const { current, previous, filename } of entries) { const comparison = calculateDifference(current, previous) - const filename = `\`${comparison.filename}\`` const currKb = `${comparison.currKb} KB` const prevKb = `${comparison.prevKb} KB` const diffKb = `${comparison.diffKb} KB` const diffPct = `${comparison.diffPct.toFixed(2)}%` const sign = comparison.diff === 0 ? "" : comparison.diff > 0 ? "+" : "-" - const line = `| ${filename} | ${currKb} | ${prevKb} | ${sign}${diffKb} (${sign}${diffPct}) |` + const line = `| \`${filename}\` | ${currKb} | ${prevKb} | ${sign}${diffKb} (${sign}${diffPct}) |` lines.push(line) } return lines.join("\n") + "\n" } + const createReport = (curr: ReadonlyArray, prev: ReadonlyArray): string => { + const entries: Array<{ + readonly current: BundleStats + readonly previous: BundleStats + readonly filename: string + }> = [] + for (const current of curr) { + const previous = prev.find((previous) => { + return path.basename(previous.path) === path.basename(current.path) + }) ?? current + entries.push({ + current, + previous, + filename: path.basename(current.path) + }) + } + return createComparisonReport(entries) + } + const createSelectedReport = (stats: ReadonlyArray): string => { const lines: Array = [ "| File Name | Current Size |", @@ -140,6 +174,22 @@ export class Reporter extends Context.Service()( return lines.join("\n") + "\n" } + const createVisualizationReport = (paths: ReadonlyArray, outputDirectory: string): string => { + const lines: Array = [ + "| File Name | Generated Bundle | Treemap | Raw Data |", + "|:----------|:----------------|:--------|:---------|" + ] + for (const entryPath of paths) { + const name = path.parse(entryPath).name + const filename = path.relative(currentDirectory, path.resolve(entryPath)) + const minified = path.join(outputDirectory, `${name}.min.js`) + const treemap = path.join(outputDirectory, `${name}.treemap.html`) + const rawData = path.join(outputDirectory, `${name}.raw-data.json`) + lines.push(`| \`${filename}\` | \`${minified}\` | \`${treemap}\` | \`${rawData}\` |`) + } + return lines.join("\n") + "\n" + } + const report = Effect.fn("Reporter.report")( function*(options: ReportOptions) { yield* Effect.logInfo(`Found ${fixtures.length} files to bundle`) @@ -168,11 +218,13 @@ export class Reporter extends Context.Service()( const visualize = Effect.fn("Reporter.visualize")( function*(options: VisualizeOptions) { + yield* fs.makeDirectory(options.outputDirectory, { recursive: true }) yield* rollup.bundleAll({ paths: options.paths, outputDirectory: options.outputDirectory, visualize: true }) + return createVisualizationReport(options.paths, options.outputDirectory) } ) @@ -185,8 +237,55 @@ export class Reporter extends Context.Service()( } ) + const reportSelectedComparison = Effect.fn("Reporter.reportSelectedComparison")( + function*(options: ReportSelectedComparisonOptions) { + yield* Effect.logInfo(`Found ${options.paths.length} files to compare`) + const baseDirectory = path.resolve(options.baseDirectory) + const currentPaths = options.paths.map((currentPath) => path.resolve(currentPath)) + const previousPaths = yield* Effect.forEach( + currentPaths, + Effect.fnUntraced(function*(currentPath) { + const relativePath = path.relative(currentDirectory, currentPath) + if (relativePath === "" || relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + return yield* Effect.fail( + new ReporterError({ + cause: `Selected bundle entry must be inside ${currentDirectory}: ${currentPath}` + }) + ) + } + const previousPath = path.join(baseDirectory, relativePath) + yield* fs.makeDirectory(path.dirname(previousPath), { recursive: true }) + yield* fs.copy(currentPath, previousPath, { overwrite: true }) + return previousPath + }), + { concurrency: currentPaths.length } + ) + + const [currentStats, previousStats] = yield* Effect.all([ + rollup.bundleAll({ paths: currentPaths }), + rollup.bundleAll({ paths: previousPaths }) + ], { concurrency: 2 }) + + const entries: Array<{ + readonly current: BundleStats + readonly previous: BundleStats + readonly filename: string + }> = [] + for (let i = 0; i < currentStats.length; i++) { + entries.push({ + current: currentStats[i]!, + previous: previousStats[i]!, + filename: path.relative(currentDirectory, currentStats[i]!.path) + }) + } + yield* Effect.logInfo("Bundling complete! Generating bundle size report...") + return createComparisonReport(entries) + } + ) + return { report, + reportSelectedComparison, reportSelected, visualize } as const diff --git a/.context/effect/packages/tools/bundle/src/Rollup.ts b/.context/effect/packages/tools/bundle/src/Rollup.ts index b84a229d0..8d900a0b2 100644 --- a/.context/effect/packages/tools/bundle/src/Rollup.ts +++ b/.context/effect/packages/tools/bundle/src/Rollup.ts @@ -28,7 +28,7 @@ import * as Stream from "effect/Stream" import { createGzip } from "node:zlib" import type { RollupOptions } from "rollup" import { rollup } from "rollup" -import { createPlugins } from "./Plugins.ts" +import { createPlugins, type VisualizationOutput } from "./Plugins.ts" /** * Error raised when Rollup bundling, output generation, or bundle size measurement fails. @@ -88,12 +88,34 @@ export class Rollup extends Context.Service()( const pathService = yield* Path.Path const fs = yield* FileSystem.FileSystem + const createVisualizationOutputs = (options: BundleOptions): ReadonlyArray => { + if (!options.visualize || !options.outputDirectory) { + return [] + } + const name = pathService.parse(options.path).name + return [ + { + filename: pathService.join(options.outputDirectory, `${name}.treemap.html`), + template: "treemap", + title: `${name} bundle treemap` + }, + { + filename: pathService.join(options.outputDirectory, `${name}.raw-data.json`), + template: "raw-data", + title: `${name} bundle raw data` + } + ] + } + const getRollupOptions = (options: BundleOptions): RollupOptions => ({ input: options.path, output: { format: "esm" }, - plugins: createPlugins(pathService, { visualize: options.visualize }), + plugins: createPlugins(pathService, { + visualize: options.visualize, + visualizations: createVisualizationOutputs(options) + }), onwarn: (warning, next) => { if (warning.code === "THIS_IS_UNDEFINED") return next(warning) diff --git a/.context/effect/packages/tools/bundle/test/Plugins.test.ts b/.context/effect/packages/tools/bundle/test/Plugins.test.ts new file mode 100644 index 000000000..9c272d97b --- /dev/null +++ b/.context/effect/packages/tools/bundle/test/Plugins.test.ts @@ -0,0 +1,74 @@ +import { createResolveLocalPackageImports } from "@effect/bundle/Plugins" +import { assert, describe, it } from "@effect/vitest" +import type * as EffectPath from "effect/Path" +import * as path from "node:path" +import { fileURLToPath } from "node:url" +import type { Plugin } from "rollup" + +type Resolved = { + readonly id: string + readonly external: false +} + +type ResolveId = ( + this: { + readonly resolve: ( + source: string, + importer?: string, + options?: { readonly skipSelf?: boolean } + ) => Promise + }, + source: string, + importer?: string +) => Promise + +const packageDir = fileURLToPath(new URL("../../../effect", import.meta.url)) +const pathService = path as unknown as EffectPath.Path + +const getResolveId = (plugin: Plugin): ResolveId => { + assert.strictEqual(typeof plugin.resolveId, "function") + return plugin.resolveId as ResolveId +} + +const resolved = (id: string): Resolved => ({ + id, + external: false +}) + +describe("createResolveLocalPackageImports", () => { + it("resolves directory package exports to dist index files", async () => { + const resolveId = getResolveId(createResolveLocalPackageImports(pathService)) + const result = await resolveId.call({ + resolve: async (source) => { + switch (source) { + case "effect/package.json": + return resolved(path.join(packageDir, "package.json")) + case "effect/testing": + return resolved(path.join(packageDir, "src", "testing", "index.ts")) + default: + return null + } + } + }, "effect/testing") + + assert.deepStrictEqual(result, resolved(path.join(packageDir, "dist", "testing", "index.js"))) + }) + + it("keeps flat package exports on flat dist files", async () => { + const resolveId = getResolveId(createResolveLocalPackageImports(pathService)) + const result = await resolveId.call({ + resolve: async (source) => { + switch (source) { + case "effect/package.json": + return resolved(path.join(packageDir, "package.json")) + case "effect/Schema": + return resolved(path.join(packageDir, "src", "Schema.ts")) + default: + return null + } + } + }, "effect/Schema") + + assert.deepStrictEqual(result, resolved(path.join(packageDir, "dist", "Schema.js"))) + }) +}) diff --git a/.context/effect/packages/tools/bundle/vitest.config.ts b/.context/effect/packages/tools/bundle/vitest.config.ts new file mode 100644 index 000000000..c8a52c182 --- /dev/null +++ b/.context/effect/packages/tools/bundle/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type ViteUserConfig } from "vitest/config" +import shared from "../../../vitest.shared.ts" + +const config: ViteUserConfig = {} + +export default mergeConfig(shared, config) diff --git a/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts b/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts index 40be7827c..ce2686b14 100644 --- a/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts +++ b/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts @@ -1748,6 +1748,7 @@ export interface JSDocModel { readonly version: 2 readonly generatedBy: "@effect/jsdocs" readonly generatedAt: string + readonly inputHash?: string readonly files: ReadonlyArray readonly apis: ReadonlyArray } @@ -1763,6 +1764,65 @@ export interface ExtractJSDocsOptions extends JSDocConfig { readonly cwd?: string } +function addInputFile(files: Set, filename: string) { + const normalized = path.resolve(filename) + if (fs.existsSync(normalized) && fs.statSync(normalized).isFile()) { + files.add(normalized) + } +} + +export function computeJSDocInputHash(options: ExtractJSDocsOptions): string { + const cwd = path.resolve(options.cwd ?? process.cwd()) + const hash = crypto.createHash("sha256") + const files = new Set() + + hash.update(JSON.stringify({ + tsconfig: options.tsconfig, + include: options.include, + exclude: options.exclude ?? [], + output: options.output + })) + + addInputFile(files, path.join(cwd, "jsdocs.config.json")) + addInputFile(files, path.resolve(cwd, options.tsconfig)) + + for ( + const filename of globSync([...options.include], { + cwd, + absolute: true, + nodir: true, + ignore: ["**/node_modules/**"] + }) + ) { + addInputFile(files, filename) + } + + for ( + const filename of globSync([ + "package.json", + "packages/**/package.json", + "tsconfig*.json", + "packages/**/tsconfig*.json" + ], { + cwd, + absolute: true, + nodir: true, + ignore: ["**/node_modules/**"] + }) + ) { + addInputFile(files, filename) + } + + for (const filename of Array.from(files).sort()) { + hash.update("\0") + hash.update(normalizeFile(cwd, filename)) + hash.update("\0") + hash.update(hashSource(fs.readFileSync(filename, "utf8"))) + } + + return hash.digest("hex") +} + function isIdentifierName(value: string): boolean { return /^[$A-Z_a-z][$\w]*$/.test(value) } @@ -3271,6 +3331,7 @@ export function extractJSDocsSync(options: ExtractJSDocsOptions): JSDocModel { version: 2, generatedBy: "@effect/jsdocs", generatedAt: new Date().toISOString(), + inputHash: computeJSDocInputHash(options), files: withPublicSeeDiagnostics.files, apis: withPublicSeeDiagnostics.apis } diff --git a/.context/effect/packages/tools/jsdocs/src/bin.ts b/.context/effect/packages/tools/jsdocs/src/bin.ts index 4bc7ee9ff..bf77d0c8b 100755 --- a/.context/effect/packages/tools/jsdocs/src/bin.ts +++ b/.context/effect/packages/tools/jsdocs/src/bin.ts @@ -1,13 +1,8 @@ #!/usr/bin/env node -import { extractJSDocsSync, loadJSDocConfig, writeJSDocModel } from "./Jsdocs.ts" +import * as path from "node:path" +import { computeJSDocInputHash, extractJSDocsSync, loadJSDocConfig, readJSDocModel, writeJSDocModel } from "./Jsdocs.ts" -try { - const cwd = process.cwd() - const check = process.argv.includes("--check") - const config = loadJSDocConfig(cwd) - const model = extractJSDocsSync({ cwd, ...config }) - writeJSDocModel(cwd, config.output, model) - process.stdout.write(`Wrote ${config.output}\n`) +function reportDiagnostics(model: ReturnType, check: boolean) { const diagnostics = model.files.reduce((count, file) => count + file.diagnostics.length, 0) if (diagnostics > 0) { for (const file of model.files) { @@ -17,6 +12,23 @@ try { } if (check) process.exitCode = 1 } +} + +try { + const cwd = process.cwd() + const check = process.argv.includes("--check") + const config = loadJSDocConfig(cwd) + const inputHash = computeJSDocInputHash({ cwd, ...config }) + const cached = readJSDocModel(path.resolve(cwd, config.output)) + if (cached._tag === "Success" && cached.value.inputHash === inputHash) { + process.stdout.write(`Skipped ${config.output}\n`) + reportDiagnostics(cached.value, check) + } else { + const model = extractJSDocsSync({ cwd, ...config }) + writeJSDocModel(cwd, config.output, model) + process.stdout.write(`Wrote ${config.output}\n`) + reportDiagnostics(model, check) + } } catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`) process.exitCode = 1 diff --git a/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts b/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts index de7434eb7..dc4b0008f 100644 --- a/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts +++ b/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts @@ -1,4 +1,4 @@ -import { extractJSDocsSync, parseJSDoc } from "@effect/jsdocs" +import { computeJSDocInputHash, extractJSDocsSync, parseJSDoc } from "@effect/jsdocs" import { assert, describe, it } from "@effect/vitest" import * as fs from "node:fs" import * as os from "node:os" @@ -120,6 +120,59 @@ export const makeValue = () => 1 }) }) + it("stores a stable input hash for cache checks", () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-")) + fs.mkdirSync(path.join(cwd, "src"), { recursive: true }) + fs.writeFileSync( + path.join(cwd, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" }, + include: ["src/**/*.ts"] + }) + ) + fs.writeFileSync( + path.join(cwd, "jsdocs.config.json"), + JSON.stringify({ + tsconfig: "tsconfig.json", + include: ["src/**/*.ts"], + output: ".data/jsdocs.json" + }) + ) + fs.writeFileSync( + path.join(cwd, "package.json"), + JSON.stringify({ + name: "@effect/sample", + type: "module", + exports: { ".": "./src/index.ts", "./*": "./src/*.ts" } + }) + ) + fs.writeFileSync(path.join(cwd, "src/index.ts"), `export * as Foo from "./Foo.ts"\n`) + fs.writeFileSync( + path.join(cwd, "src/Foo.ts"), + `/** + * Creates a value. + * + * @category constructors + * @since 1.0.0 + */ +export const makeValue = () => 1 +` + ) + const options = { + cwd, + tsconfig: "tsconfig.json", + include: ["src/**/*.ts"], + output: ".data/jsdocs.json" + } + const inputHash = computeJSDocInputHash(options) + const model = extractJSDocsSync(options) + assert.strictEqual(model.inputHash, inputHash) + assert.strictEqual(computeJSDocInputHash(options), inputHash) + + fs.appendFileSync(path.join(cwd, "src/Foo.ts"), "\n") + assert.notStrictEqual(computeJSDocInputHash(options), inputHash) + }) + it("extracts typechecker signatures for top-level functions", () => { const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-")) fs.mkdirSync(path.join(cwd, "src"), { recursive: true }) diff --git a/.context/effect/packages/tools/openapi-generator/CHANGELOG.md b/.context/effect/packages/tools/openapi-generator/CHANGELOG.md index 2a14c4a56..15d204144 100644 --- a/.context/effect/packages/tools/openapi-generator/CHANGELOG.md +++ b/.context/effect/packages/tools/openapi-generator/CHANGELOG.md @@ -1,5 +1,73 @@ # @effect/openapi-generator +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + - @effect/platform-node@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + - @effect/platform-node@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + - @effect/platform-node@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + - @effect/platform-node@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + - @effect/platform-node@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + - @effect/platform-node@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- [#2469](https://github.com/Effect-TS/effect-smol/pull/2469) [`12fcf35`](https://github.com/Effect-TS/effect-smol/commit/12fcf35b92879f70a0e3b32b18359986439b9283) Thanks @jbmusso! - Fix the generated SSE `sseRequest` helper to reference `Schema.ConstraintDecoder` instead of the no-longer-exported `Schema.Decoder`. + + For specs with `text/event-stream` responses the generator emitted a helper typed as `Schema.Decoder`, but `Schema` exports that decode-only interface as `ConstraintDecoder`, so generated clients failed to compile (`'"effect/Schema"' has no exported member named 'Decoder'`). The emitted helper now uses `Schema.ConstraintDecoder`. + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + - @effect/platform-node@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`28b4196`](https://github.com/Effect-TS/effect-smol/commit/28b4196390d3ab83be1567b65440919a9061fcc3), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + - @effect/platform-node@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/tools/openapi-generator/package.json b/.context/effect/packages/tools/openapi-generator/package.json index 7830bebbb..13adf44e0 100644 --- a/.context/effect/packages/tools/openapi-generator/package.json +++ b/.context/effect/packages/tools/openapi-generator/package.json @@ -1,7 +1,7 @@ { "name": "@effect/openapi-generator", "type": "module", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "license": "MIT", "description": "Generate Effect Schema types, HTTP clients, and HttpApi modules from OpenAPI specifications", "homepage": "https://effect.website", diff --git a/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts b/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts index 066489bda..e511a6407 100644 --- a/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts +++ b/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts @@ -263,11 +263,11 @@ export const make = ( ): ${name} => { ${helpers.join("\n ")} const decodeSuccess = - (schema: Schema) => + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => HttpClientResponse.schemaBodyJson(schema)(response) const decodeError = - (tag: Tag, schema: Schema) => + (tag: Tag, schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap( HttpClientResponse.schemaBodyJson(schema)(response), @@ -917,7 +917,7 @@ const sseRequestSource = (_importName: string) => Type, DecodingServices >( - schema: Schema.Decoder + schema: Schema.ConstraintDecoder ) => ( request: HttpClientRequest.HttpClientRequest diff --git a/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts b/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts index 359b629a9..bf93c46b4 100644 --- a/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts +++ b/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts @@ -416,11 +416,11 @@ export const make = ( : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse) } const decodeSuccess = - (schema: Schema) => + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => HttpClientResponse.schemaBodyJson(schema)(response) const decodeError = - (tag: Tag, schema: Schema) => + (tag: Tag, schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap( HttpClientResponse.schemaBodyJson(schema)(response), @@ -520,7 +520,8 @@ export const TestClientError = ( `import * as Sse from "effect/unstable/encoding/Sse"`, `readonly "streamEventsSse": () => Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof StreamEvents200Sse.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry, typeof StreamEvents200Sse.DecodingServices>`, `"streamEventsSse": () => HttpClientRequest.get(\`/events\`).pipe(`, - `sseRequest(StreamEvents200Sse)` + `sseRequest(StreamEvents200Sse)`, + `schema: Schema.ConstraintDecoder` ] )) diff --git a/.context/effect/packages/tools/oxc/oxlintrc.json b/.context/effect/packages/tools/oxc/oxlintrc.json index 7bbf4da34..10cddc50e 100644 --- a/.context/effect/packages/tools/oxc/oxlintrc.json +++ b/.context/effect/packages/tools/oxc/oxlintrc.json @@ -20,6 +20,7 @@ }], "effect/no-js-extension-imports": "error", "effect/no-opaque-instance-fields": "error", + "effect/no-unused-internal": "error", "effect/jsdocs": "error", // Tune native rules // Import rules diff --git a/.context/effect/packages/tools/oxc/src/oxlint/index.ts b/.context/effect/packages/tools/oxc/src/oxlint/index.ts index 4eab7a8f0..5f8d63651 100644 --- a/.context/effect/packages/tools/oxc/src/oxlint/index.ts +++ b/.context/effect/packages/tools/oxc/src/oxlint/index.ts @@ -3,6 +3,7 @@ import noBigIntLiterals from "./rules/no-bigint-literals.ts" import noImportFromBarrelPackage from "./rules/no-import-from-barrel-package.ts" import noJsExtensionImports from "./rules/no-js-extension-imports.ts" import noOpaqueInstanceFields from "./rules/no-opaque-instance-fields.ts" +import noUnusedInternal from "./rules/no-unused-internal.ts" export default { meta: { @@ -13,6 +14,7 @@ export default { "no-import-from-barrel-package": noImportFromBarrelPackage, "no-js-extension-imports": noJsExtensionImports, "no-opaque-instance-fields": noOpaqueInstanceFields, + "no-unused-internal": noUnusedInternal, "jsdocs": jsdocs } } diff --git a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-unused-internal.ts b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-unused-internal.ts new file mode 100644 index 000000000..55de8c187 --- /dev/null +++ b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-unused-internal.ts @@ -0,0 +1,642 @@ +import * as fs from "node:fs" +import * as path from "node:path" +import type { CreateRule, ESTree, Visitor } from "oxlint" +import ts from "typescript" + +interface InternalExport { + readonly fileName: string + readonly declaration: ts.Node + readonly name: string + readonly range: readonly [number, number] + used: boolean +} + +interface Diagnostic { + readonly fileName: string + readonly range: readonly [number, number] + readonly message: string +} + +interface Analysis { + readonly diagnosticsByFile: ReadonlyMap> +} + +interface ImportBinding { + readonly fileName: string + readonly importedName: string +} + +interface FileInfo { + readonly fileName: string + readonly sourceFile: ts.SourceFile + readonly imports: Map + readonly namespaceImports: Map + readonly internalExports: Map> + readonly publicDeclarations: Array +} + +const cache = new Map() +const sourceExtensions = new Set([".ts"]) + +function getCwd(context: { readonly cwd?: string; getCwd?: () => string }): string { + return context.cwd ?? context.getCwd?.() ?? process.cwd() +} + +function normalizePathName(filename: string): string { + return path.resolve(filename).split(path.sep).join("/") +} + +function rangeNode(range: readonly [number, number]): ESTree.Node { + return { range: [range[0], range[1]] } as ESTree.Node +} + +function isSourceFile(fileName: string): boolean { + return sourceExtensions.has(path.extname(fileName)) && !fileName.endsWith(".d.ts") +} + +function findSourceFiles(cwd: string): Array { + const packagesDir = path.join(cwd, "packages") + if (!fs.existsSync(packagesDir)) return [] + + const files: Array = [] + const visit = (directory: string) => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + if (entry.name !== "dist" && entry.name !== "build" && entry.name !== "node_modules") { + visit(entryPath) + } + } else if (entry.isFile() && isSourceFile(entryPath) && normalizePathName(entryPath).includes("/src/")) { + files.push(entryPath) + } + } + } + + visit(packagesDir) + return files +} + +function findWorkspacePackages(cwd: string): Map { + const packagesDir = path.join(cwd, "packages") + const packages = new Map() + if (!fs.existsSync(packagesDir)) return packages + + const visit = (directory: string) => { + const packageJson = path.join(directory, "package.json") + if (fs.existsSync(packageJson)) { + try { + const json = JSON.parse(fs.readFileSync(packageJson, "utf8")) as { readonly name?: unknown } + if (typeof json.name === "string") { + packages.set(json.name, directory) + } + } catch { + // Ignore malformed package metadata; TypeScript will report that elsewhere. + } + return + } + + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "build") { + visit(path.join(directory, entry.name)) + } + } + } + + visit(packagesDir) + return packages +} + +function resolveSourceFile(basePath: string): string | undefined { + const candidates = path.extname(basePath) === ".ts" + ? [basePath] + : [basePath, `${basePath}.ts`, path.join(basePath, "index.ts")] + + for (const candidate of candidates) { + if (fs.existsSync(candidate) && isSourceFile(candidate)) { + return normalizePathName(candidate) + } + } + return undefined +} + +function resolveModule( + source: string, + containingFile: string, + workspacePackages: ReadonlyMap +): string | undefined { + if (source.startsWith("./") || source.startsWith("../")) { + return resolveSourceFile(path.resolve(path.dirname(containingFile), source)) + } + + let packageName: string | undefined + for (const name of workspacePackages.keys()) { + if (source === name || source.startsWith(`${name}/`)) { + if (packageName === undefined || name.length > packageName.length) { + packageName = name + } + } + } + if (packageName === undefined) return undefined + + const packageRoot = workspacePackages.get(packageName) + if (packageRoot === undefined) return undefined + + if (source === packageName) { + return resolveSourceFile(path.join(packageRoot, "src", "index.ts")) + } + + const subpath = source.slice(packageName.length + 1) + return resolveSourceFile(path.join(packageRoot, "src", subpath)) +} + +function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean { + return ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === kind) +} + +function hasExportModifier(node: ts.Node): boolean { + return hasModifier(node, ts.SyntaxKind.ExportKeyword) +} + +function hasPrivateOrProtectedModifier(node: ts.Node): boolean { + return hasModifier(node, ts.SyntaxKind.PrivateKeyword) || hasModifier(node, ts.SyntaxKind.ProtectedKeyword) +} + +function hasInternalJSDoc(node: ts.Node): boolean { + return ts.getJSDocTags(node).some((tag) => tag.tagName.getText() === "internal") +} + +function hasInternalApiJSDoc(node: ts.Node): boolean { + if (hasInternalJSDoc(node)) return true + if (ts.isVariableDeclaration(node)) { + return hasInternalJSDoc(node.parent.parent) + } + if (ts.isBindingElement(node) && ts.isVariableDeclaration(node.parent.parent)) { + return hasInternalJSDoc(node.parent.parent.parent) + } + return false +} + +function isInternalPath(fileName: string): boolean { + return /\/src\/(?:.*\/)?internal\//.test(normalizePathName(fileName)) +} + +function collectBindingNames(name: ts.BindingName, out: Array) { + if (ts.isIdentifier(name)) { + out.push(name) + return + } + for (const element of name.elements) { + if (ts.isBindingElement(element)) { + collectBindingNames(element.name, out) + } + } +} + +function getTopLevelDeclarationNameNodes(node: ts.Node): Array { + if ( + (ts.isFunctionDeclaration(node) || + ts.isClassDeclaration(node) || + ts.isInterfaceDeclaration(node) || + ts.isTypeAliasDeclaration(node) || + ts.isEnumDeclaration(node) || + ts.isModuleDeclaration(node)) && + node.name !== undefined && + ts.isIdentifier(node.name) + ) { + return [node.name] + } + + if (ts.isVariableStatement(node)) { + const names: Array = [] + for (const declaration of node.declarationList.declarations) { + collectBindingNames(declaration.name, names) + } + return names + } + + return [] +} + +function getDeclarationForName(node: ts.Node, name: ts.Identifier): ts.Node { + if (!ts.isVariableStatement(node)) return node + + let result: ts.Node = node + const visit = (current: ts.Node) => { + if (ts.isVariableDeclaration(current) || ts.isBindingElement(current)) { + const names: Array = [] + collectBindingNames(current.name, names) + if (names.includes(name)) { + result = current + return + } + } + ts.forEachChild(current, visit) + } + visit(node.declarationList) + return result +} + +function isImportOrExportBinding(identifier: ts.Identifier): boolean { + const parent = identifier.parent + return (ts.isImportSpecifier(parent) && (parent.name === identifier || parent.propertyName === identifier)) || + (ts.isImportClause(parent) && parent.name === identifier) || + (ts.isNamespaceImport(parent) && parent.name === identifier) || + (ts.isExportSpecifier(parent) && (parent.name === identifier || parent.propertyName === identifier)) +} + +function isDeclarationName(identifier: ts.Identifier): boolean { + const parent = identifier.parent + return (ts.isVariableDeclaration(parent) && parent.name === identifier) || + (ts.isBindingElement(parent) && parent.name === identifier) || + (ts.isParameter(parent) && parent.name === identifier) || + (ts.isFunctionDeclaration(parent) && parent.name === identifier) || + (ts.isClassDeclaration(parent) && parent.name === identifier) || + (ts.isInterfaceDeclaration(parent) && parent.name === identifier) || + (ts.isTypeAliasDeclaration(parent) && parent.name === identifier) || + (ts.isEnumDeclaration(parent) && parent.name === identifier) || + (ts.isEnumMember(parent) && parent.name === identifier) || + (ts.isModuleDeclaration(parent) && parent.name === identifier) +} + +function isNonReferenceName(identifier: ts.Identifier): boolean { + const parent = identifier.parent + return isImportOrExportBinding(identifier) || + isDeclarationName(identifier) || + (ts.isPropertyAccessExpression(parent) && parent.name === identifier) || + (ts.isPropertyAssignment(parent) && parent.name === identifier) || + (ts.isPropertyDeclaration(parent) && parent.name === identifier) || + (ts.isPropertySignature(parent) && parent.name === identifier) || + (ts.isMethodDeclaration(parent) && parent.name === identifier) || + (ts.isMethodSignature(parent) && parent.name === identifier) || + (ts.isGetAccessorDeclaration(parent) && parent.name === identifier) || + (ts.isSetAccessorDeclaration(parent) && parent.name === identifier) +} + +function isInside(node: ts.Node, container: ts.Node): boolean { + if (node.getSourceFile() !== container.getSourceFile()) return false + return node.getStart() >= container.getStart() && node.getEnd() <= container.getEnd() +} + +function getExports( + fileInfos: ReadonlyMap, + fileName: string | undefined, + exportName: string +): ReadonlyArray { + if (fileName === undefined) return [] + return fileInfos.get(fileName)?.internalExports.get(exportName) ?? [] +} + +function markUsed(exports: ReadonlyArray, node: ts.Node) { + for (const internal of exports) { + if (!isInside(node, internal.declaration)) { + internal.used = true + } + } +} + +function addFileDiagnostic( + diagnosticsByFile: Map>, + diagnostic: Diagnostic +) { + const normalized = normalizePathName(diagnostic.fileName) + const diagnostics = diagnosticsByFile.get(normalized) ?? [] + diagnostics.push(diagnostic) + diagnosticsByFile.set(normalized, diagnostics) +} + +function addInternalExport(fileInfo: FileInfo, internal: InternalExport) { + const exports = fileInfo.internalExports.get(internal.name) ?? [] + exports.push(internal) + fileInfo.internalExports.set(internal.name, exports) +} + +function collectFileInfo( + sourceFile: ts.SourceFile, + workspacePackages: ReadonlyMap +): FileInfo { + const fileInfo: FileInfo = { + fileName: normalizePathName(sourceFile.fileName), + sourceFile, + imports: new Map(), + namespaceImports: new Map(), + internalExports: new Map(), + publicDeclarations: [] + } + const publicDeclarationSet = new Set() + const isInternalFile = isInternalPath(sourceFile.fileName) + + for (const statement of sourceFile.statements) { + if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) { + const importedFile = resolveModule(statement.moduleSpecifier.text, sourceFile.fileName, workspacePackages) + const importClause = statement.importClause + if (importedFile === undefined || importClause === undefined) continue + + if (importClause.name !== undefined) { + fileInfo.imports.set(importClause.name.text, { fileName: importedFile, importedName: "default" }) + } + + const namedBindings = importClause.namedBindings + if (namedBindings === undefined) continue + if (ts.isNamespaceImport(namedBindings)) { + fileInfo.namespaceImports.set(namedBindings.name.text, importedFile) + } else { + for (const specifier of namedBindings.elements) { + const importedName = (specifier.propertyName ?? specifier.name).text + fileInfo.imports.set(specifier.name.text, { fileName: importedFile, importedName }) + } + } + continue + } + + if (!hasExportModifier(statement)) continue + + const names = getTopLevelDeclarationNameNodes(statement) + if (names.length === 0) continue + + const statementIsInternal = hasInternalApiJSDoc(statement) + for (const name of names) { + const declaration = getDeclarationForName(statement, name) + const declarationIsInternal = statementIsInternal || hasInternalApiJSDoc(declaration) + if (declarationIsInternal) { + addInternalExport(fileInfo, { + fileName: sourceFile.fileName, + declaration, + name: name.text, + range: [name.getStart(sourceFile), name.getEnd()], + used: false + }) + } else if (!isInternalFile && !publicDeclarationSet.has(statement)) { + fileInfo.publicDeclarations.push(statement) + publicDeclarationSet.add(statement) + } + } + } + + return fileInfo +} + +function markNamespaceReference( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + namespace: ts.Identifier, + name: ts.Identifier +) { + const importedFile = fileInfo.namespaceImports.get(namespace.text) + markUsed(getExports(fileInfos, importedFile, name.text), name) +} + +function scanUsage(fileInfos: ReadonlyMap, fileInfo: FileInfo) { + const visit = (node: ts.Node) => { + if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && ts.isIdentifier(node.name)) { + markNamespaceReference(fileInfos, fileInfo, node.expression, node.name) + } else if (ts.isQualifiedName(node) && ts.isIdentifier(node.left)) { + markNamespaceReference(fileInfos, fileInfo, node.left, node.right) + } else if (ts.isIdentifier(node) && !isNonReferenceName(node)) { + markUsed(fileInfo.internalExports.get(node.text) ?? [], node) + const imported = fileInfo.imports.get(node.text) + markUsed(getExports(fileInfos, imported?.fileName, imported?.importedName ?? ""), node) + } + ts.forEachChild(node, visit) + } + visit(fileInfo.sourceFile) +} + +function findInternalReferences( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + diagnostics: Array, + node: ts.Node +) { + const visit = (current: ts.Node) => { + if (ts.isPropertyAccessExpression(current) && ts.isIdentifier(current.expression)) { + const importedFile = fileInfo.namespaceImports.get(current.expression.text) + for (const internal of getExports(fileInfos, importedFile, current.name.text)) { + diagnostics.push({ + fileName: fileInfo.fileName, + range: [current.name.getStart(), current.name.getEnd()], + message: `Do not reference @internal export "${internal.name}" in a public exported type signature` + }) + } + } else if (ts.isQualifiedName(current) && ts.isIdentifier(current.left)) { + const importedFile = fileInfo.namespaceImports.get(current.left.text) + for (const internal of getExports(fileInfos, importedFile, current.right.text)) { + diagnostics.push({ + fileName: fileInfo.fileName, + range: [current.right.getStart(), current.right.getEnd()], + message: `Do not reference @internal export "${internal.name}" in a public exported type signature` + }) + } + } else if (ts.isIdentifier(current) && !isNonReferenceName(current)) { + const localExports = fileInfo.internalExports.get(current.text) ?? [] + const imported = fileInfo.imports.get(current.text) + const importedExports = getExports(fileInfos, imported?.fileName, imported?.importedName ?? "") + + for (const internal of [...localExports, ...importedExports]) { + diagnostics.push({ + fileName: fileInfo.fileName, + range: [current.getStart(), current.getEnd()], + message: `Do not reference @internal export "${internal.name}" in a public exported type signature` + }) + } + } + ts.forEachChild(current, visit) + } + visit(node) +} + +function scanTypeParameters( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + diagnostics: Array, + typeParameters: ts.NodeArray | undefined +) { + if (typeParameters === undefined) return + for (const typeParameter of typeParameters) { + if (typeParameter.constraint !== undefined) { + findInternalReferences(fileInfos, fileInfo, diagnostics, typeParameter.constraint) + } + if (typeParameter.default !== undefined) { + findInternalReferences(fileInfos, fileInfo, diagnostics, typeParameter.default) + } + } +} + +function scanParameterTypes( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + diagnostics: Array, + parameters: ts.NodeArray +) { + for (const parameter of parameters) { + if (parameter.type !== undefined) { + findInternalReferences(fileInfos, fileInfo, diagnostics, parameter.type) + } + } +} + +function scanFunctionLikeSignature( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + diagnostics: Array, + node: ts.SignatureDeclarationBase +) { + scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters) + scanParameterTypes(fileInfos, fileInfo, diagnostics, node.parameters) + if (node.type !== undefined) { + findInternalReferences(fileInfos, fileInfo, diagnostics, node.type) + } +} + +function scanClassMemberSignature( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + diagnostics: Array, + member: ts.ClassElement +) { + if (hasInternalJSDoc(member)) return + if (hasPrivateOrProtectedModifier(member)) return + if ( + ts.isMethodDeclaration(member) || + ts.isConstructorDeclaration(member) || + ts.isGetAccessorDeclaration(member) || + ts.isSetAccessorDeclaration(member) + ) { + scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, member) + } else if (ts.isPropertyDeclaration(member) && member.type !== undefined) { + findInternalReferences(fileInfos, fileInfo, diagnostics, member.type) + } +} + +function scanInterfaceMemberSignature( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + diagnostics: Array, + member: ts.TypeElement +) { + if (hasInternalJSDoc(member)) return + if ( + ts.isMethodSignature(member) || ts.isCallSignatureDeclaration(member) || ts.isConstructSignatureDeclaration(member) + ) { + scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, member) + } else if (ts.isPropertySignature(member) && member.type !== undefined) { + findInternalReferences(fileInfos, fileInfo, diagnostics, member.type) + } else if (ts.isIndexSignatureDeclaration(member)) { + scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, member) + } +} + +function scanPublicSignature( + fileInfos: ReadonlyMap, + fileInfo: FileInfo, + diagnostics: Array, + node: ts.Node +) { + if (ts.isFunctionDeclaration(node)) { + scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, node) + } else if (ts.isVariableStatement(node)) { + for (const declaration of node.declarationList.declarations) { + if (declaration.type !== undefined) { + findInternalReferences(fileInfos, fileInfo, diagnostics, declaration.type) + } + } + } else if (ts.isTypeAliasDeclaration(node)) { + scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters) + findInternalReferences(fileInfos, fileInfo, diagnostics, node.type) + } else if (ts.isInterfaceDeclaration(node)) { + scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters) + for (const heritage of node.heritageClauses ?? []) { + for (const type of heritage.types) { + findInternalReferences(fileInfos, fileInfo, diagnostics, type) + } + } + for (const member of node.members) { + scanInterfaceMemberSignature(fileInfos, fileInfo, diagnostics, member) + } + } else if (ts.isClassDeclaration(node)) { + scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters) + for (const heritage of node.heritageClauses ?? []) { + for (const type of heritage.types) { + findInternalReferences(fileInfos, fileInfo, diagnostics, type) + } + } + for (const member of node.members) { + scanClassMemberSignature(fileInfos, fileInfo, diagnostics, member) + } + } +} + +function analyze(cwd: string): Analysis { + const normalizedCwd = normalizePathName(cwd) + const cached = cache.get(normalizedCwd) + if (cached !== undefined) return cached + + const workspacePackages = findWorkspacePackages(cwd) + const fileInfos = new Map() + for (const fileName of findSourceFiles(cwd)) { + const sourceFile = ts.createSourceFile(fileName, fs.readFileSync(fileName, "utf8"), ts.ScriptTarget.Latest, true) + const fileInfo = collectFileInfo(sourceFile, workspacePackages) + fileInfos.set(fileInfo.fileName, fileInfo) + } + + for (const fileInfo of fileInfos.values()) { + scanUsage(fileInfos, fileInfo) + } + + const diagnosticsByFile = new Map>() + + for (const fileInfo of fileInfos.values()) { + const diagnostics: Array = [] + for (const declaration of fileInfo.publicDeclarations) { + scanPublicSignature(fileInfos, fileInfo, diagnostics, declaration) + } + for (const diagnostic of diagnostics) { + addFileDiagnostic(diagnosticsByFile, diagnostic) + } + + for (const exportsForName of fileInfo.internalExports.values()) { + for (const internal of exportsForName) { + if (!internal.used) { + addFileDiagnostic(diagnosticsByFile, { + fileName: internal.fileName, + range: internal.range, + message: `@internal export "${internal.name}" is not used by production sources` + }) + } + } + } + } + + for (const diagnostics of diagnosticsByFile.values()) { + diagnostics.sort((self, that) => self.range[0] - that.range[0]) + } + + const analysis = { diagnosticsByFile } + cache.set(normalizedCwd, analysis) + return analysis +} + +const rule: CreateRule = { + meta: { + type: "problem", + docs: { + description: "Disallow unused @internal exports and references to @internal exports from public type signatures" + } + }, + create(context) { + const analysis = analyze(getCwd(context)) + const diagnostics = analysis.diagnosticsByFile.get(normalizePathName(context.filename)) ?? [] + + return { + Program() { + for (const diagnostic of diagnostics) { + context.report({ + node: rangeNode(diagnostic.range), + message: diagnostic.message + }) + } + } + } as Visitor + } +} + +export default rule diff --git a/.context/effect/packages/tools/oxc/test/no-unused-internal.test.ts b/.context/effect/packages/tools/oxc/test/no-unused-internal.test.ts new file mode 100644 index 000000000..a14ba07d6 --- /dev/null +++ b/.context/effect/packages/tools/oxc/test/no-unused-internal.test.ts @@ -0,0 +1,129 @@ +import rule from "@effect/oxc/oxlint/rules/no-unused-internal" +import * as fs from "node:fs" +import * as os from "node:os" +import * as path from "node:path" +import { describe, expect, it } from "vitest" +import { createTestContext } from "./utils.ts" + +function writeFixture(files: Record): string { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "no-unused-internal-rule-")) + for (const [file, source] of Object.entries(files)) { + const fileName = path.join(cwd, file) + fs.mkdirSync(path.dirname(fileName), { recursive: true }) + fs.writeFileSync(fileName, source) + } + return cwd +} + +function run(cwd: string, file: string) { + const filename = path.join(cwd, file) + const source = fs.readFileSync(filename, "utf8") + const { context, errors } = createTestContext({ sourceCode: source, cwd, filename }) + const visitors = rule.create(context as never) + visitors.Program?.({ type: "Program", range: [0, source.length] } as never) + return errors.map((error) => error.message) +} + +describe("no-unused-internal", () => { + it("reports unused top-level @internal exports", () => { + const cwd = writeFixture({ + "packages/foo/src/Internal.ts": `/** @internal */ +export const unused = 1 +` + }) + + expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([ + `@internal export "unused" is not used by production sources` + ]) + }) + + it("counts same-file references as usage", () => { + const cwd = writeFixture({ + "packages/foo/src/Internal.ts": `/** @internal */ +export const used = 1 + +export const value = used +` + }) + + expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([]) + }) + + it("counts type-only references as usage", () => { + const cwd = writeFixture({ + "packages/foo/src/Internal.ts": `/** @internal */ +export interface Internal { + readonly value: string +} +`, + "packages/foo/src/Consumer.ts": `import type { Internal } from "./Internal.ts" + +type Local = Internal +` + }) + + expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([]) + }) + + it("reports @internal exports used in public exported type signatures", () => { + const cwd = writeFixture({ + "packages/foo/src/Internal.ts": `/** @internal */ +export interface Internal { + readonly value: string +} +`, + "packages/foo/src/Public.ts": `import type { Internal } from "./Internal.ts" + +export interface Public { + readonly value: Internal +} +` + }) + + expect(run(cwd, "packages/foo/src/Public.ts")).toEqual([ + `Do not reference @internal export "Internal" in a public exported type signature` + ]) + expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([]) + }) + + it("ignores @internal class member type signatures", () => { + const cwd = writeFixture({ + "packages/foo/src/Internal.ts": `/** @internal */ +export interface Internal { + readonly value: string +} +`, + "packages/foo/src/Public.ts": `import type { Internal } from "./Internal.ts" + +export class Public { + /** @internal */ + getInternal(): Internal { + throw new Error("not implemented") + } +} +` + }) + + expect(run(cwd, "packages/foo/src/Public.ts")).toEqual([]) + expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([]) + }) + + it("ignores public type signatures in internal directories", () => { + const cwd = writeFixture({ + "packages/foo/src/Internal.ts": `/** @internal */ +export interface Internal { + readonly value: string +} +`, + "packages/foo/src/internal/Consumer.ts": `import type { Internal } from "../Internal.ts" + +export interface PublicInsideInternalDirectory { + readonly value: Internal +} +` + }) + + expect(run(cwd, "packages/foo/src/internal/Consumer.ts")).toEqual([]) + expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([]) + }) +}) diff --git a/.context/effect/packages/vitest/CHANGELOG.md b/.context/effect/packages/vitest/CHANGELOG.md index f3ab0bf4e..5f13ff849 100644 --- a/.context/effect/packages/vitest/CHANGELOG.md +++ b/.context/effect/packages/vitest/CHANGELOG.md @@ -1,5 +1,61 @@ # @effect/vitest +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + ## 4.0.0-beta.85 ### Patch Changes diff --git a/.context/effect/packages/vitest/package.json b/.context/effect/packages/vitest/package.json index 692d80e47..bd636edfd 100644 --- a/.context/effect/packages/vitest/package.json +++ b/.context/effect/packages/vitest/package.json @@ -1,6 +1,6 @@ { "name": "@effect/vitest", - "version": "4.0.0-beta.85", + "version": "4.0.0-beta.93", "type": "module", "license": "MIT", "description": "A set of helpers for testing Effects with vitest", diff --git a/.context/effect/scripts/bundle-analyze.sh b/.context/effect/scripts/bundle-analyze.sh new file mode 100755 index 000000000..74ff292e5 --- /dev/null +++ b/.context/effect/scripts/bundle-analyze.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate bundle composition artifacts for explicitly selected entry files. +# +# Usage: +# scripts/bundle-analyze.sh [--output-dir ] + +usage() { + echo "Usage: scripts/bundle-analyze.sh [--output-dir ] " +} + +OUTPUT_DIR="tmp/bundle-analysis" +FILES=() + +while [ "$#" -gt 0 ]; do + case "$1" in + -o | --output-dir) + if [ "$#" -lt 2 ]; then + usage + exit 1 + fi + OUTPUT_DIR="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + --) + shift + FILES+=("$@") + break + ;; + -*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + FILES+=("$1") + shift + ;; + esac +done + +if [ "${#FILES[@]}" -eq 0 ]; then + usage + exit 1 +fi + +ROOT="$(git rev-parse --show-toplevel)" + +cd "$ROOT" + +echo "Building current checkout..." +pnpm build:tsgo + +node packages/tools/bundle/src/bin.ts visualize-selected \ + --output-dir "$OUTPUT_DIR" \ + "${FILES[@]}" diff --git a/.context/effect/scripts/bundle-compare-selected.sh b/.context/effect/scripts/bundle-compare-selected.sh new file mode 100755 index 000000000..0df1d9ffb --- /dev/null +++ b/.context/effect/scripts/bundle-compare-selected.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Compare bundle size for explicitly selected entry files against a base ref. +# +# Usage: +# scripts/bundle-compare-selected.sh [--base ] [--keep-base] +# +# The selected files are copied into the base worktree before measurement so the +# report isolates source changes instead of fixture content changes. + +usage() { + echo "Usage: scripts/bundle-compare-selected.sh [--base ] [--keep-base] " +} + +BASE_REF="main" +KEEP_BASE_WORKTREE=false +FILES=() + +while [ "$#" -gt 0 ]; do + case "$1" in + -b | --base) + if [ "$#" -lt 2 ]; then + usage + exit 1 + fi + BASE_REF="$2" + shift 2 + ;; + --keep-base) + KEEP_BASE_WORKTREE=true + shift + ;; + -h | --help) + usage + exit 0 + ;; + --) + shift + FILES+=("$@") + break + ;; + -*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + FILES+=("$1") + shift + ;; + esac +done + +if [ "${#FILES[@]}" -eq 0 ]; then + usage + exit 1 +fi + +ROOT="$(git rev-parse --show-toplevel)" +BASE_DIR="$ROOT/tmp/bundle-base" +BASE_STAMP="$BASE_DIR/.bundle-build-stamp" + +cleanup() { + if [ "$KEEP_BASE_WORKTREE" = "false" ] && [ -d "$BASE_DIR" ]; then + git worktree remove --force "$BASE_DIR" + fi +} + +trap cleanup EXIT + +cd "$ROOT" + +echo "Building current checkout..." +pnpm build:tsgo + +BASE_COMMIT="$(git rev-parse "$BASE_REF")" + +if [ -d "$BASE_DIR/packages/effect/dist" ] && + [ -f "$BASE_STAMP" ] && + [ "$(git -C "$BASE_DIR" rev-parse HEAD)" = "$BASE_COMMIT" ] && + [ "$(cat "$BASE_STAMP")" = "$BASE_COMMIT" ]; then + echo "Reusing built base worktree at $BASE_DIR ($BASE_REF @ ${BASE_COMMIT:0:8})" +else + if [ -d "$BASE_DIR" ]; then + git worktree remove --force "$BASE_DIR" + fi + echo "Creating base worktree at $BASE_DIR ($BASE_REF @ ${BASE_COMMIT:0:8})" + git worktree add --detach "$BASE_DIR" "$BASE_COMMIT" + echo "Building base checkout..." + (cd "$BASE_DIR" && pnpm install && pnpm build:tsgo) + printf "%s\n" "$BASE_COMMIT" > "$BASE_STAMP" +fi + +node packages/tools/bundle/src/bin.ts compare-selected \ + --base-dir "$BASE_DIR" \ + "${FILES[@]}" diff --git a/.context/effect/scripts/check-cookbook-schedule.ts b/.context/effect/scripts/check-cookbook-schedule.ts deleted file mode 100644 index b577534f3..000000000 --- a/.context/effect/scripts/check-cookbook-schedule.ts +++ /dev/null @@ -1,390 +0,0 @@ -import * as ChildProcess from "node:child_process" -import * as Fs from "node:fs" -import * as Os from "node:os" -import * as Path from "node:path" -import { performance } from "node:perf_hooks" - -interface FenceInfo { - readonly language: string - readonly noCheck: boolean - readonly runnable: boolean - readonly deterministic: boolean -} - -interface Example { - readonly file: string - readonly startLine: number - readonly code: string - readonly noCheck: boolean - readonly runnable: boolean - readonly deterministic: boolean -} - -interface GeneratedExample extends Example { - readonly generatedFile: string -} - -interface OpenFence { - readonly startLine: number - readonly info: FenceInfo - readonly lines: Array -} - -interface RunResult { - readonly status: number - readonly message: string -} - -const files = process.argv.slice(2) - -if (files.length === 0) { - console.error("Usage: node scripts/check-cookbook-schedule.ts ") - process.exit(1) -} - -const outputDirectory = Path.join("scratchpad", ".markdown-examples") - -function parseFenceInfo(info: string): FenceInfo { - const parts = info.trim().split(/\s+/).filter(Boolean) - const language = parts[0] ?? "" - const metadata = new Set(parts.slice(1)) - return { - language, - noCheck: metadata.has("no-check"), - runnable: metadata.has("runnable"), - deterministic: metadata.has("deterministic") - } -} - -function isTypeScript(language: string): boolean { - return language === "ts" || language === "typescript" -} - -function hasNoCheckComment(lines: ReadonlyArray, index: number): boolean { - let previous = index - 1 - while (previous >= 0 && (lines[previous] ?? "").trim() === "") { - previous-- - } - return /^$/.test(lines[previous] ?? "") -} - -function extractExamples(file: string): Array { - const lines = Fs.readFileSync(file, "utf8").split(/\r?\n/) - const examples: Array = [] - let open: OpenFence | undefined = undefined - - for (let index = 0; index < lines.length; index++) { - const line = lines[index] ?? "" - - if (!line.startsWith("```")) { - if (open !== undefined) { - open.lines.push(line) - } - continue - } - - if (open === undefined) { - const info = parseFenceInfo(line.slice(3)) - if (isTypeScript(info.language) && info.noCheck && !hasNoCheckComment(lines, index)) { - throw new Error( - `${file}:${index + 1} has a no-check TypeScript fence without a preceding comment` - ) - } - open = { - startLine: index + 1, - info, - lines: [] - } - continue - } - - if (isTypeScript(open.info.language)) { - examples.push({ - file, - startLine: open.startLine, - code: open.lines.join("\n"), - noCheck: open.info.noCheck, - runnable: open.info.runnable, - deterministic: open.info.deterministic - }) - } - open = undefined - } - - if (open !== undefined) { - throw new Error(`${file}:${open.startLine} has an unclosed code fence`) - } - - return examples -} - -function sanitizeFileName(file: string): string { - return file.replaceAll(Path.sep, "_").replaceAll(/[^a-zA-Z0-9_.-]/g, "_") -} - -function run(command: string, args: Array): number { - const result = ChildProcess.spawnSync(command, args, { - stdio: "inherit", - shell: process.platform === "win32" - }) - - if (result.error !== undefined) { - throw result.error - } - - return result.status ?? 1 -} - -function stripAnsi(text: string): string { - const escape = String.fromCharCode(27) - return text.replace(new RegExp(`${escape}\\[[0-9;]*m`, "g"), "") -} - -function normalizeOutput(text: string): string { - return stripAnsi(text).trimEnd() -} - -function variableOutputLine(example: Example): number { - return example.code.split("\n").findIndex((line) => line.trim().startsWith("// Output may vary:")) -} - -function hasVariableOutputMarker(example: Example): boolean { - return variableOutputLine(example) !== -1 -} - -function variableOutputSample(example: Example): Array { - const lines = example.code.split("\n") - const outputLine = variableOutputLine(example) - - if (outputLine === -1) { - return [] - } - - const output: Array = [] - for (const line of lines.slice(outputLine + 1)) { - const match = /^\/\/ ?(.*)$/.exec(line) - if (match === null) { - if (line.trim() === "") { - continue - } - break - } - output.push(match[1] ?? "") - } - - return output -} - -function expectedOutput(example: Example): string { - const lines = example.code.split("\n") - const outputLine = lines.findIndex((line) => line.trim() === "// Output:") - - if (outputLine === -1) { - throw new Error(`${example.file}:${example.startLine} is deterministic but has no // Output: comment`) - } - - const output: Array = [] - for (const line of lines.slice(outputLine + 1)) { - const match = /^\/\/ ?(.*)$/.exec(line) - if (match === null) { - if (line.trim() === "") { - continue - } - break - } - output.push(match[1] ?? "") - } - - return output.join("\n") -} - -function formatDuration(milliseconds: number): string { - if (milliseconds < 1000) { - return `${Math.round(milliseconds)}ms` - } - return `${(milliseconds / 1000).toFixed(1)}s` -} - -function runExample(example: GeneratedExample): Promise { - return new Promise((resolve, reject) => { - const child = ChildProcess.spawn("node", [example.generatedFile], { - shell: process.platform === "win32" - }) - const stdout: Array = [] - const stderr: Array = [] - - if (child.stdout === null || child.stderr === null) { - reject(new Error("Expected child process stdio streams")) - return - } - - child.stdout.on("data", (chunk: Buffer) => { - stdout.push(chunk) - }) - child.stderr.on("data", (chunk: Buffer) => { - stderr.push(chunk) - }) - child.on("error", reject) - child.on("close", (status) => { - const actualStdout = Buffer.concat(stdout).toString("utf8") - const actualStderr = Buffer.concat(stderr).toString("utf8") - const normalizedStdout = normalizeOutput(actualStdout) - - if ((status ?? 1) !== 0) { - resolve({ - status: status ?? 1, - message: [ - `${example.file}:${example.startLine} failed while running`, - actualStdout, - actualStderr - ].filter((part) => part.length > 0).join("\n") - }) - return - } - - if (example.deterministic) { - const expected = expectedOutput(example) - if (normalizedStdout !== normalizeOutput(expected)) { - resolve({ - status: 1, - message: [ - `${example.file}:${example.startLine} output did not match // Output:`, - "Expected:", - expected, - "Actual:", - normalizedStdout - ].join("\n") - }) - return - } - } else if (hasVariableOutputMarker(example) && normalizedStdout.length === 0) { - resolve({ - status: 1, - message: `${example.file}:${example.startLine} is marked // Output may vary: but produced no stdout` - }) - return - } - - resolve({ status: 0, message: "" }) - }) - }) -} - -async function runExamples(examples: ReadonlyArray): Promise> { - const concurrency = Math.max(1, Math.min(Os.availableParallelism(), examples.length)) - const startedAt = performance.now() - let next = 0 - let completed = 0 - const results: Array = [] - - if (examples.length > 0) { - console.error(`Running ${examples.length} runnable example(s) with concurrency ${concurrency}`) - } - - function elapsed(): string { - return formatDuration(performance.now() - startedAt) - } - - function reportProgress(): void { - if (completed === examples.length || completed % 10 === 0) { - console.error(`Ran ${completed}/${examples.length} runnable example(s) in ${elapsed()}`) - } - } - - async function worker(workerId: number): Promise { - while (next < examples.length) { - const index = next++ - const example = examples[index]! - const exampleStartedAt = performance.now() - console.error(`Worker ${workerId}: ${example.file}:${example.startLine} started at ${elapsed()}`) - results[index] = await runExample(example) - completed++ - console.error( - `Worker ${workerId}: ${example.file}:${example.startLine} finished in ${ - formatDuration(performance.now() - exampleStartedAt) - }` - ) - reportProgress() - } - } - - await Promise.all(Array.from({ length: concurrency }, (_, index) => worker(index + 1))) - return results -} - -Fs.rmSync(outputDirectory, { recursive: true, force: true }) -Fs.mkdirSync(outputDirectory, { recursive: true }) - -try { - const examples = files.flatMap(extractExamples) - const checkedExamples = examples.filter((example) => !example.noCheck) - - const generatedFiles: Array = checkedExamples.map((example, index) => { - const generatedFile = Path.join( - outputDirectory, - `${String(index + 1).padStart(3, "0")}-${sanitizeFileName(example.file)}-L${example.startLine}.ts` - ) - Fs.writeFileSync( - generatedFile, - `// Generated from ${example.file}:${example.startLine}\n${example.code}\n` - ) - return { ...example, generatedFile } - }) - - if (generatedFiles.length > 0) { - const status = run("pnpm", [ - "exec", - "tsc", - "--noEmit", - "--skipLibCheck", - "--moduleResolution", - "bundler", - "--module", - "esnext", - "--target", - "es2022", - "--lib", - "esnext,dom", - "--types", - "node", - "--strict", - "--ignoreConfig", - "--allowImportingTsExtensions", - ...generatedFiles.map((example) => example.generatedFile) - ]) - - if (status !== 0) { - process.exit(status) - } - } - - const runnableExamples = generatedFiles.filter((example) => example.runnable) - const deterministicExamples = runnableExamples.filter((example) => example.deterministic) - for (const example of runnableExamples) { - if (!example.deterministic && !hasVariableOutputMarker(example)) { - console.error( - `${example.file}:${example.startLine} is runnable but has neither deterministic metadata nor // Output may vary:` - ) - process.exit(1) - } - if (!example.deterministic && variableOutputSample(example).length === 0) { - console.error(`${example.file}:${example.startLine} has // Output may vary: but no sample output lines`) - process.exit(1) - } - } - - const results = await runExamples(runnableExamples) - const failed = results.filter((result) => result.status !== 0) - for (const result of failed) { - console.error(result.message) - } - if (failed.length > 0) { - process.exit(1) - } - - console.log( - `Checked ${checkedExamples.length} TypeScript example(s) from ${files.length} Markdown file(s); ran ${runnableExamples.length}; compared ${deterministicExamples.length}` - ) -} finally { - Fs.rmSync(outputDirectory, { recursive: true, force: true }) -} diff --git a/.context/effect/tsconfig.json b/.context/effect/tsconfig.json index 612d0c70b..b2a9c688e 100644 --- a/.context/effect/tsconfig.json +++ b/.context/effect/tsconfig.json @@ -89,6 +89,7 @@ "@effect/ai-codegen/*": ["./packages/tools/ai-codegen/src/*.ts"], "@effect/ai-docgen": ["./packages/tools/ai-docgen/src/index.ts"], "@effect/ai-docgen/*": ["./packages/tools/ai-docgen/src/*.ts"], + "@effect/bundle/*": ["./packages/tools/bundle/src/*.ts"], "@effect/openapi-generator": ["./packages/tools/openapi-generator/src/index.ts"], "@effect/openapi-generator/*": ["./packages/tools/openapi-generator/src/*.ts"], "@effect/oxc": ["./packages/tools/oxc/src/index.ts"], diff --git a/.context/effect/tsconfig.packages.json b/.context/effect/tsconfig.packages.json index 1ac8199fb..229e31251 100644 --- a/.context/effect/tsconfig.packages.json +++ b/.context/effect/tsconfig.packages.json @@ -30,6 +30,7 @@ { "path": "packages/sql/sqlite-react-native" }, { "path": "packages/sql/sqlite-wasm" }, { "path": "packages/tools/ai-codegen" }, + { "path": "packages/tools/bundle" }, { "path": "packages/tools/openapi-generator" }, { "path": "packages/tools/jsdocs" }, { "path": "packages/tools/oxc" }, diff --git a/apps/api/src/mcp/tools/registry.ts b/apps/api/src/mcp/tools/registry.ts index e8c6133ac..260491590 100644 --- a/apps/api/src/mcp/tools/registry.ts +++ b/apps/api/src/mcp/tools/registry.ts @@ -62,7 +62,7 @@ import { registerUpdateDashboardWidgetTool } from "./update-dashboard-widget" export interface MapleToolDefinition { readonly name: string readonly description: string - readonly schema: Schema.Decoder + readonly schema: Schema.Codec readonly handler: (params: unknown) => Effect.Effect } diff --git a/apps/api/src/mcp/tools/types.ts b/apps/api/src/mcp/tools/types.ts index 323fbe912..9ba8b3325 100644 --- a/apps/api/src/mcp/tools/types.ts +++ b/apps/api/src/mcp/tools/types.ts @@ -38,7 +38,7 @@ export interface McpToolResult { } export interface McpToolRegistrar { - tool>( + tool>( name: string, description: string, schema: TSchema, diff --git a/apps/scraper/package.json b/apps/scraper/package.json index 2249c3a26..e33fe496f 100644 --- a/apps/scraper/package.json +++ b/apps/scraper/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.85", + "@effect/vitest": "4.0.0-beta.93", "@types/bun": "^1.3.11", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", diff --git a/bun.lock b/bun.lock index 5254080ba..19fa49cb9 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "maple", "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", - "@effect/vitest": "4.0.0-beta.85", + "@effect/vitest": "4.0.0-beta.93", "@maple/infra": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "@types/node": "catalog:tooling", @@ -237,7 +237,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.85", + "@effect/vitest": "4.0.0-beta.93", "@types/bun": "^1.3.11", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", @@ -560,7 +560,7 @@ }, }, "patchedDependencies": { - "effect@4.0.0-beta.85": "patches/effect@4.0.0-beta.85.patch", + "effect@4.0.0-beta.93": "patches/effect@4.0.0-beta.93.patch", }, "catalog": { "vite": "^8.0.16", @@ -571,10 +571,10 @@ "@clickhouse/client-web": "^1.21.0", }, "effect": { - "@effect/atom-react": "4.0.0-beta.85", + "@effect/atom-react": "4.0.0-beta.93", "@effect/language-service": "^0.86.2", - "@effect/platform-bun": "4.0.0-beta.85", - "effect": "4.0.0-beta.85", + "@effect/platform-bun": "4.0.0-beta.93", + "effect": "4.0.0-beta.93", }, "react": { "@types/react": "^19.2.14", @@ -986,15 +986,15 @@ "@effect-router/core": ["@effect-router/core@workspace:packages/effect-router"], - "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.85", "", { "peerDependencies": { "effect": "^4.0.0-beta.85", "react": "^19.2.4", "scheduler": "*" } }, "sha512-DsBau/iXF8GN6lpbObhwTsP0HcqzoaSJWzbewIUHp/SY2YeQV2bvo0IfPa0dhfwACZHZmhgzdPUaD7SpFyukmg=="], + "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.93", "", { "peerDependencies": { "effect": "^4.0.0-beta.93", "react": "^19.2.4", "scheduler": "*" } }, "sha512-43FOYozhZT+VIQBH5LVAbabhdjGCuxGKdVTeUnkMp5NY+/g3nDLcAyAkNYn99G3+XDbqFvJaqmNJP7NLF5+WFg=="], "@effect/language-service": ["@effect/language-service@0.86.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-SaPln+8srOqDJDUwNTDmP5e+IYpEDr9+1epGznnsLqu8xvo6VnxyWARdeLpqvZJlb0Pgy9ca7ppqvvdWbHPXAg=="], - "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.85", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.85" }, "peerDependencies": { "effect": "^4.0.0-beta.85" } }, "sha512-hSOCmUiIFQ6uWGlmfKF0ZS3dK2VQLLhrlTJU3I9rqQTfwDVGUUeoVR8I2wvmBZercYLClt/OsLTR6KQ1RbCZkA=="], + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.93", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.93" }, "peerDependencies": { "effect": "^4.0.0-beta.93" } }, "sha512-pny29d1NhJ7XLv19as7A0pIZr+QFi5KejnwB/lLW1c8P63t+PDMPEn21toy2vx2yzl9xRTo8heyiXPXRsqPN5w=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.85", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.85" } }, "sha512-JXM5f2j2L2oGxuQG12sysPJeFYKtB388BruyHb/IfxJ/5Wx8KDo3eKLRe6tmg7y7BNeZTxEb14tPKC853+mqeg=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.93", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.93" } }, "sha512-XUqZ2u5GglBqY8q2jj4Q7GjN5K/enedk8auZM9rY/l5a/myaQTrQp3QnvpIK4/Yg0WFjLGuctGPMKWRk3OLIrA=="], - "@effect/vitest": ["@effect/vitest@4.0.0-beta.85", "", { "peerDependencies": { "effect": "^4.0.0-beta.85", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-iQC5grIDhZFvvug1HdoMVk9S+UxurXMlhnQm31NBpRcEh/ulvK82wSbZ4X5BrsMJTqxJgrKeC27p64HMcpgM5g=="], + "@effect/vitest": ["@effect/vitest@4.0.0-beta.93", "", { "peerDependencies": { "effect": "^4.0.0-beta.93", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-gMAnZ9PiMeJMDED9s0jWgCOhc2JccrTCxowhur/KriImsHnHIRj4VG/vK0xLw0Axe4AkTWzXNdRsFrYOjBTl3A=="], "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], @@ -3028,7 +3028,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.85", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Cjv9YQyv4CiIccmRAQIWAoeESCpCpiuHYY8zb5vqiYs3Ac2yE5RQAnKq7z4Ir/3VJYZb8kxh6rS8czDDnpTdkQ=="], + "effect": ["effect@4.0.0-beta.93", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-wNS5MKFa3C42uBfIDik2oJ78lhpoYz2hN4oBR0229BeeDCIrkg/FiOvoiPGdCVlWa7MEKxEL5I0f8AILVHSD9A=="], "effect-cf": ["effect-cf@workspace:lib/effect-cf"], @@ -5474,6 +5474,10 @@ "@solana/web3.js/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "@standard-community/standard-json/effect": ["effect@4.0.0-beta.85", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Cjv9YQyv4CiIccmRAQIWAoeESCpCpiuHYY8zb5vqiYs3Ac2yE5RQAnKq7z4Ir/3VJYZb8kxh6rS8czDDnpTdkQ=="], + + "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.85", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Cjv9YQyv4CiIccmRAQIWAoeESCpCpiuHYY8zb5vqiYs3Ac2yE5RQAnKq7z4Ir/3VJYZb8kxh6rS8czDDnpTdkQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], diff --git a/lib/effect-cloudflare/src/fetch.ts b/lib/effect-cloudflare/src/fetch.ts index 18bff3671..9fdd196cc 100644 --- a/lib/effect-cloudflare/src/fetch.ts +++ b/lib/effect-cloudflare/src/fetch.ts @@ -19,7 +19,7 @@ import * as Stream from "effect/Stream" import * as HttpClientError from "effect/unstable/http/HttpClientError" import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" -import * as UrlParams from "effect/unstable/http/UrlParams" +import * as Url from "effect/unstable/http/Url" import { WorkerEnvironment } from "./worker-environment.ts" export interface ServiceBindingToken { @@ -56,7 +56,7 @@ const doFetch = ( fetcher: runtime.Fetcher, request: HttpClientRequest.HttpClientRequest, ): Effect.Effect => { - const urlResult = UrlParams.makeUrl( + const urlResult = Url.make( request.url, request.urlParams, request.hash.pipe(Option.getOrUndefined), diff --git a/package.json b/package.json index de024fb9f..1dc8550e2 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dependencies": {}, "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", - "@effect/vitest": "4.0.0-beta.85", + "@effect/vitest": "4.0.0-beta.93", "@maple/infra": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "@types/node": "catalog:tooling", @@ -60,10 +60,10 @@ "@clickhouse/client-web": "^1.21.0" }, "effect": { - "@effect/atom-react": "4.0.0-beta.85", + "@effect/atom-react": "4.0.0-beta.93", "@effect/language-service": "^0.86.2", - "@effect/platform-bun": "4.0.0-beta.85", - "effect": "4.0.0-beta.85" + "@effect/platform-bun": "4.0.0-beta.93", + "effect": "4.0.0-beta.93" }, "react": { "@types/react": "^19.2.14", @@ -93,6 +93,6 @@ } }, "patchedDependencies": { - "effect@4.0.0-beta.85": "patches/effect@4.0.0-beta.85.patch" + "effect@4.0.0-beta.93": "patches/effect@4.0.0-beta.93.patch" } } diff --git a/packages/domain/src/optional-key-schema.test.ts b/packages/domain/src/optional-key-schema.test.ts index 0014a3c0a..af365c86d 100644 --- a/packages/domain/src/optional-key-schema.test.ts +++ b/packages/domain/src/optional-key-schema.test.ts @@ -15,7 +15,7 @@ type DecodeResult = | { readonly ok: true; readonly value: A } | { readonly ok: false; readonly error: string } -const decode = (schema: Schema.Decoder, value: unknown): DecodeResult => { +const decode = (schema: Schema.ConstraintDecoder, value: unknown): DecodeResult => { try { return { ok: true, value: Schema.decodeUnknownSync(schema)(value) } } catch (error) { diff --git a/patches/effect@4.0.0-beta.85.patch b/patches/effect@4.0.0-beta.93.patch similarity index 72% rename from patches/effect@4.0.0-beta.85.patch rename to patches/effect@4.0.0-beta.93.patch index 89c62c73b..3870a30f4 100644 --- a/patches/effect@4.0.0-beta.85.patch +++ b/patches/effect@4.0.0-beta.93.patch @@ -1,25 +1,5 @@ -diff --git a/dist/internal/effect.js b/dist/internal/effect.js -index eb1cd7d128916a04abbcdfb3b613fc53dbe88620..7e9253352c50a9b620918b056f1b984c830996bf 100644 ---- a/dist/internal/effect.js -+++ b/dist/internal/effect.js -@@ -2537,7 +2537,14 @@ const processOrPerformanceNow = /*#__PURE__*/function () { - if (!processHrtime) { - return performanceNowNanos; - } -- const origin = /*#__PURE__*/performanceNowNanos() - /*#__PURE__*/processHrtime.bigint(); -+ // maple patch (workerd/miniflare clock fix): anchor the hrtime offset on Date.now() -+ // instead of performanceNowNanos(). performanceNowNanos() is 0 at module-load scope in -+ // `wrangler dev`/miniflare while process.hrtime is already live wall-clock, which poisons -+ // this cached origin and makes Clock.currentTimeNanos (and span start times) ~1970. -+ // Date.now() is consistent with hrtime's scale at load in every runtime (both 0 in -+ // production workerd, both live in miniflare, wall-clock+monotonic in node) so origin is -+ // correct everywhere. Deployed workerd is unaffected. See workers-sdk / effect-smol issues. -+ const origin = BigInt(Date.now()) * BigInt(1_000_000) - processHrtime.bigint(); - return () => origin + processHrtime.bigint(); - }(); - /** @internal */ diff --git a/dist/unstable/ai/McpServer.d.ts b/dist/unstable/ai/McpServer.d.ts -index a990324bedf77c24d6b195fac3abb62089ca5582..f453c6faa1b06bf67c15a94ee4c89653a63a623b 100644 +index 87a6f7acdd3d7f89b41fac68e575d80a09748f64..b523535a8123bb7e6bb0f8022ca597cb91e36f5f 100644 --- a/dist/unstable/ai/McpServer.d.ts +++ b/dist/unstable/ai/McpServer.d.ts @@ -10,7 +10,7 @@ import type * as RpcGroup from "../rpc/RpcGroup.ts";