From 1a15543f69888fa13b8bee3c4c0c338936de0328 Mon Sep 17 00:00:00 2001 From: Aurelien JF Vabre Date: Thu, 24 Sep 2026 05:45:53 +0200 Subject: [PATCH 1/2] Migrate all examples to Effect v4 syntax Mechanical migration of every TypeScript example (content/**/*.mdx) from Effect v3 to Effect v4, following the official v3-to-v4 migration guides (Effect-TS/effect MIGRATION.md + migration/*). Highlights: - Services: Context.GenericTag/Tag, Effect.Tag/Service -> Context.Service with make + explicit static layer; .Default -> .layer - Errors: catchAll -> catch, catchAllCause -> catchCause, catchAllDefect -> catchDefect, catchSome -> catchFilter - Either -> Result (succeed/fail, isSuccess/isFailure, getSuccess/ getFailure, mapError, fromNullishOr, match onFailure/onSuccess, _tag Success/Failure); Effect.either -> Effect.result - Concurrency: Effect.fork -> forkChild, forkDaemon -> forkDetach - Constructors: Effect.async -> Effect.callback; Scope.extend -> Scope.provide - Schema: decode/encode -> decodeEffect/encodeEffect (+Unknown variants), Either decoders -> Exit/Result variants, Literal/Union/Tuple -> array form, Record({k,v}) shape, Transform -> decodeTo + SchemaTransformation.transform, Struct spread/Struct.pick/omit/map instead of extend/pick/omit/partial, filters -> check(is*), filter() -> check(makeFilter()), Date -> DateFromString (Date kept for Date-typed targets), TaggedError/Data APIs kept where valid - Time: DurationInput -> Input, decode -> fromInputUnsafe, lessThanOrEqualTo/greaterThan -> is* predicates - Schedule: intersect/union/compose -> max/min, tap/while/delayed/ until/recurWhile/filter/upTo/jittered moved to v4 signatures, Command -> ChildProcess + ChildProcessSpawner, Stream.fromChunk -> fromArray, Http.* namespace -> HttpRouter/HttpServerRequest/ HttpServerResponse from effect/http, platform HttpClient.* -> effect/http, FileSystem/Terminal stay in effect barrel, KeyValueStore -> effect/persistence - Deps: effect + @effect/* bumped to 4.0.0-rc.117; folded packages (@effect/platform, @effect/schema, @effect/rpc, @effect/cli) removed (now part of effect core) Notes / follow-ups: - import paths use final v4 layout (effect/http, effect/process, effect/sql, ...) per MIGRATION.md; the npm rc.117 build still exposes them under effect/unstable/*, main branch already uses the final paths - packages/*/src application code is NOT migrated yet (blocked on @effect/cli v4); run bun install to refresh the lockfile - Data.TaggedError kept intentionally (still valid v4) --- content/learning-roadmap/ROADMAP-module1.mdx | 4 +- content/learning-roadmap/ROADMAP-module10.mdx | 12 +- .../ROADMAP-module11-schema-patterns.mdx | 10 +- .../ROADMAP-module13-scheduling-patterns.mdx | 14 +- content/learning-roadmap/ROADMAP-module2.mdx | 2 +- content/learning-roadmap/ROADMAP-module5.mdx | 10 +- content/learning-roadmap/ROADMAP-module6.mdx | 10 +- content/learning-roadmap/ROADMAP-module7.mdx | 12 +- content/learning-roadmap/ROADMAP-overview.mdx | 20 +- content/new/pattern-option-either-match.mdx | 48 ++--- content/new/raw/ROADMAP-module1.mdx | 4 +- content/new/raw/ROADMAP-module10.mdx | 12 +- .../raw/ROADMAP-module11-schema-patterns.mdx | 10 +- .../ROADMAP-module13-scheduling-patterns.mdx | 14 +- content/new/raw/ROADMAP-module2.mdx | 2 +- content/new/raw/ROADMAP-module5.mdx | 10 +- content/new/raw/ROADMAP-module6.mdx | 10 +- content/new/raw/ROADMAP-module7.mdx | 12 +- content/new/raw/ROADMAP-overview.mdx | 20 +- .../new/raw/pattern-option-either-match.mdx | 48 ++--- .../building-apis/api-authentication.mdx | 11 +- .../patterns/building-apis/api-cors.mdx | 2 +- .../patterns/building-apis/api-middleware.mdx | 8 +- .../patterns/building-apis/api-openapi.mdx | 16 +- .../building-apis/api-rate-limiting.mdx | 9 +- .../building-apis/extract-path-parameters.mdx | 38 ++-- .../building-apis/handle-api-errors.mdx | 72 ++++--- .../building-apis/handle-get-request.mdx | 42 ++-- .../building-apis/launch-http-server.mdx | 31 +-- .../make-http-client-request.mdx | 62 +++--- .../provide-dependencies-to-routes.mdx | 58 ++--- .../building-apis/send-json-response.mdx | 52 +++-- .../building-apis/validate-request-body.mdx | 65 +++--- .../pipeline-backpressure.mdx | 2 +- .../pipeline-dead-letter-queue.mdx | 4 +- .../stream-from-file.mdx | 6 +- .../stream-manage-resources.mdx | 18 +- .../stream-process-concurrently.mdx | 2 +- .../stream-retry-on-failure.mdx | 2 +- .../add-caching-by-wrapping-a-layer.mdx | 15 +- ...rency-pattern-coordinate-with-deferred.mdx | 30 +-- ...currency-pattern-coordinate-with-latch.mdx | 16 +- ...urrency-pattern-pubsub-event-broadcast.mdx | 18 +- ...rrency-pattern-queue-work-distribution.mdx | 18 +- .../concurrency-pattern-race-timeout.mdx | 36 ++-- ...ency-pattern-rate-limit-with-semaphore.mdx | 8 +- .../decouple-fibers-with-queue-pubsub.mdx | 8 +- .../concurrency-fork-basics.mdx | 22 +- .../concurrency-understanding-fibers.mdx | 6 +- .../implement-graceful-shutdown.mdx | 14 +- .../manage-resource-lifecycles-with-scope.mdx | 4 +- .../poll-for-status-until-task-completes.mdx | 2 +- .../concurrency/race-concurrent-effects.mdx | 4 +- .../run-background-tasks-with-fork.mdx | 18 +- ...derstand-fibers-as-lightweight-threads.mdx | 6 +- .../access-config-in-context.mdx | 14 +- .../core-concepts/beyond-the-date-type.mdx | 2 +- .../core-concepts/combinator-conditional.mdx | 8 +- .../combinator-error-handling.mdx | 26 +-- .../core-concepts/combinator-filter.mdx | 20 +- .../core-concepts/combinator-flatmap.mdx | 12 +- .../core-concepts/combinator-foreach-all.mdx | 8 +- .../patterns/core-concepts/combinator-map.mdx | 14 +- .../core-concepts/combinator-sequencing.mdx | 6 +- .../patterns/core-concepts/combinator-zip.mdx | 14 +- ...data-by-value-with-structural-equality.mdx | 14 +- .../constructor-fail-none-left.mdx | 18 +- ...onstructor-from-nullable-option-either.mdx | 42 ++-- .../constructor-succeed-some-right.mdx | 18 +- .../core-concepts/constructor-sync-async.mdx | 4 +- .../control-flow-with-combinators.mdx | 8 +- .../create-reusable-runtime-from-layers.mdx | 14 +- .../patterns/core-concepts/data-array.mdx | 25 ++- .../patterns/core-concepts/data-case.mdx | 16 +- .../patterns/core-concepts/data-cause.mdx | 8 +- .../patterns/core-concepts/data-class.mdx | 10 +- .../patterns/core-concepts/data-either.mdx | 46 ++-- .../patterns/core-concepts/data-option.mdx | 4 +- .../patterns/core-concepts/data-struct.mdx | 23 +- .../patterns/core-concepts/data-tuple.mdx | 25 ++- .../core-concepts/execute-with-runsync.mdx | 2 +- .../optional-pattern-optional-chains.mdx | 6 +- .../process-streaming-data-with-stream.mdx | 4 +- .../core-concepts/provide-config-layer.mdx | 16 +- .../representing-time-spans-with-duration.mdx | 4 +- .../solve-promise-problems-with-effect.mdx | 16 +- .../understand-effect-channels.mdx | 16 +- ...rstand-layers-for-dependency-injection.mdx | 25 ++- ...chunk-for-high-performance-collections.mdx | 2 +- .../use-pipe-for-composition.mdx | 2 +- .../wrap-asynchronous-computations.mdx | 33 +-- .../wrap-synchronous-computations.mdx | 6 +- .../write-sequential-code-with-gen.mdx | 6 +- ...accumulate-multiple-errors-with-either.mdx | 40 ++-- .../domain-modeling/brand-validate-parse.mdx | 4 +- .../define-contracts-with-schema.mdx | 20 +- .../domain-modeling/define-tagged-errors.mdx | 4 +- .../distinguish-not-found-from-errors.mdx | 4 +- .../domain-modeling-option-basics.mdx | 4 +- .../model-optional-values-with-option.mdx | 2 +- .../parse-with-schema-decode.mdx | 16 +- .../transform-data-with-schema.mdx | 63 ++---- .../use-gen-for-business-logic.mdx | 4 +- .../control-repetition-with-schedule.mdx | 2 +- .../error-handling-pattern-accumulation.mdx | 4 +- ...ror-handling-pattern-custom-strategies.mdx | 10 +- .../error-handling-pattern-propagation.mdx | 18 +- .../error-management-extract-cause.mdx | 6 +- .../error-management-hello-world.mdx | 18 +- .../handle-errors-with-catch.mdx | 24 ++- ...le-flaky-operations-with-retry-timeout.mdx | 23 +- .../handle-unexpected-errors-with-cause.mdx | 65 +++--- .../mapping-errors-to-fit-your-domain.mdx | 2 +- .../error-management/pattern-catchtag.mdx | 2 +- .../error-management/pattern-match.mdx | 22 +- .../pattern-option-either-checks.mdx | 36 ++-- .../pattern-option-either-match.mdx | 50 ++--- .../retry-based-on-specific-errors.mdx | 8 +- ...scheduling-pattern-exponential-backoff.mdx | 46 ++-- .../getting-started-handle-errors.mdx | 20 +- .../getting-started-retry-on-failure.mdx | 8 +- .../getting-started-transform-with-map.mdx | 2 +- .../build-a-basic-http-server.mdx | 4 +- .../create-a-testable-http-client-service.mdx | 24 ++- .../making-http-requests/http-caching.mdx | 28 +-- .../making-http-requests/http-hello-world.mdx | 16 +- .../http-json-responses.mdx | 14 +- .../making-http-requests/http-logging.mdx | 14 +- .../http-rate-limit-handling.mdx | 31 ++- .../making-http-requests/http-retries.mdx | 56 ++--- .../making-http-requests/http-timeouts.mdx | 26 +-- .../model-dependencies-as-services.mdx | 14 +- .../observability/observability-alerting.mdx | 4 +- .../observability/observability-debugging.mdx | 2 +- .../observability-distributed-tracing.mdx | 9 +- .../observability-prometheus.mdx | 2 +- .../getting-started/platform-hello-world.mdx | 6 +- .../platform-filesystem-operations.mdx | 12 +- .../platform-keyvaluestore-persistence.mdx | 3 +- .../platform-pattern-advanced-filesystem.mdx | 4 +- .../platform-pattern-command-execution.mdx | 204 ++++++++++-------- .../platform-pattern-path-manipulation.mdx | 2 +- .../platform-terminal-interactive.mdx | 2 +- .../compose-scoped-layers.mdx | 24 ++- ...e-managed-runtime-for-scoped-resources.mdx | 14 +- .../manual-scope-management.mdx | 4 +- ...resource-management-runtime-vs-provide.mdx | 14 +- .../resource-management/resource-timeouts.mdx | 2 +- .../scoped-service-layer.mdx | 24 ++- .../scheduling/scheduling-hello-world.mdx | 17 +- ...heduling-pattern-advanced-retry-chains.mdx | 73 +++---- .../scheduling-pattern-cron-expressions.mdx | 46 ++-- .../scheduling-pattern-debounce-throttle.mdx | 2 +- ...attern-repeat-effect-on-fixed-interval.mdx | 18 +- .../scheduling/scheduling-retry-basics.mdx | 12 +- .../schema/ai-schemas/output-basics.mdx | 8 +- .../schema/ai-schemas/output-descriptions.mdx | 4 +- .../schema/ai-schemas/output-enums.mdx | 10 +- .../schema/ai-schemas/output-nested.mdx | 2 +- .../schema/ai-schemas/output-unions.mdx | 10 +- .../schema/ai-schemas/parsing-basics.mdx | 6 +- .../schema/ai-schemas/parsing-partial.mdx | 2 +- .../schema/ai-schemas/parsing-recovery.mdx | 8 +- .../schema/ai-schemas/parsing-retry.mdx | 39 ++-- .../schema/ai-schemas/parsing-streaming.mdx | 4 +- .../schema/ai-schemas/vercel-ai-sdk.mdx | 6 +- .../patterns/schema/arrays/tuples.mdx | 29 +-- .../schema/async-validation/basic-async.mdx | 30 +-- .../schema/async-validation/batched-async.mdx | 14 +- .../async-validation/database-checks.mdx | 28 +-- .../external-api-validation.mdx | 24 +-- .../schema/composition/extend-schemas.mdx | 39 ++-- .../composition/inheritance-patterns.mdx | 21 +- .../schema/composition/merge-schemas.mdx | 39 ++-- .../patterns/schema/composition/pick-omit.mdx | 34 +-- .../environment-config/config-layers.mdx | 8 +- .../environment-config/env-variables.mdx | 20 +- .../environment-config/feature-flags.mdx | 4 +- .../environment-config/secrets-redaction.mdx | 4 +- .../error-handling/error-aggregation.mdx | 72 +++---- .../error-handling/recovery-strategies.mdx | 16 +- .../schema/error-handling/tagged-errors.mdx | 2 +- .../error-handling/user-friendly-messages.mdx | 28 +-- .../form-validation/async-validation.mdx | 14 +- .../patterns/schema/form-validation/basic.mdx | 18 +- .../form-validation/collect-all-errors.mdx | 14 +- .../form-validation/dependent-fields.mdx | 12 +- .../schema/form-validation/nested-forms.mdx | 24 +-- .../getting-started/handling-errors.mdx | 26 +-- .../schema/getting-started/schema-vs-zod.mdx | 18 +- .../schema/json-validation/config-files.mdx | 22 +- .../json-validation/database-columns.mdx | 12 +- .../json-validation/file-validation.mdx | 14 +- .../schema/json-validation/multiple-files.mdx | 20 +- .../json-validation/partial-documents.mdx | 24 +-- .../schema/json-validation/postgres-jsonb.mdx | 20 +- .../json-validation/schema-evolution.mdx | 10 +- .../schema/json-validation/with-defaults.mdx | 20 +- .../schema/objects/optional-fields.mdx | 2 +- .../schema/primitives/date-validation.mdx | 52 ++--- .../schema/primitives/enums-literals.mdx | 12 +- .../schema/primitives/number-validation.mdx | 26 +-- .../schema/primitives/string-validation.mdx | 28 +-- .../schema/recursive/basic-recursive.mdx | 10 +- .../patterns/schema/recursive/json-ast.mdx | 6 +- .../schema/recursive/nested-comments.mdx | 6 +- .../transformations/basic-transforms.mdx | 46 ++-- .../schema/transformations/bidirectional.mdx | 68 +++--- .../schema/transformations/branded-types.mdx | 32 +-- .../transformations/data-normalization.mdx | 70 +++--- .../patterns/schema/unions/basic-unions.mdx | 24 +-- .../schema/unions/discriminated-unions.mdx | 20 +- .../schema/unions/exhaustive-matching.mdx | 6 +- .../schema/unions/polymorphic-apis.mdx | 20 +- .../schema/validating-api-responses/basic.mdx | 4 +- .../error-handling.mdx | 8 +- .../nested-responses.mdx | 14 +- .../union-responses.mdx | 12 +- .../with-http-client.mdx | 18 +- .../validating-api-responses/with-retry.mdx | 4 +- .../schema/web-standards-validation/email.mdx | 16 +- .../web-standards-validation/http-headers.mdx | 24 +-- .../web-standards-validation/iso-date.mdx | 16 +- .../web-standards-validation/mime-types.mdx | 34 +-- .../schema/web-standards-validation/url.mdx | 14 +- .../schema/web-standards-validation/uuid.mdx | 14 +- ...ll-back-to-alternative-sink-on-failure.mdx | 34 +-- ...pattern-retry-failed-stream-operations.mdx | 19 +- ...n-send-stream-records-to-message-queue.mdx | 2 +- ...ink-pattern-write-stream-lines-to-file.mdx | 2 +- ...tream-pattern-advanced-transformations.mdx | 2 +- .../stream-pattern-backpressure-control.mdx | 2 +- .../streams/stream-pattern-error-handling.mdx | 26 +-- .../streams/stream-pattern-merge-combine.mdx | 4 +- .../stream-pattern-resource-management.mdx | 13 +- .../testing/mocking-dependencies-in-tests.mdx | 25 ++- ...rganize-layers-into-composable-modules.mdx | 56 ++--- .../testing/testing-concurrent-code.mdx | 12 +- .../testing/testing-property-based.mdx | 10 +- .../patterns/testing/testing-streams.mdx | 6 +- .../testing/testing-with-services.mdx | 7 +- .../testing/use-default-layer-for-tests.mdx | 30 +-- ...e-tests-that-adapt-to-application-code.mdx | 34 ++- ...charge-your-editor-with-the-effect-lsp.mdx | 16 +- .../tooling-devtools.mdx | 12 +- .../tooling-profiling.mdx | 2 +- .../tooling-type-errors.mdx | 4 +- package.json | 15 +- packages/analysis-core/package.json | 2 +- packages/api-server/package.json | 8 +- packages/ep-admin/package.json | 6 +- packages/ep-cli/package.json | 9 +- packages/ep-shared-services/package.json | 7 +- packages/mcp-transport/package.json | 4 +- packages/pipeline-state/package.json | 10 +- packages/toolkit/package.json | 14 +- 256 files changed, 2269 insertions(+), 2309 deletions(-) diff --git a/content/learning-roadmap/ROADMAP-module1.mdx b/content/learning-roadmap/ROADMAP-module1.mdx index ee432225..544e935b 100644 --- a/content/learning-roadmap/ROADMAP-module1.mdx +++ b/content/learning-roadmap/ROADMAP-module1.mdx @@ -35,7 +35,7 @@ This stage covers how to handle the reality of failure, moving from just crashin - **8. Defining Your Own Errors** `(Intermediate)` - 🟣 [Creating Type-Safe Errors with `Data.TaggedError`](./content/define-tagged-errors.mdx) - **9. Handling Errors** `(Intermediate)` - - 🟣 [Recovering from Failures with `catchTag` & `catchAll`](./content/handle-errors-with-catch.mdx) + - 🟣 [Recovering from Failures with `catchTag` & `catch`](./content/handle-errors-with-catch.mdx) - **10. Transforming Errors** `(Intermediate)` - 🟣 [Mapping Errors to Fit Your Domain](./content/mapping-errors.mdx) @@ -48,7 +48,7 @@ This stage provides a tour of Effect's powerful, immutable data structures that - **11. Handling Optional Values** `(Intermediate)` - 🟣 [Modeling Absence Safely with `Option`](./content/model-optional-values-with-option.mdx) - **12. Accumulating Errors** `(Intermediate)` - - 🟣 [Handling Multiple Errors with `Either`](./content/accumulate-multiple-errors-with-either.mdx) + - 🟣 [Handling Multiple Errors with `Result`](./content/accumulate-multiple-errors-with-either.mdx) - **13. High-Performance Collections** `(Intermediate)` - 🟣 [Using `Chunk` for Efficient Data Processing](./content/use-chunk-for-high-performance-collections.mdx) - **14. Comparing Data by Value** `(Intermediate)` diff --git a/content/learning-roadmap/ROADMAP-module10.mdx b/content/learning-roadmap/ROADMAP-module10.mdx index 9978defa..628fa1e9 100644 --- a/content/learning-roadmap/ROADMAP-module10.mdx +++ b/content/learning-roadmap/ROADMAP-module10.mdx @@ -13,7 +13,7 @@ This module introduces the most important data types in the Effect ecosystem, ex | ----------------| -------------------------------------------------------| ----------------------------------------------| | `Option` | Represent presence or absence of a value | [Model Optional Values Safely with Option](./ patterns / data - option) | -| `Either` | Represent a value that can be one of two types(e.g., error or success) | [Accumulate Multiple Errors with Either](./ patterns / data - either) | +| `Result` | Represent a value that can be one of two types(e.g., error or success) | [Accumulate Multiple Errors with Result](./ patterns / data - either) | | `Chunk` | High - performance, immutable collections | [Use Chunk for High - Performance Collections](./ patterns / data - chunk) | | `HashSet` | Immutable, high - performance set | [Work with Immutable Sets using HashSet](./ patterns / data - hashset) | | `BigDecimal` | Arbitrary - precision decimal arithmetic | [Work with Arbitrary - Precision Numbers using BigDecimal](./ patterns / data - bigdecimal) | @@ -23,10 +23,10 @@ This module introduces the most important data types in the Effect ecosystem, ex | `Exit` | Represent the result(success or failure) of an Effect | [Modeling Effect Results with Exit](./ patterns / data - exit) | | `Cause` | Rich, structured error causes for debugging | [Handle Unexpected Errors by Inspecting the Cause](./patterns/data - cause) | | `Redacted` | Securely handle sensitive data | [Redact and Handle Sensitive Data](./patterns/data - redacted) | -| `Data.struct` | Create immutable, structurally - typed objects | [Comparing Data by Value with Data.struct](./ patterns / data - struct) | +| Plain objects | Create immutable, structurally - typed values | [Comparing Data by Value](./ patterns / data - struct) | | `Data.tuple` | Create immutable, typed tuples | [Working with Tuples using Data.tuple](./ patterns / data - tuple) | | `Data.array` | Immutable, type - safe arrays | [Working with Immutable Arrays using Data.array](./ patterns / data - array) | -| `Data.case` | Create tagged unions and ADTs | [Modeling Tagged Unions with Data.case](./ patterns / data -case) | +| `Data.taggedEnum` | Create tagged unions and ADTs | [Modeling Tagged Unions](./ patterns / data -case) | | `Data.Class` | Type classes for equality, ordering, and hashing | [Type Classes for Equality, Ordering, and Hashing with Data.Class](./ patterns / data - class) | --- @@ -34,7 +34,7 @@ This module introduces the most important data types in the Effect ecosystem, ex ## Learning Path 1. ####[Model Optional Values Safely with Option](./ patterns / data - option) -2. ####[Accumulate Multiple Errors with Either](./ patterns / data - either) +2. ####[Accumulate Multiple Errors with Result](./ patterns / data - either) 3. ####[Use Chunk for High - Performance Collections](./ patterns / data - chunk) 4. ####[Work with Immutable Sets using HashSet](./ patterns / data - hashset) 5. ####[Work with Arbitrary - Precision Numbers using BigDecimal](./ patterns / data - bigdecimal) @@ -44,10 +44,10 @@ This module introduces the most important data types in the Effect ecosystem, ex 9. ####[Modeling Effect Results with Exit](./ patterns / data - exit) 10. ####[Handle Unexpected Errors by Inspecting the Cause](./ patterns / data - cause) 11. ####[Redact and Handle Sensitive Data](./ patterns / data - redacted) -12. ####[Comparing Data by Value with Data.struct](./ patterns / data - struct) +12. ####[Comparing Data by Value](./ patterns / data - struct) 13. ####[Working with Tuples using Data.tuple](./ patterns / data - tuple) 14. ####[Working with Immutable Arrays using Data.array](./ patterns / data - array) -15. ####[Modeling Tagged Unions with Data.case](./ patterns / data -case) +15. ####[Modeling Tagged Unions](./ patterns / data -case) 16. ####[Type Classes for Equality, Ordering, and Hashing with Data.Class](./ patterns / data - class) --- diff --git a/content/learning-roadmap/ROADMAP-module11-schema-patterns.mdx b/content/learning-roadmap/ROADMAP-module11-schema-patterns.mdx index 6c5b9a71..c6c1bf12 100644 --- a/content/learning-roadmap/ROADMAP-module11-schema-patterns.mdx +++ b/content/learning-roadmap/ROADMAP-module11-schema-patterns.mdx @@ -43,7 +43,7 @@ Learn why runtime validation is critical and how Schema provides a unified appro - 🟣 **Documentation**: Self-documenting contracts - **3. The Schema β†’ Effect Integration** `(Intermediate)` - - 🟣 How Schema.decode returns an Effect + - 🟣 How Schema.decodeEffect returns an Effect - 🟣 Error handling built into the validation pipeline - 🟣 Composing validation with other Effect operations @@ -125,10 +125,10 @@ Transform data during validation to get exactly what you need. ### Parsing Operations -- **12. Parse and Validate with Schema.decode** `(Intermediate)` - - 🟣 [Using Schema.decode](./content/published/patterns/core/parse-with-schema-decode.mdx) - - 🟣 `Schema.decodeUnknown` for untrusted data - - 🟣 `Schema.validate` for validation without parsing +- **12. Parse and Validate with Schema.decodeEffect** `(Intermediate)` + - 🟣 [Using Schema.decodeEffect](./content/published/patterns/core/parse-with-schema-decode.mdx) + - 🟣 `Schema.decodeUnknownEffect` for untrusted data + - 🟣 `Schema.decode*` + `Schema.toType` for validation without full parsing - 🟣 Error handling in the parse pipeline ### Transformation diff --git a/content/learning-roadmap/ROADMAP-module13-scheduling-patterns.mdx b/content/learning-roadmap/ROADMAP-module13-scheduling-patterns.mdx index 70ffd9e8..5fb7bf6c 100644 --- a/content/learning-roadmap/ROADMAP-module13-scheduling-patterns.mdx +++ b/content/learning-roadmap/ROADMAP-module13-scheduling-patterns.mdx @@ -44,7 +44,7 @@ Learn why timing is critical and how Schedule abstracts timing concerns. - **3. Built-in Schedule Basics** `(Intermediate)` - 🟣 `Schedule.recurs` for fixed repetitions - 🟣 `Schedule.forever` for infinite repetition - - 🟣 `Schedule.once` for single execution + - 🟣 `Schedule.duration(Duration.zero)` for single execution --- @@ -72,7 +72,7 @@ Learn core patterns for repeating operations. ### Timed Repetition - **7. Repeat Effect While Result Satisfies Condition** `(Intermediate)` - - 🟣 Using `Schedule.whileOutput` for predicate-based repetition + - 🟣 Using `Schedule.while` with `output` metadata for predicate-based repetition - 🟣 Deciding when to stop based on result - 🟣 Exponential search patterns @@ -95,7 +95,7 @@ Learn sophisticated retry and backoff patterns. - 🟣 Cap maximum delay to prevent extreme waits - **10. Linear Backoff with Ceiling** `(Intermediate)` - - 🟣 Using `Schedule.linear` for predictable delays + - 🟣 Building linear delays with `Schedule.forever`, `Schedule.map`, and `Schedule.modifyDelay` - 🟣 When linear beats exponential - 🟣 Composing with caps and maximums @@ -108,7 +108,7 @@ Learn sophisticated retry and backoff patterns. - **12. Backoff with Maximum Total Time** `(Intermediate)` - 🟣 Limiting total retry duration - - 🟣 Using `Schedule.elapsed` to bound retries + - 🟣 Bounding retries with `Schedule.map` over schedule metadata (`elapsed`) - 🟣 Giving up after reasonable effort --- @@ -150,12 +150,12 @@ Learn to build complex schedules from simpler pieces. ### Combining Schedules - **17. Combine Multiple Schedules with OR Logic** `(Intermediate)` - - 🟣 Using `Schedule.union` to try alternatives + - 🟣 Using `Schedule.min` to try alternatives (fastest delay wins) - 🟣 Fallback schedules - 🟣 Racing schedule decisions - **18. Combine Multiple Schedules with AND Logic** `(Intermediate)` - - 🟣 Using `Schedule.intersect` for strict requirements + - 🟣 Using `Schedule.max` for strict requirements (slowest delay wins) - 🟣 Both conditions must be satisfied - 🟣 Coordinating independent timing rules @@ -167,7 +167,7 @@ Learn to build complex schedules from simpler pieces. - 🟣 Learning from outcomes - **20. Reset Schedule State Based on Events** `(Intermediate)` - - 🟣 Using `Schedule.resetAfter` to restart backoff + - 🟣 Reacquiring schedule state with `Schedule.fromStep`/`Schedule.toStep` to restart backoff - 🟣 Circuit breaker patterns - 🟣 Success-based resets diff --git a/content/learning-roadmap/ROADMAP-module2.mdx b/content/learning-roadmap/ROADMAP-module2.mdx index 76d7b805..0353e991 100644 --- a/content/learning-roadmap/ROADMAP-module2.mdx +++ b/content/learning-roadmap/ROADMAP-module2.mdx @@ -43,7 +43,7 @@ Follow these patterns in order to progressively build your knowledge. 7. #### [Provide Dependencies to Routes](./patterns/provide-dependencies-to-routes) - **Goal**: Inject services like database connections into HTTP route handlers using Layer and Effect.Service. + **Goal**: Inject services like database connections into HTTP route handlers using Layer and Context.Service. This is the key to building scalable, testable, and maintainable applications by decoupling your logic from its dependencies. 8. #### [Make an Outgoing HTTP Client Request](./patterns/make-http-client-request) diff --git a/content/learning-roadmap/ROADMAP-module5.mdx b/content/learning-roadmap/ROADMAP-module5.mdx index 1c7a44aa..18e22e79 100644 --- a/content/learning-roadmap/ROADMAP-module5.mdx +++ b/content/learning-roadmap/ROADMAP-module5.mdx @@ -1,6 +1,6 @@ # Module 5: Composing with Combinators -Combinators are the heart of functional programming with Effectβ€”they let you build complex, robust workflows by composing simple building blocks. The same combinators (`map`, `flatMap`, `filter`, etc.) work across many types: `Effect`, `Stream`, `Option`, and `Either`. +Combinators are the heart of functional programming with Effectβ€”they let you build complex, robust workflows by composing simple building blocks. The same combinators (`map`, `flatMap`, `filter`, etc.) work across many types: `Effect`, `Stream`, `Option`, and `Result`. Mastering these will make your code more expressive, maintainable, and safe. This module will teach you the most important combinators, how they work, and how to use them to compose computations, handle errors, branch conditionally, and work with collections and streams. You’ll see that the same mental model applies everywhere. @@ -15,7 +15,7 @@ This module will teach you the most important combinators, how they work, and ho | `flatMap` | Chain computations that may themselves be effectful | [Chaining Computations with flatMap](./patterns/combinator-flatmap) | | `filter` | Keep/discard values based on a predicate | [Filtering Results with filter](./patterns/combinator-filter) | | `if`, `when`, `cond` | Declarative conditional branching | [Conditional Branching with if, when, and cond](./patterns/combinator-conditional) | -| `catchAll`, `orElse`, `match` | Handle errors, provide fallbacks | [Handling Errors with catchAll, orElse, and match](./patterns/combinator-error-handling) | +| `catch`, `orElse`, `match` | Handle errors, provide fallbacks | [Handling Errors with catch, orElse, and match](./patterns/combinator-error-handling) | | `forEach`, `all` | Apply effectful functions to collections, batch/parallel | [Mapping and Chaining over Collections with forEach and all](./patterns/combinator-foreach-all) | | `zip` | Pair results of two computations | [Combining Values with zip](./patterns/combinator-zip) | | `andThen`, `tap`, `flatten` | Sequence, run side effects, flatten nesting | [Sequencing with andThen, tap, and flatten](./patterns/combinator-sequencing) | @@ -28,7 +28,7 @@ Follow these patterns in order to build a comprehensive understanding of combina 1. #### [Transforming Values with map](./patterns/combinator-map) - **Goal**: Learn how to transform the result of an `Effect`, `Stream`, `Option`, or `Either` using `map`. + **Goal**: Learn how to transform the result of an `Effect`, `Stream`, `Option`, or `Result` using `map`. 2. #### [Chaining Computations with flatMap](./patterns/combinator-flatmap) @@ -42,7 +42,7 @@ Follow these patterns in order to build a comprehensive understanding of combina **Goal**: Express conditional logic and branching workflows using combinators instead of imperative `if` statements. -5. #### [Handling Errors with catchAll, orElse, and match](./patterns/combinator-error-handling) +5. #### [Handling Errors with catch, orElse, and match](./patterns/combinator-error-handling) **Goal**: Recover from errors, provide fallbacks, or transform errors using combinators. @@ -61,7 +61,7 @@ Follow these patterns in order to build a comprehensive understanding of combina **By the end of this module, you’ll be able to:** -- Recognize and use the most important combinators in Effect, Stream, Option, and Either. +- Recognize and use the most important combinators in Effect, Stream, Option, and Result. - Write expressive, declarative, and robust code by composing small building blocks. - Understand the universal mental model behind functional combinators. diff --git a/content/learning-roadmap/ROADMAP-module6.mdx b/content/learning-roadmap/ROADMAP-module6.mdx index 30c25c02..33ea74b6 100644 --- a/content/learning-roadmap/ROADMAP-module6.mdx +++ b/content/learning-roadmap/ROADMAP-module6.mdx @@ -1,6 +1,6 @@ # Module 6: Creating with Constructors -Constructors are the entry point to the Effect ecosystem. They let you create `Effect`, `Stream`, `Option`, and `Either` values from plain values, errors, promises, callbacks, and more. +Constructors are the entry point to the Effect ecosystem. They let you create `Effect`, `Stream`, `Option`, and `Result` values from plain values, errors, promises, callbacks, and more. Mastering constructors is the first step to writing robust, type-safe, and composable functional code. This module will teach you the most important constructors, how they work, and when to use each one. You’ll learn to lift values and errors into the Effect world, wrap synchronous and asynchronous computations, and create streams and options from real-world data. @@ -16,7 +16,7 @@ This module will teach you the most important constructors, how they work, and w | `try`, `tryPromise` | Wrap sync/async computations that may throw | [Wrapping Synchronous and Asynchronous Computations](./patterns/constructor-try-trypromise) | | `sync`, `async` | Create from callback-based or sync code | [Creating from Synchronous and Callback Code](./patterns/constructor-sync-async) | | `fromIterable`, `fromArray` | Create from collections | [Creating from Collections](./patterns/constructor-from-iterable) | -| `fromNullable`, `fromOption`, `fromEither` | Convert from nullable, Option, or Either | [Converting from Nullable, Option, or Either](./patterns/constructor-from-nullable-option-either) | +| `fromNullishOr`, `fromOption`, `fromResult` | Convert from nullable, Option, or Result | [Converting from Nullable, Option, or Result](./patterns/constructor-from-nullable-option-either) | --- @@ -26,7 +26,7 @@ Follow these patterns in order to build a comprehensive understanding of constru 1. #### [Lifting Values with succeed, some, and right](./patterns/constructor-succeed-some-right) - **Goal**: Learn how to create an `Effect`, `Option`, or `Either` from a plain value. + **Goal**: Learn how to create an `Effect`, `Option`, or `Result` from a plain value. 2. #### [Lifting Errors and Absence with fail, none, and left](./patterns/constructor-fail-none-left) @@ -44,8 +44,8 @@ Follow these patterns in order to build a comprehensive understanding of constru **Goal**: Create streams or effects from arrays, iterables, or other collections. -6. #### [Converting from Nullable, Option, or Either](./patterns/constructor-from-nullable-option-either) - **Goal**: Convert nullable values, `Option`, or `Either` into Effects or Streams. +6. #### [Converting from Nullable, Option, or Result](./patterns/constructor-from-nullable-option-either) + **Goal**: Convert nullable values, `Option`, or `Result` into Effects or Streams. --- diff --git a/content/learning-roadmap/ROADMAP-module7.mdx b/content/learning-roadmap/ROADMAP-module7.mdx index a0ffb145..3249dc42 100644 --- a/content/learning-roadmap/ROADMAP-module7.mdx +++ b/content/learning-roadmap/ROADMAP-module7.mdx @@ -2,7 +2,7 @@ Pattern matching is a cornerstone of robust, declarative functional programming. It allows you to handle different cases of data, errors, and tagged unions in a type-safe, readable, and maintainable way. -Effect, Option, and Either all provide powerful pattern matching combinators that let you express complex logic without resorting to nested if/else or switch statements. +Effect, Option, and Result all provide powerful pattern matching combinators that let you express complex logic without resorting to nested if/else or switch statements. This module will teach you the most important pattern matching techniques in the Effect ecosystem, how to use them, and when to reach for each one. @@ -16,8 +16,8 @@ This module will teach you the most important pattern matching techniques in the | `matchTag`, `matchTags` | Match on specific tagged union cases | [Matching Tagged Unions with matchTag and matchTags](./patterns/pattern-matchtag) | | `matchEffect` | Pattern match with effectful branches | [Effectful Pattern Matching with matchEffect](./patterns/pattern-matcheffect) | | `catchTag`, `catchTags` | Handle specific error types in the failure channel | [Handling Specific Errors with catchTag and catchTags](./patterns/pattern-catchtag) | -| `Option.match`, `Either.match` | Handle Option/Either cases declaratively | [Pattern Matching on Option and Either](./patterns/pattern-option-either-match) | -| `isSome`, `isNone`, `isLeft`, `isRight` | Simple case checks for Option/Either | [Checking Option and Either Cases](./patterns/pattern-option-either-checks) | +| `Option.match`, `Result.match` | Handle Option/Result cases declaratively | [Pattern Matching on Option and Result](./patterns/pattern-option-either-match) | +| `isSome`, `isNone`, `isFailure`, `isSuccess` | Simple case checks for Option/Result | [Checking Option and Result Cases](./patterns/pattern-option-either-checks) | --- @@ -41,11 +41,11 @@ Follow these patterns in order to build a comprehensive understanding of pattern **Goal**: Recover from or handle specific error types in the Effect failure channel. -5. #### [Pattern Matching on Option and Either](./patterns/pattern-option-either-match) +5. #### [Pattern Matching on Option and Result](./patterns/pattern-option-either-match) - **Goal**: Handle Option and Either cases declaratively, without manual checks. + **Goal**: Handle Option and Result cases declaratively, without manual checks. -6. #### [Checking Option and Either Cases](./patterns/pattern-option-either-checks) +6. #### [Checking Option and Result Cases](./patterns/pattern-option-either-checks) **Goal**: Use simple predicates to check for Some/None or Left/Right cases. --- diff --git a/content/learning-roadmap/ROADMAP-overview.mdx b/content/learning-roadmap/ROADMAP-overview.mdx index cea87a9c..4710bd65 100644 --- a/content/learning-roadmap/ROADMAP-overview.mdx +++ b/content/learning-roadmap/ROADMAP-overview.mdx @@ -39,7 +39,7 @@ you have the foundation to understand them. - Creating and executing Effects - Composing Effects with `pipe`, `map`, `flatMap`, and `Effect.gen` - Type-safe error handling with `Data.TaggedError` -- Working with Effect's data types: `Option`, `Either`, `Chunk` +- Working with Effect's data types: `Option`, `Result`, `Chunk` - Time management with `Duration`, `Schedule`, and `Clock` - Domain modeling with `Schema` and `Brand` - Observability with structured logging, metrics, and tracing @@ -63,7 +63,7 @@ you have the foundation to understand them. - Validating request bodies with Schema - Sending JSON responses with proper status codes - Translating application errors to HTTP error responses -- Dependency injection for routes using Layer and Effect.Service +- Dependency injection for routes using Layer and Context.Service - Making outgoing HTTP client requests **Key Patterns**: 8 patterns covering the complete HTTP request lifecycle @@ -120,13 +120,13 @@ you have the foundation to understand them. - Chaining computations with `flatMap` - Filtering results with `filter` - Conditional branching with `if`, `when`, and `cond` -- Error handling with `catchAll`, `orElse`, and `match` +- Error handling with `catch`, `orElse`, and `match` - Working with collections using `forEach` and `all` - Combining values with `zip` - Sequencing with `andThen`, `tap`, and `flatten` **Key Insight**: The same combinators work across `Effect`, `Stream`, -`Option`, and `Either` +`Option`, and `Result` --- @@ -143,7 +143,7 @@ you have the foundation to understand them. - Wrapping computations with `try` and `tryPromise` - Creating from synchronous and callback code - Creating from collections and iterables -- Converting from nullable values, `Option`, and `Either` +- Converting from nullable values, `Option`, and `Result` **Key Skill**: Bringing any value, error, or computation into the Effect world @@ -161,7 +161,7 @@ you have the foundation to understand them. - Matching tagged unions with `matchTag` and `matchTags` - Effectful pattern matching with `matchEffect` - Handling specific errors with `catchTag` and `catchTags` -- Pattern matching on `Option` and `Either` +- Pattern matching on `Option` and `Result` - Simple case checks with predicates **Key Benefit**: Replace nested if/else and switch statements with @@ -214,7 +214,7 @@ types **What You'll Learn**: - Optional values with `Option` -- Error accumulation with `Either` +- Error accumulation with `Result` - High-performance collections with `Chunk` and `HashSet` - Arbitrary-precision arithmetic with `BigDecimal` - Time-zone-aware dates with `DateTime` @@ -222,8 +222,8 @@ types - Safe concurrent state with `Ref` - Effect results with `Exit` and `Cause` - Sensitive data handling with `Redacted` -- Immutable structures with `Data.struct`, `Data.tuple`, and `Data.array` -- Tagged unions with `Data.case` +- Structural equality for plain objects, tuples, and arrays with `Equal.equals` +- Tagged unions with `Data.taggedEnum` - Type classes with `Data.Class` **Key Benefit**: Practical, robust functional programming in TypeScript @@ -239,7 +239,7 @@ types **What You'll Learn**: - Defining data contracts upfront with `Schema.Struct` -- Validating and parsing unknown data with `Schema.decode` +- Validating and parsing unknown data with `Schema.decodeEffect` - Adding constraints: string length, numeric ranges, custom predicates - Transforming data during validation (string β†’ Date, raw β†’ Branded types) - Handling validation errors gracefully and user-friendly diff --git a/content/new/pattern-option-either-match.mdx b/content/new/pattern-option-either-match.mdx index 04a7989d..b8ab612c 100644 --- a/content/new/pattern-option-either-match.mdx +++ b/content/new/pattern-option-either-match.mdx @@ -1,6 +1,6 @@ --- id: pattern-option-either-match -title: Pattern Match on Option and Either +title: Pattern Match on Option and Result summary: Use declarative match() combinators to handle optional and error-prone values skillLevel: beginner useCase: ["Control Flow"] @@ -8,12 +8,12 @@ tags: [option, either, pattern-matching, match, declarative] related: [pattern-match, data-option, data-either, pattern-option-either-checks] author: Effect Patterns Hub rule: - description: "Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values" + description: "Use Option.match() and Result.match() for declarative pattern matching on optional and error-prone values" --- ## Guideline -When you need to handle `Option` or `Either` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Either) in a single expression. +When you need to handle `Option` or `Result` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Result) in a single expression. Use `.match()` when: - You need to handle both success and failure cases @@ -23,7 +23,7 @@ Use `.match()` when: ## Rationale -The `.match()` combinator is superior to manual checks (`isSome()`, `isLeft()`) because: +The `.match()` combinator is superior to manual checks (`isSome()`, `isFailure()`) because: 1. **Declarative**: Expresses intent clearly - "match on these cases" 2. **Type-safe**: TypeScript ensures all cases are handled @@ -57,23 +57,23 @@ console.log(displayUser(1)); // "Hello, Alice!" console.log(displayUser(999)); // "Guest User" ``` -### Basic Either Matching +### Basic Result Matching ```typescript -import { Either } from "effect"; +import { Result } from "effect"; -const validateAge = (age: number): Either.Either => { +const validateAge = (age: number): Result.Result => { return age >= 18 - ? Either.right(age) - : Either.left("Must be 18 or older"); + ? Result.succeed(age) + : Result.fail("Must be 18 or older"); }; // Using .match() for error handling const processAge = (age: number): string => validateAge(age).pipe( - Either.match({ - onLeft: (error) => `Validation failed: ${error}`, - onRight: (validAge) => `Age ${validAge} is valid`, + Result.match({ + onFailure: (error) => `Validation failed: ${error}`, + onSuccess: (validAge) => `Age ${validAge} is valid`, }) ); @@ -83,10 +83,10 @@ console.log(processAge(15)); // "Validation failed: Must be 18 or older" ### Advanced: Nested Matching -When dealing with nested Option and Either, use nested `.match()` calls: +When dealing with nested Option and Result, use nested `.match()` calls: ```typescript -import { Option, Either } from "effect"; +import { Option, Result } from "effect"; interface UserProfile { name: string; @@ -95,22 +95,22 @@ interface UserProfile { const getUserProfile = ( id: number -): Option.Option> => { +): Option.Option> => { if (id === 0) return Option.none(); // User not found - if (id === 1) return Option.some(Either.left("Profile incomplete")); - return Option.some(Either.right({ name: "Bob", age: 25 })); + if (id === 1) return Option.some(Result.fail("Profile incomplete")); + return Option.some(Result.succeed({ name: "Bob", age: 25 })); }; -// Nested matching - first on Option, then on Either +// Nested matching - first on Option, then on Result const displayProfile = (id: number): string => getUserProfile(id).pipe( Option.match({ onNone: () => "User not found", onSome: (result) => result.pipe( - Either.match({ - onLeft: (error) => `Error: ${error}`, - onRight: (profile) => `${profile.name} (${profile.age})`, + Result.match({ + onFailure: (error) => `Error: ${error}`, + onSuccess: (profile) => `${profile.name} (${profile.age})`, }) ), }) @@ -138,9 +138,9 @@ if (Option.isSome(name)) { // ❌ ANTI-PATTERN: Nested ternaries const ageResult = validateAge(25); const message = ageResult.pipe( - Either.match({ - onLeft: () => "Invalid", - onRight: (age) => age >= 21 ? "Can drink" : "Cannot drink", + Result.match({ + onFailure: () => "Invalid", + onSuccess: (age) => age >= 21 ? "Can drink" : "Cannot drink", }) ); diff --git a/content/new/raw/ROADMAP-module1.mdx b/content/new/raw/ROADMAP-module1.mdx index ee432225..544e935b 100644 --- a/content/new/raw/ROADMAP-module1.mdx +++ b/content/new/raw/ROADMAP-module1.mdx @@ -35,7 +35,7 @@ This stage covers how to handle the reality of failure, moving from just crashin - **8. Defining Your Own Errors** `(Intermediate)` - 🟣 [Creating Type-Safe Errors with `Data.TaggedError`](./content/define-tagged-errors.mdx) - **9. Handling Errors** `(Intermediate)` - - 🟣 [Recovering from Failures with `catchTag` & `catchAll`](./content/handle-errors-with-catch.mdx) + - 🟣 [Recovering from Failures with `catchTag` & `catch`](./content/handle-errors-with-catch.mdx) - **10. Transforming Errors** `(Intermediate)` - 🟣 [Mapping Errors to Fit Your Domain](./content/mapping-errors.mdx) @@ -48,7 +48,7 @@ This stage provides a tour of Effect's powerful, immutable data structures that - **11. Handling Optional Values** `(Intermediate)` - 🟣 [Modeling Absence Safely with `Option`](./content/model-optional-values-with-option.mdx) - **12. Accumulating Errors** `(Intermediate)` - - 🟣 [Handling Multiple Errors with `Either`](./content/accumulate-multiple-errors-with-either.mdx) + - 🟣 [Handling Multiple Errors with `Result`](./content/accumulate-multiple-errors-with-either.mdx) - **13. High-Performance Collections** `(Intermediate)` - 🟣 [Using `Chunk` for Efficient Data Processing](./content/use-chunk-for-high-performance-collections.mdx) - **14. Comparing Data by Value** `(Intermediate)` diff --git a/content/new/raw/ROADMAP-module10.mdx b/content/new/raw/ROADMAP-module10.mdx index 9978defa..628fa1e9 100644 --- a/content/new/raw/ROADMAP-module10.mdx +++ b/content/new/raw/ROADMAP-module10.mdx @@ -13,7 +13,7 @@ This module introduces the most important data types in the Effect ecosystem, ex | ----------------| -------------------------------------------------------| ----------------------------------------------| | `Option` | Represent presence or absence of a value | [Model Optional Values Safely with Option](./ patterns / data - option) | -| `Either` | Represent a value that can be one of two types(e.g., error or success) | [Accumulate Multiple Errors with Either](./ patterns / data - either) | +| `Result` | Represent a value that can be one of two types(e.g., error or success) | [Accumulate Multiple Errors with Result](./ patterns / data - either) | | `Chunk` | High - performance, immutable collections | [Use Chunk for High - Performance Collections](./ patterns / data - chunk) | | `HashSet` | Immutable, high - performance set | [Work with Immutable Sets using HashSet](./ patterns / data - hashset) | | `BigDecimal` | Arbitrary - precision decimal arithmetic | [Work with Arbitrary - Precision Numbers using BigDecimal](./ patterns / data - bigdecimal) | @@ -23,10 +23,10 @@ This module introduces the most important data types in the Effect ecosystem, ex | `Exit` | Represent the result(success or failure) of an Effect | [Modeling Effect Results with Exit](./ patterns / data - exit) | | `Cause` | Rich, structured error causes for debugging | [Handle Unexpected Errors by Inspecting the Cause](./patterns/data - cause) | | `Redacted` | Securely handle sensitive data | [Redact and Handle Sensitive Data](./patterns/data - redacted) | -| `Data.struct` | Create immutable, structurally - typed objects | [Comparing Data by Value with Data.struct](./ patterns / data - struct) | +| Plain objects | Create immutable, structurally - typed values | [Comparing Data by Value](./ patterns / data - struct) | | `Data.tuple` | Create immutable, typed tuples | [Working with Tuples using Data.tuple](./ patterns / data - tuple) | | `Data.array` | Immutable, type - safe arrays | [Working with Immutable Arrays using Data.array](./ patterns / data - array) | -| `Data.case` | Create tagged unions and ADTs | [Modeling Tagged Unions with Data.case](./ patterns / data -case) | +| `Data.taggedEnum` | Create tagged unions and ADTs | [Modeling Tagged Unions](./ patterns / data -case) | | `Data.Class` | Type classes for equality, ordering, and hashing | [Type Classes for Equality, Ordering, and Hashing with Data.Class](./ patterns / data - class) | --- @@ -34,7 +34,7 @@ This module introduces the most important data types in the Effect ecosystem, ex ## Learning Path 1. ####[Model Optional Values Safely with Option](./ patterns / data - option) -2. ####[Accumulate Multiple Errors with Either](./ patterns / data - either) +2. ####[Accumulate Multiple Errors with Result](./ patterns / data - either) 3. ####[Use Chunk for High - Performance Collections](./ patterns / data - chunk) 4. ####[Work with Immutable Sets using HashSet](./ patterns / data - hashset) 5. ####[Work with Arbitrary - Precision Numbers using BigDecimal](./ patterns / data - bigdecimal) @@ -44,10 +44,10 @@ This module introduces the most important data types in the Effect ecosystem, ex 9. ####[Modeling Effect Results with Exit](./ patterns / data - exit) 10. ####[Handle Unexpected Errors by Inspecting the Cause](./ patterns / data - cause) 11. ####[Redact and Handle Sensitive Data](./ patterns / data - redacted) -12. ####[Comparing Data by Value with Data.struct](./ patterns / data - struct) +12. ####[Comparing Data by Value](./ patterns / data - struct) 13. ####[Working with Tuples using Data.tuple](./ patterns / data - tuple) 14. ####[Working with Immutable Arrays using Data.array](./ patterns / data - array) -15. ####[Modeling Tagged Unions with Data.case](./ patterns / data -case) +15. ####[Modeling Tagged Unions](./ patterns / data -case) 16. ####[Type Classes for Equality, Ordering, and Hashing with Data.Class](./ patterns / data - class) --- diff --git a/content/new/raw/ROADMAP-module11-schema-patterns.mdx b/content/new/raw/ROADMAP-module11-schema-patterns.mdx index 6c5b9a71..c6c1bf12 100644 --- a/content/new/raw/ROADMAP-module11-schema-patterns.mdx +++ b/content/new/raw/ROADMAP-module11-schema-patterns.mdx @@ -43,7 +43,7 @@ Learn why runtime validation is critical and how Schema provides a unified appro - 🟣 **Documentation**: Self-documenting contracts - **3. The Schema β†’ Effect Integration** `(Intermediate)` - - 🟣 How Schema.decode returns an Effect + - 🟣 How Schema.decodeEffect returns an Effect - 🟣 Error handling built into the validation pipeline - 🟣 Composing validation with other Effect operations @@ -125,10 +125,10 @@ Transform data during validation to get exactly what you need. ### Parsing Operations -- **12. Parse and Validate with Schema.decode** `(Intermediate)` - - 🟣 [Using Schema.decode](./content/published/patterns/core/parse-with-schema-decode.mdx) - - 🟣 `Schema.decodeUnknown` for untrusted data - - 🟣 `Schema.validate` for validation without parsing +- **12. Parse and Validate with Schema.decodeEffect** `(Intermediate)` + - 🟣 [Using Schema.decodeEffect](./content/published/patterns/core/parse-with-schema-decode.mdx) + - 🟣 `Schema.decodeUnknownEffect` for untrusted data + - 🟣 `Schema.decode*` + `Schema.toType` for validation without full parsing - 🟣 Error handling in the parse pipeline ### Transformation diff --git a/content/new/raw/ROADMAP-module13-scheduling-patterns.mdx b/content/new/raw/ROADMAP-module13-scheduling-patterns.mdx index 70ffd9e8..5fb7bf6c 100644 --- a/content/new/raw/ROADMAP-module13-scheduling-patterns.mdx +++ b/content/new/raw/ROADMAP-module13-scheduling-patterns.mdx @@ -44,7 +44,7 @@ Learn why timing is critical and how Schedule abstracts timing concerns. - **3. Built-in Schedule Basics** `(Intermediate)` - 🟣 `Schedule.recurs` for fixed repetitions - 🟣 `Schedule.forever` for infinite repetition - - 🟣 `Schedule.once` for single execution + - 🟣 `Schedule.duration(Duration.zero)` for single execution --- @@ -72,7 +72,7 @@ Learn core patterns for repeating operations. ### Timed Repetition - **7. Repeat Effect While Result Satisfies Condition** `(Intermediate)` - - 🟣 Using `Schedule.whileOutput` for predicate-based repetition + - 🟣 Using `Schedule.while` with `output` metadata for predicate-based repetition - 🟣 Deciding when to stop based on result - 🟣 Exponential search patterns @@ -95,7 +95,7 @@ Learn sophisticated retry and backoff patterns. - 🟣 Cap maximum delay to prevent extreme waits - **10. Linear Backoff with Ceiling** `(Intermediate)` - - 🟣 Using `Schedule.linear` for predictable delays + - 🟣 Building linear delays with `Schedule.forever`, `Schedule.map`, and `Schedule.modifyDelay` - 🟣 When linear beats exponential - 🟣 Composing with caps and maximums @@ -108,7 +108,7 @@ Learn sophisticated retry and backoff patterns. - **12. Backoff with Maximum Total Time** `(Intermediate)` - 🟣 Limiting total retry duration - - 🟣 Using `Schedule.elapsed` to bound retries + - 🟣 Bounding retries with `Schedule.map` over schedule metadata (`elapsed`) - 🟣 Giving up after reasonable effort --- @@ -150,12 +150,12 @@ Learn to build complex schedules from simpler pieces. ### Combining Schedules - **17. Combine Multiple Schedules with OR Logic** `(Intermediate)` - - 🟣 Using `Schedule.union` to try alternatives + - 🟣 Using `Schedule.min` to try alternatives (fastest delay wins) - 🟣 Fallback schedules - 🟣 Racing schedule decisions - **18. Combine Multiple Schedules with AND Logic** `(Intermediate)` - - 🟣 Using `Schedule.intersect` for strict requirements + - 🟣 Using `Schedule.max` for strict requirements (slowest delay wins) - 🟣 Both conditions must be satisfied - 🟣 Coordinating independent timing rules @@ -167,7 +167,7 @@ Learn to build complex schedules from simpler pieces. - 🟣 Learning from outcomes - **20. Reset Schedule State Based on Events** `(Intermediate)` - - 🟣 Using `Schedule.resetAfter` to restart backoff + - 🟣 Reacquiring schedule state with `Schedule.fromStep`/`Schedule.toStep` to restart backoff - 🟣 Circuit breaker patterns - 🟣 Success-based resets diff --git a/content/new/raw/ROADMAP-module2.mdx b/content/new/raw/ROADMAP-module2.mdx index 76d7b805..0353e991 100644 --- a/content/new/raw/ROADMAP-module2.mdx +++ b/content/new/raw/ROADMAP-module2.mdx @@ -43,7 +43,7 @@ Follow these patterns in order to progressively build your knowledge. 7. #### [Provide Dependencies to Routes](./patterns/provide-dependencies-to-routes) - **Goal**: Inject services like database connections into HTTP route handlers using Layer and Effect.Service. + **Goal**: Inject services like database connections into HTTP route handlers using Layer and Context.Service. This is the key to building scalable, testable, and maintainable applications by decoupling your logic from its dependencies. 8. #### [Make an Outgoing HTTP Client Request](./patterns/make-http-client-request) diff --git a/content/new/raw/ROADMAP-module5.mdx b/content/new/raw/ROADMAP-module5.mdx index 1c7a44aa..18e22e79 100644 --- a/content/new/raw/ROADMAP-module5.mdx +++ b/content/new/raw/ROADMAP-module5.mdx @@ -1,6 +1,6 @@ # Module 5: Composing with Combinators -Combinators are the heart of functional programming with Effectβ€”they let you build complex, robust workflows by composing simple building blocks. The same combinators (`map`, `flatMap`, `filter`, etc.) work across many types: `Effect`, `Stream`, `Option`, and `Either`. +Combinators are the heart of functional programming with Effectβ€”they let you build complex, robust workflows by composing simple building blocks. The same combinators (`map`, `flatMap`, `filter`, etc.) work across many types: `Effect`, `Stream`, `Option`, and `Result`. Mastering these will make your code more expressive, maintainable, and safe. This module will teach you the most important combinators, how they work, and how to use them to compose computations, handle errors, branch conditionally, and work with collections and streams. You’ll see that the same mental model applies everywhere. @@ -15,7 +15,7 @@ This module will teach you the most important combinators, how they work, and ho | `flatMap` | Chain computations that may themselves be effectful | [Chaining Computations with flatMap](./patterns/combinator-flatmap) | | `filter` | Keep/discard values based on a predicate | [Filtering Results with filter](./patterns/combinator-filter) | | `if`, `when`, `cond` | Declarative conditional branching | [Conditional Branching with if, when, and cond](./patterns/combinator-conditional) | -| `catchAll`, `orElse`, `match` | Handle errors, provide fallbacks | [Handling Errors with catchAll, orElse, and match](./patterns/combinator-error-handling) | +| `catch`, `orElse`, `match` | Handle errors, provide fallbacks | [Handling Errors with catch, orElse, and match](./patterns/combinator-error-handling) | | `forEach`, `all` | Apply effectful functions to collections, batch/parallel | [Mapping and Chaining over Collections with forEach and all](./patterns/combinator-foreach-all) | | `zip` | Pair results of two computations | [Combining Values with zip](./patterns/combinator-zip) | | `andThen`, `tap`, `flatten` | Sequence, run side effects, flatten nesting | [Sequencing with andThen, tap, and flatten](./patterns/combinator-sequencing) | @@ -28,7 +28,7 @@ Follow these patterns in order to build a comprehensive understanding of combina 1. #### [Transforming Values with map](./patterns/combinator-map) - **Goal**: Learn how to transform the result of an `Effect`, `Stream`, `Option`, or `Either` using `map`. + **Goal**: Learn how to transform the result of an `Effect`, `Stream`, `Option`, or `Result` using `map`. 2. #### [Chaining Computations with flatMap](./patterns/combinator-flatmap) @@ -42,7 +42,7 @@ Follow these patterns in order to build a comprehensive understanding of combina **Goal**: Express conditional logic and branching workflows using combinators instead of imperative `if` statements. -5. #### [Handling Errors with catchAll, orElse, and match](./patterns/combinator-error-handling) +5. #### [Handling Errors with catch, orElse, and match](./patterns/combinator-error-handling) **Goal**: Recover from errors, provide fallbacks, or transform errors using combinators. @@ -61,7 +61,7 @@ Follow these patterns in order to build a comprehensive understanding of combina **By the end of this module, you’ll be able to:** -- Recognize and use the most important combinators in Effect, Stream, Option, and Either. +- Recognize and use the most important combinators in Effect, Stream, Option, and Result. - Write expressive, declarative, and robust code by composing small building blocks. - Understand the universal mental model behind functional combinators. diff --git a/content/new/raw/ROADMAP-module6.mdx b/content/new/raw/ROADMAP-module6.mdx index 30c25c02..33ea74b6 100644 --- a/content/new/raw/ROADMAP-module6.mdx +++ b/content/new/raw/ROADMAP-module6.mdx @@ -1,6 +1,6 @@ # Module 6: Creating with Constructors -Constructors are the entry point to the Effect ecosystem. They let you create `Effect`, `Stream`, `Option`, and `Either` values from plain values, errors, promises, callbacks, and more. +Constructors are the entry point to the Effect ecosystem. They let you create `Effect`, `Stream`, `Option`, and `Result` values from plain values, errors, promises, callbacks, and more. Mastering constructors is the first step to writing robust, type-safe, and composable functional code. This module will teach you the most important constructors, how they work, and when to use each one. You’ll learn to lift values and errors into the Effect world, wrap synchronous and asynchronous computations, and create streams and options from real-world data. @@ -16,7 +16,7 @@ This module will teach you the most important constructors, how they work, and w | `try`, `tryPromise` | Wrap sync/async computations that may throw | [Wrapping Synchronous and Asynchronous Computations](./patterns/constructor-try-trypromise) | | `sync`, `async` | Create from callback-based or sync code | [Creating from Synchronous and Callback Code](./patterns/constructor-sync-async) | | `fromIterable`, `fromArray` | Create from collections | [Creating from Collections](./patterns/constructor-from-iterable) | -| `fromNullable`, `fromOption`, `fromEither` | Convert from nullable, Option, or Either | [Converting from Nullable, Option, or Either](./patterns/constructor-from-nullable-option-either) | +| `fromNullishOr`, `fromOption`, `fromResult` | Convert from nullable, Option, or Result | [Converting from Nullable, Option, or Result](./patterns/constructor-from-nullable-option-either) | --- @@ -26,7 +26,7 @@ Follow these patterns in order to build a comprehensive understanding of constru 1. #### [Lifting Values with succeed, some, and right](./patterns/constructor-succeed-some-right) - **Goal**: Learn how to create an `Effect`, `Option`, or `Either` from a plain value. + **Goal**: Learn how to create an `Effect`, `Option`, or `Result` from a plain value. 2. #### [Lifting Errors and Absence with fail, none, and left](./patterns/constructor-fail-none-left) @@ -44,8 +44,8 @@ Follow these patterns in order to build a comprehensive understanding of constru **Goal**: Create streams or effects from arrays, iterables, or other collections. -6. #### [Converting from Nullable, Option, or Either](./patterns/constructor-from-nullable-option-either) - **Goal**: Convert nullable values, `Option`, or `Either` into Effects or Streams. +6. #### [Converting from Nullable, Option, or Result](./patterns/constructor-from-nullable-option-either) + **Goal**: Convert nullable values, `Option`, or `Result` into Effects or Streams. --- diff --git a/content/new/raw/ROADMAP-module7.mdx b/content/new/raw/ROADMAP-module7.mdx index a0ffb145..3249dc42 100644 --- a/content/new/raw/ROADMAP-module7.mdx +++ b/content/new/raw/ROADMAP-module7.mdx @@ -2,7 +2,7 @@ Pattern matching is a cornerstone of robust, declarative functional programming. It allows you to handle different cases of data, errors, and tagged unions in a type-safe, readable, and maintainable way. -Effect, Option, and Either all provide powerful pattern matching combinators that let you express complex logic without resorting to nested if/else or switch statements. +Effect, Option, and Result all provide powerful pattern matching combinators that let you express complex logic without resorting to nested if/else or switch statements. This module will teach you the most important pattern matching techniques in the Effect ecosystem, how to use them, and when to reach for each one. @@ -16,8 +16,8 @@ This module will teach you the most important pattern matching techniques in the | `matchTag`, `matchTags` | Match on specific tagged union cases | [Matching Tagged Unions with matchTag and matchTags](./patterns/pattern-matchtag) | | `matchEffect` | Pattern match with effectful branches | [Effectful Pattern Matching with matchEffect](./patterns/pattern-matcheffect) | | `catchTag`, `catchTags` | Handle specific error types in the failure channel | [Handling Specific Errors with catchTag and catchTags](./patterns/pattern-catchtag) | -| `Option.match`, `Either.match` | Handle Option/Either cases declaratively | [Pattern Matching on Option and Either](./patterns/pattern-option-either-match) | -| `isSome`, `isNone`, `isLeft`, `isRight` | Simple case checks for Option/Either | [Checking Option and Either Cases](./patterns/pattern-option-either-checks) | +| `Option.match`, `Result.match` | Handle Option/Result cases declaratively | [Pattern Matching on Option and Result](./patterns/pattern-option-either-match) | +| `isSome`, `isNone`, `isFailure`, `isSuccess` | Simple case checks for Option/Result | [Checking Option and Result Cases](./patterns/pattern-option-either-checks) | --- @@ -41,11 +41,11 @@ Follow these patterns in order to build a comprehensive understanding of pattern **Goal**: Recover from or handle specific error types in the Effect failure channel. -5. #### [Pattern Matching on Option and Either](./patterns/pattern-option-either-match) +5. #### [Pattern Matching on Option and Result](./patterns/pattern-option-either-match) - **Goal**: Handle Option and Either cases declaratively, without manual checks. + **Goal**: Handle Option and Result cases declaratively, without manual checks. -6. #### [Checking Option and Either Cases](./patterns/pattern-option-either-checks) +6. #### [Checking Option and Result Cases](./patterns/pattern-option-either-checks) **Goal**: Use simple predicates to check for Some/None or Left/Right cases. --- diff --git a/content/new/raw/ROADMAP-overview.mdx b/content/new/raw/ROADMAP-overview.mdx index cea87a9c..4710bd65 100644 --- a/content/new/raw/ROADMAP-overview.mdx +++ b/content/new/raw/ROADMAP-overview.mdx @@ -39,7 +39,7 @@ you have the foundation to understand them. - Creating and executing Effects - Composing Effects with `pipe`, `map`, `flatMap`, and `Effect.gen` - Type-safe error handling with `Data.TaggedError` -- Working with Effect's data types: `Option`, `Either`, `Chunk` +- Working with Effect's data types: `Option`, `Result`, `Chunk` - Time management with `Duration`, `Schedule`, and `Clock` - Domain modeling with `Schema` and `Brand` - Observability with structured logging, metrics, and tracing @@ -63,7 +63,7 @@ you have the foundation to understand them. - Validating request bodies with Schema - Sending JSON responses with proper status codes - Translating application errors to HTTP error responses -- Dependency injection for routes using Layer and Effect.Service +- Dependency injection for routes using Layer and Context.Service - Making outgoing HTTP client requests **Key Patterns**: 8 patterns covering the complete HTTP request lifecycle @@ -120,13 +120,13 @@ you have the foundation to understand them. - Chaining computations with `flatMap` - Filtering results with `filter` - Conditional branching with `if`, `when`, and `cond` -- Error handling with `catchAll`, `orElse`, and `match` +- Error handling with `catch`, `orElse`, and `match` - Working with collections using `forEach` and `all` - Combining values with `zip` - Sequencing with `andThen`, `tap`, and `flatten` **Key Insight**: The same combinators work across `Effect`, `Stream`, -`Option`, and `Either` +`Option`, and `Result` --- @@ -143,7 +143,7 @@ you have the foundation to understand them. - Wrapping computations with `try` and `tryPromise` - Creating from synchronous and callback code - Creating from collections and iterables -- Converting from nullable values, `Option`, and `Either` +- Converting from nullable values, `Option`, and `Result` **Key Skill**: Bringing any value, error, or computation into the Effect world @@ -161,7 +161,7 @@ you have the foundation to understand them. - Matching tagged unions with `matchTag` and `matchTags` - Effectful pattern matching with `matchEffect` - Handling specific errors with `catchTag` and `catchTags` -- Pattern matching on `Option` and `Either` +- Pattern matching on `Option` and `Result` - Simple case checks with predicates **Key Benefit**: Replace nested if/else and switch statements with @@ -214,7 +214,7 @@ types **What You'll Learn**: - Optional values with `Option` -- Error accumulation with `Either` +- Error accumulation with `Result` - High-performance collections with `Chunk` and `HashSet` - Arbitrary-precision arithmetic with `BigDecimal` - Time-zone-aware dates with `DateTime` @@ -222,8 +222,8 @@ types - Safe concurrent state with `Ref` - Effect results with `Exit` and `Cause` - Sensitive data handling with `Redacted` -- Immutable structures with `Data.struct`, `Data.tuple`, and `Data.array` -- Tagged unions with `Data.case` +- Structural equality for plain objects, tuples, and arrays with `Equal.equals` +- Tagged unions with `Data.taggedEnum` - Type classes with `Data.Class` **Key Benefit**: Practical, robust functional programming in TypeScript @@ -239,7 +239,7 @@ types **What You'll Learn**: - Defining data contracts upfront with `Schema.Struct` -- Validating and parsing unknown data with `Schema.decode` +- Validating and parsing unknown data with `Schema.decodeEffect` - Adding constraints: string length, numeric ranges, custom predicates - Transforming data during validation (string β†’ Date, raw β†’ Branded types) - Handling validation errors gracefully and user-friendly diff --git a/content/new/raw/pattern-option-either-match.mdx b/content/new/raw/pattern-option-either-match.mdx index 04a7989d..b8ab612c 100644 --- a/content/new/raw/pattern-option-either-match.mdx +++ b/content/new/raw/pattern-option-either-match.mdx @@ -1,6 +1,6 @@ --- id: pattern-option-either-match -title: Pattern Match on Option and Either +title: Pattern Match on Option and Result summary: Use declarative match() combinators to handle optional and error-prone values skillLevel: beginner useCase: ["Control Flow"] @@ -8,12 +8,12 @@ tags: [option, either, pattern-matching, match, declarative] related: [pattern-match, data-option, data-either, pattern-option-either-checks] author: Effect Patterns Hub rule: - description: "Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values" + description: "Use Option.match() and Result.match() for declarative pattern matching on optional and error-prone values" --- ## Guideline -When you need to handle `Option` or `Either` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Either) in a single expression. +When you need to handle `Option` or `Result` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Result) in a single expression. Use `.match()` when: - You need to handle both success and failure cases @@ -23,7 +23,7 @@ Use `.match()` when: ## Rationale -The `.match()` combinator is superior to manual checks (`isSome()`, `isLeft()`) because: +The `.match()` combinator is superior to manual checks (`isSome()`, `isFailure()`) because: 1. **Declarative**: Expresses intent clearly - "match on these cases" 2. **Type-safe**: TypeScript ensures all cases are handled @@ -57,23 +57,23 @@ console.log(displayUser(1)); // "Hello, Alice!" console.log(displayUser(999)); // "Guest User" ``` -### Basic Either Matching +### Basic Result Matching ```typescript -import { Either } from "effect"; +import { Result } from "effect"; -const validateAge = (age: number): Either.Either => { +const validateAge = (age: number): Result.Result => { return age >= 18 - ? Either.right(age) - : Either.left("Must be 18 or older"); + ? Result.succeed(age) + : Result.fail("Must be 18 or older"); }; // Using .match() for error handling const processAge = (age: number): string => validateAge(age).pipe( - Either.match({ - onLeft: (error) => `Validation failed: ${error}`, - onRight: (validAge) => `Age ${validAge} is valid`, + Result.match({ + onFailure: (error) => `Validation failed: ${error}`, + onSuccess: (validAge) => `Age ${validAge} is valid`, }) ); @@ -83,10 +83,10 @@ console.log(processAge(15)); // "Validation failed: Must be 18 or older" ### Advanced: Nested Matching -When dealing with nested Option and Either, use nested `.match()` calls: +When dealing with nested Option and Result, use nested `.match()` calls: ```typescript -import { Option, Either } from "effect"; +import { Option, Result } from "effect"; interface UserProfile { name: string; @@ -95,22 +95,22 @@ interface UserProfile { const getUserProfile = ( id: number -): Option.Option> => { +): Option.Option> => { if (id === 0) return Option.none(); // User not found - if (id === 1) return Option.some(Either.left("Profile incomplete")); - return Option.some(Either.right({ name: "Bob", age: 25 })); + if (id === 1) return Option.some(Result.fail("Profile incomplete")); + return Option.some(Result.succeed({ name: "Bob", age: 25 })); }; -// Nested matching - first on Option, then on Either +// Nested matching - first on Option, then on Result const displayProfile = (id: number): string => getUserProfile(id).pipe( Option.match({ onNone: () => "User not found", onSome: (result) => result.pipe( - Either.match({ - onLeft: (error) => `Error: ${error}`, - onRight: (profile) => `${profile.name} (${profile.age})`, + Result.match({ + onFailure: (error) => `Error: ${error}`, + onSuccess: (profile) => `${profile.name} (${profile.age})`, }) ), }) @@ -138,9 +138,9 @@ if (Option.isSome(name)) { // ❌ ANTI-PATTERN: Nested ternaries const ageResult = validateAge(25); const message = ageResult.pipe( - Either.match({ - onLeft: () => "Invalid", - onRight: (age) => age >= 21 ? "Can drink" : "Cannot drink", + Result.match({ + onFailure: () => "Invalid", + onSuccess: (age) => age >= 21 ? "Can drink" : "Cannot drink", }) ); diff --git a/content/published/patterns/building-apis/api-authentication.mdx b/content/published/patterns/building-apis/api-authentication.mdx index 646eaba0..dde4dafd 100644 --- a/content/published/patterns/building-apis/api-authentication.mdx +++ b/content/published/patterns/building-apis/api-authentication.mdx @@ -39,7 +39,7 @@ Authentication protects your API: ```typescript import { Effect, Context, Layer, Data } from "effect" -import { HttpServer, HttpServerRequest, HttpServerResponse } from "@effect/platform" +import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/http" // ============================================ // 1. Define authentication types @@ -51,10 +51,9 @@ interface User { readonly roles: ReadonlyArray } -class AuthenticatedUser extends Context.Tag("AuthenticatedUser")< - AuthenticatedUser, - User ->() {} +class AuthenticatedUser extends Context.Service< + AuthenticatedUser, User +>()("AuthenticatedUser") {} class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{ readonly reason: string @@ -72,7 +71,7 @@ interface JwtService { readonly verify: (token: string) => Effect.Effect } -class Jwt extends Context.Tag("Jwt")() {} +class Jwt extends Context.Service()("Jwt") {} const JwtLive = Layer.succeed(Jwt, { verify: (token) => diff --git a/content/published/patterns/building-apis/api-cors.mdx b/content/published/patterns/building-apis/api-cors.mdx index f9227f69..c7e7f73d 100644 --- a/content/published/patterns/building-apis/api-cors.mdx +++ b/content/published/patterns/building-apis/api-cors.mdx @@ -43,7 +43,7 @@ Browsers block cross-origin requests by default: ```typescript import { Effect } from "effect" -import { HttpServerRequest, HttpServerResponse } from "@effect/platform" +import { HttpServerRequest, HttpServerResponse } from "effect/http" // ============================================ // 1. CORS configuration diff --git a/content/published/patterns/building-apis/api-middleware.mdx b/content/published/patterns/building-apis/api-middleware.mdx index ebd3a054..82597a55 100644 --- a/content/published/patterns/building-apis/api-middleware.mdx +++ b/content/published/patterns/building-apis/api-middleware.mdx @@ -40,7 +40,7 @@ Middleware provides separation of concerns: ```typescript import { Effect, Context, Layer, Duration } from "effect" -import { HttpServerRequest, HttpServerResponse } from "@effect/platform" +import { HttpServerRequest, HttpServerResponse } from "effect/http" // ============================================ // 1. Define middleware type @@ -95,7 +95,7 @@ const withTiming: Middleware = (handler) => const withErrorHandling: Middleware = (handler) => handler.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Unhandled error: ${error}`) @@ -111,7 +111,7 @@ const withErrorHandling: Middleware = (handler) => // 5. Request ID middleware // ============================================ -class RequestId extends Context.Tag("RequestId")() {} +class RequestId extends Context.Service()("RequestId") {} const withRequestId: Middleware = (handler) => Effect.gen(function* () { @@ -128,7 +128,7 @@ const withRequestId: Middleware = (handler) => // 6. Timeout middleware // ============================================ -const withTimeout = (duration: Duration.DurationInput): Middleware => +const withTimeout = (duration: Duration.Input): Middleware => (handler) => handler.pipe( Effect.timeout(duration), diff --git a/content/published/patterns/building-apis/api-openapi.mdx b/content/published/patterns/building-apis/api-openapi.mdx index c8b8125a..447bed0c 100644 --- a/content/published/patterns/building-apis/api-openapi.mdx +++ b/content/published/patterns/building-apis/api-openapi.mdx @@ -43,15 +43,9 @@ OpenAPI documentation provides: ## Good Example ```typescript -import { Effect, Schema } from "effect" -import { - HttpApi, - HttpApiBuilder, - HttpApiEndpoint, - HttpApiGroup, - HttpApiSwagger, - OpenApi, -} from "@effect/platform" +import { Effect, Schema, Layer } from "effect" +import { } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSwagger, OpenApi } from "effect/http" // ============================================ // 1. Define schemas for request/response @@ -59,13 +53,13 @@ import { const UserSchema = Schema.Struct({ id: Schema.String, - email: Schema.String.pipe(Schema.pattern(/@/)), + email: Schema.String.pipe(Schema.check(Schema.isPattern(/@/))), name: Schema.String, createdAt: Schema.DateFromString, }) const CreateUserSchema = Schema.Struct({ - email: Schema.String.pipe(Schema.pattern(/@/)), + email: Schema.String.pipe(Schema.check(Schema.isPattern(/@/))), name: Schema.String, }) diff --git a/content/published/patterns/building-apis/api-rate-limiting.mdx b/content/published/patterns/building-apis/api-rate-limiting.mdx index e73b7ae5..c8db1253 100644 --- a/content/published/patterns/building-apis/api-rate-limiting.mdx +++ b/content/published/patterns/building-apis/api-rate-limiting.mdx @@ -39,7 +39,7 @@ Rate limiting protects your API: ```typescript import { Effect, Context, Layer, Ref, HashMap, Data, Duration } from "effect" -import { HttpServerRequest, HttpServerResponse } from "@effect/platform" +import { HttpServerRequest, HttpServerResponse } from "effect/http" // ============================================ // 1. Define rate limit types @@ -72,10 +72,9 @@ interface RateLimiter { }> } -class RateLimiterService extends Context.Tag("RateLimiter")< - RateLimiterService, - RateLimiter ->() {} +class RateLimiterService extends Context.Service< + RateLimiterService, RateLimiter +>()("RateLimiter") {} // ============================================ // 3. In-memory rate limiter implementation diff --git a/content/published/patterns/building-apis/extract-path-parameters.mdx b/content/published/patterns/building-apis/extract-path-parameters.mdx index 714c3fc8..1ee3bfc0 100644 --- a/content/published/patterns/building-apis/extract-path-parameters.mdx +++ b/content/published/patterns/building-apis/extract-path-parameters.mdx @@ -29,14 +29,14 @@ To capture dynamic parts of a URL, define your route path with a colon-prefixed ## Rationale -APIs often need to operate on specific resources identified by a unique key in the URL, such as `/products/123` or `/orders/abc`. The `Http.router` provides a clean, declarative way to handle these dynamic paths without resorting to manual string parsing. +APIs often need to operate on specific resources identified by a unique key in the URL, such as `/products/123` or `/orders/abc`. The `HttpRouter` provides a clean, declarative way to handle these dynamic paths without resorting to manual string parsing. By defining parameters directly in the path string, you gain several benefits: 1. **Declarative**: The route's structure is immediately obvious from its definition. The code clearly states, "this route expects a dynamic segment here." 2. **Safe and Robust**: The router handles the logic of extracting the parameter. This is less error-prone and more robust than manually splitting or using regular expressions on the URL string. 3. **Clean Handler Logic**: The business logic inside your handler is separated from the concern of URL parsing. The handler simply receives the parameters it needs to do its job. -4. **Composability**: This pattern composes perfectly with the rest of the `Http` module, allowing you to build complex and well-structured APIs. +4. **Composability**: This pattern composes perfectly with the rest of the `effect/http` module, allowing you to build complex and well-structured APIs. --- @@ -45,7 +45,7 @@ By defining parameters directly in the path string, you gain several benefits: This example defines a route that captures a `userId`. The handler for this route accesses the parsed parameters and uses the `userId` to construct a personalized greeting. The router automatically makes the parameters available to the handler. ```typescript -import { Data, Effect } from "effect"; +import { Data, Effect, Context, Layer } from "effect"; // Define tagged error for invalid paths interface InvalidPathErrorSchema { @@ -67,8 +67,8 @@ interface PathOps { } // Create service -class PathService extends Effect.Service()("PathService", { - sync: () => ({ +class PathService extends Context.Service()("PathService", { + make: Effect.sync(() => ({ extractUserId: (path: string) => Effect.gen(function* () { yield* Effect.logInfo( @@ -92,8 +92,10 @@ class PathService extends Effect.Service()("PathService", { yield* Effect.logInfo(greeting); return greeting; }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Compose the functions with proper error handling const processPath = ( @@ -126,7 +128,7 @@ const program = Effect.gen(function* () { yield* Effect.logInfo(result3); }); -Effect.runPromise(Effect.provide(program, PathService.Default)); +Effect.runPromise(Effect.provide(program, PathService.layer)); ``` ## Anti-Pattern @@ -135,28 +137,32 @@ The anti-pattern is to manually parse the URL string inside the handler. This ap ```typescript import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"; +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { createServer } from "node:http"; // This route matches any sub-path of /users/, forcing manual parsing. -const app = Http.router.get( +const app = HttpRouter.route("GET", "/users/*", // Using a wildcard - Http.request.ServerRequest.pipe( + HttpServerRequest.HttpServerRequest.pipe( Effect.flatMap((req) => { // Manually split the URL to find the ID. const parts = req.url.split("/"); // e.g., ['', 'users', '123'] if (parts.length === 3 && parts[2]) { const userId = parts[2]; - return Http.response.text(`Hello, user ${userId}!`); + return HttpServerResponse.text(`Hello, user ${userId}!`); } // Manual handling for missing ID. - return Http.response.empty({ status: 404 }); + return HttpServerResponse.empty({ status: 404 }); }) ) ); -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); +const ServerLive = HttpRouter.serve(app).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +); + +Layer.launch(ServerLive).pipe(NodeRuntime.runMain); NodeRuntime.runMain(program); ``` diff --git a/content/published/patterns/building-apis/handle-api-errors.mdx b/content/published/patterns/building-apis/handle-api-errors.mdx index 954fde84..5ee0bde7 100644 --- a/content/published/patterns/building-apis/handle-api-errors.mdx +++ b/content/published/patterns/building-apis/handle-api-errors.mdx @@ -14,7 +14,7 @@ tags: - data rule: description: >- - Model application errors as typed classes and use Http.server.serveOptions + Model application errors as typed classes and use route-level error handling to map them to specific HTTP responses. author: PaulJPhilp related: @@ -25,7 +25,7 @@ lessonOrder: 4 ## Guideline -Define specific error types for your application logic and use `Http.server.serveOptions` with a custom `unhandledErrorResponse` function to map those errors to appropriate HTTP status codes and responses. +Define specific error types for your application logic and use `route-level error handling` with a custom `unhandledErrorResponse` function to map those errors to appropriate HTTP status codes and responses. --- @@ -47,7 +47,7 @@ Centralizing error handling at the server level provides a clean separation of c This example defines two custom error types, `UserNotFoundError` and `InvalidIdError`. The route logic can fail with either. The `unhandledErrorResponse` function inspects the error and returns a `404` or `400` response accordingly, with a generic `500` for any other unexpected errors. ```typescript -import { Cause, Data, Effect } from "effect"; +import { Cause, Data, Effect, Context, Layer } from "effect"; // Define our domain types export interface User { @@ -73,10 +73,8 @@ export class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{ }> {} // Define error handler service -export class ErrorHandlerService extends Effect.Service()( - "ErrorHandlerService", - { - sync: () => ({ +export class ErrorHandlerService extends Context.Service()("ErrorHandlerService", { + make: Effect.sync(() => ({ // Handle API errors with proper logging handleApiError: (error: E): Effect.Effect => Effect.gen(function* () { @@ -111,8 +109,8 @@ export class ErrorHandlerService extends Effect.Service()( Effect.gen(function* () { yield* Effect.logError("Unexpected error occurred"); - if (Cause.isDie(cause)) { - const defect = Cause.failureOption(cause); + if (Cause.hasDies(cause)) { + const defect = Cause.findErrorOption(cause); if (defect._tag === "Some") { const error = defect.value as Error; yield* Effect.logError(`Defect: ${error.message}`); @@ -124,15 +122,14 @@ export class ErrorHandlerService extends Effect.Service()( return Effect.succeed(void 0); }), - }), - } -) {} + })), + }) { + static readonly layer = Layer.effect(this, this.make) +} // Define UserRepository service -export class UserRepository extends Effect.Service()( - "UserRepository", - { - sync: () => { +export class UserRepository extends Context.Service()("UserRepository", { + make: Effect.sync(() => { const users = new Map([ [ "user_123", @@ -211,9 +208,10 @@ export class UserRepository extends Effect.Service()( return Effect.succeed(void 0); }), }; - }, - } -) {} + }), + }) { + static readonly layer = Layer.effect(this, this.make) +} interface ApiResponse { readonly error?: string; @@ -240,7 +238,7 @@ const createRoutes = () => email: user.role === "admin" ? user.email : "[hidden]", }, })), - Effect.catchAll((error) => errorHandler.handleApiError(error)) + Effect.catch((error) => errorHandler.handleApiError(error)) ); yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`); @@ -254,12 +252,12 @@ const createRoutes = () => yield* repo.checkRole(adminUser, "admin").pipe( Effect.tap(() => Effect.logInfo("Admin access successful")), - Effect.catchAll((error) => errorHandler.handleApiError(error)) + Effect.catch((error) => errorHandler.handleApiError(error)) ); yield* repo.checkRole(regularUser, "admin").pipe( Effect.tap(() => Effect.logInfo("User admin access successful")), - Effect.catchAll((error) => errorHandler.handleApiError(error)) + Effect.catch((error) => errorHandler.handleApiError(error)) ); return { message: "Tests completed successfully" }; @@ -268,8 +266,8 @@ const createRoutes = () => // Run the program with all services Effect.runPromise( Effect.provide( - Effect.provide(createRoutes(), ErrorHandlerService.Default), - UserRepository.Default + Effect.provide(createRoutes(), ErrorHandlerService.layer), + UserRepository.layer ) ); ``` @@ -280,35 +278,39 @@ The anti-pattern is to handle HTTP-specific error logic inside each route handle ```typescript import { Effect, Data } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { HttpRouter, HttpServerResponse } from "effect/http"; +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { createServer } from "node:http"; class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ id: string; }> {} // ... same getUser function and error classes -const userRoute = Http.router.get( +const userRoute = HttpRouter.route("GET", "/users/:userId", - Effect.flatMap(Http.request.ServerRequest, (req) => - getUser(req.params.userId) + Effect.flatMap(HttpRouter.params, (params) => + getUser(params.userId) ).pipe( - Effect.map(Http.response.json), + Effect.map(HttpServerResponse.json), // Manually catching errors inside the route logic Effect.catchTag("UserNotFoundError", (e) => - Http.response.text(`User ${e.id} not found`, { status: 404 }) + HttpServerResponse.text(`User ${e.id} not found`, { status: 404 }) ), Effect.catchTag("InvalidIdError", (e) => - Http.response.text(`ID ${e.id} is not a valid format`, { status: 400 }) + HttpServerResponse.text(`ID ${e.id} is not a valid format`, { status: 400 }) ) ) ); -const app = Http.router.empty.pipe(Http.router.addRoute(userRoute)); +const app = HttpRouter.addAll([userRoute]); // No centralized error handling -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); +const ServerLive = HttpRouter.serve(app).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +); + +Layer.launch(ServerLive).pipe(NodeRuntime.runMain); NodeRuntime.runMain(program); ``` diff --git a/content/published/patterns/building-apis/handle-get-request.mdx b/content/published/patterns/building-apis/handle-get-request.mdx index 19f76a08..7dbddbcc 100644 --- a/content/published/patterns/building-apis/handle-get-request.mdx +++ b/content/published/patterns/building-apis/handle-get-request.mdx @@ -10,7 +10,7 @@ tags: - routing - get rule: - description: Use Http.router.get to associate a URL path with a specific response Effect. + description: Use HttpRouter.route to associate a URL path with a specific response Effect. author: PaulJPhilp related: - launch-http-server @@ -21,13 +21,13 @@ lessonOrder: 3 ## Guideline -To handle specific URL paths, create individual routes using `Http.router` functions (like `Http.router.get`) and combine them into a single `Http.App`. +To handle specific URL paths, create individual routes using `HttpRouter` functions (like `Http.router.get`) and combine them into a single `HttpEffect`. --- ## Rationale -A real application needs to respond differently to different URLs. The `Http.router` provides a declarative, type-safe, and composable way to manage this routing logic. Instead of a single handler with complex conditional logic, you define many small, focused handlers and assign them to specific paths and HTTP methods. +A real application needs to respond differently to different URLs. The `HttpRouter` provides a declarative, type-safe, and composable way to manage this routing logic. Instead of a single handler with complex conditional logic, you define many small, focused handlers and assign them to specific paths and HTTP methods. This approach has several advantages: @@ -43,7 +43,7 @@ This approach has several advantages: This example defines two separate GET routes, one for the root path (`/`) and one for `/hello`. We create an empty router and add each route to it. The resulting `app` is then served. The router automatically handles sending a `404 Not Found` response for any path that doesn't match. ```typescript -import { Data, Effect } from "effect"; +import { Data, Effect, Context, Layer } from "effect"; // Define response types interface RouteResponse { @@ -62,8 +62,8 @@ class RouteHandlerError extends Data.TaggedError("RouteHandlerError")<{ }> {} // Define route service -class RouteService extends Effect.Service()("RouteService", { - sync: () => { +class RouteService extends Context.Service()("RouteService", { + make: Effect.sync(() => { // Create instance methods const handleRoute = ( path: string @@ -108,8 +108,10 @@ class RouteService extends Effect.Service()("RouteService", { return response; }), }; - }, -}) {} + }), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create program with proper error handling const program = Effect.gen(function* () { @@ -148,7 +150,7 @@ const program = Effect.gen(function* () { }); // Run the program -Effect.runPromise(Effect.provide(program, RouteService.Default)); +Effect.runPromise(Effect.provide(program, RouteService.layer)); ``` ## Anti-Pattern @@ -157,26 +159,30 @@ The anti-pattern is to create a single, monolithic handler that uses conditional ```typescript import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"; +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { createServer } from "node:http"; // A single app that manually checks the URL -const app = Http.request.ServerRequest.pipe( +const app = HttpServerRequest.HttpServerRequest.pipe( Effect.flatMap((req) => { if (req.url === "/") { - return Effect.succeed(Http.response.text("Welcome to the home page!")); + return Effect.succeed(HttpServerResponse.text("Welcome to the home page!")); } else if (req.url === "/hello") { - return Effect.succeed(Http.response.text("Hello, Effect!")); + return Effect.succeed(HttpServerResponse.text("Hello, Effect!")); } else { - return Effect.succeed(Http.response.empty({ status: 404 })); + return Effect.succeed(HttpServerResponse.empty({ status: 404 })); } }) ); -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); +const ServerLive = HttpRouter.serve(app).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +); + +Layer.launch(ServerLive).pipe(NodeRuntime.runMain); NodeRuntime.runMain(program); ``` -This manual routing logic is verbose, error-prone (a typo in a string breaks the route), and mixes the "what" (the response) with the "where" (the routing). It doesn't scale to handle different HTTP methods, path parameters, or middleware gracefully. The `Http.router` is designed to solve all of these problems elegantly. +This manual routing logic is verbose, error-prone (a typo in a string breaks the route), and mixes the "what" (the response) with the "where" (the routing). It doesn't scale to handle different HTTP methods, path parameters, or middleware gracefully. The `HttpRouter` is designed to solve all of these problems elegantly. diff --git a/content/published/patterns/building-apis/launch-http-server.mdx b/content/published/patterns/building-apis/launch-http-server.mdx index 106eb6ad..57d0ce52 100644 --- a/content/published/patterns/building-apis/launch-http-server.mdx +++ b/content/published/patterns/building-apis/launch-http-server.mdx @@ -11,7 +11,7 @@ tags: - node rule: description: >- - Use Http.server.serve with a platform-specific layer to run an HTTP + Use HttpRouter.serve with a platform-specific layer to run an HTTP application. author: PaulJPhilp related: @@ -22,7 +22,7 @@ lessonOrder: 1 ## Guideline -To create and run a web server, define your application as an `Http.App` and execute it using `Http.server.serve`, providing a platform-specific layer like `NodeHttpServer.layer`. +To create and run a web server, define your application as an `HttpEffect` and execute it using `HttpRouter.serve`, providing a platform-specific layer like `NodeHttpServer.layer`. --- @@ -30,27 +30,27 @@ To create and run a web server, define your application as an `Http.App` and exe In Effect, an HTTP server is not just a side effect; it's a managed, effectful process. The `@effect/platform` package provides a platform-agnostic API for defining HTTP applications, while packages like `@effect/platform-node` provide the concrete implementation. -The core function `Http.server.serve(app)` takes your application logic and returns an `Effect` that, when run, starts the server. This `Effect` is designed to run indefinitely, only terminating if the server crashes or is gracefully shut down. +The core function `HttpRouter.serve(routes)` takes your route layers and returns a `Layer` that, when launched, starts the server. This `Effect` is designed to run indefinitely, only terminating if the server crashes or is gracefully shut down. This approach provides several key benefits: 1. **Lifecycle Management**: The server's lifecycle is managed by the Effect runtime. This means structured concurrency applies, ensuring graceful shutdowns and proper resource handling automatically. 2. **Integration**: The server is a first-class citizen in the Effect ecosystem. It can seamlessly access dependencies provided by `Layer`, use `Config` for configuration, and integrate with `Logger`. -3. **Platform Agnosticism**: By coding to the `Http.App` interface, your application logic remains portable across different JavaScript runtimes (Node.js, Bun, Deno) by simply swapping out the platform layer. +3. **Platform Agnosticism**: By coding to the `HttpEffect` interface, your application logic remains portable across different JavaScript runtimes (Node.js, Bun, Deno) by simply swapping out the platform layer. --- ## Good Example -This example creates a minimal server that responds to all requests with "Hello, World!". The application logic is a simple `Effect` that returns an `Http.response`. We use `NodeRuntime.runMain` to execute the server effect, which is the standard way to launch a long-running application. +This example creates a minimal server that responds to all requests with "Hello, World!". The application logic is a simple `Effect` that returns an `HttpServerResponse`. We use `NodeRuntime.runMain` to execute the server effect, which is the standard way to launch a long-running application. ```typescript -import { Effect, Duration } from "effect"; +import { Effect, Duration, Context, Layer } from "effect"; import * as http from "http"; // Create HTTP server service -class HttpServer extends Effect.Service()("HttpServer", { - sync: () => ({ +class HttpServer extends Context.Service()("HttpServer", { + make: Effect.sync(() => ({ start: () => Effect.gen(function* () { const server = http.createServer( @@ -69,14 +69,14 @@ class HttpServer extends Effect.Service()("HttpServer", { ); // Start server with timeout - yield* Effect.async((resume) => { + yield* Effect.callback((resume) => { server.on("error", (error) => resume(Effect.fail(error))); server.listen(3456, "localhost", () => { resume(Effect.succeed(void 0)); }); }).pipe( Effect.timeout(Duration.seconds(5)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Failed to start server: ${error}`); return yield* Effect.fail(error); @@ -90,8 +90,10 @@ class HttpServer extends Effect.Service()("HttpServer", { yield* Effect.sleep(Duration.seconds(3)); yield* Effect.logInfo("Server demonstration complete"); }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create program with proper error handling const program = Effect.gen(function* () { @@ -107,9 +109,9 @@ const program = Effect.gen(function* () { // Run the server with proper error handling const programWithErrorHandling = Effect.provide( program, - HttpServer.Default + HttpServer.layer ).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Program failed: ${error}`); return yield* Effect.fail(error); @@ -136,6 +138,7 @@ The common anti-pattern is to use the raw Node.js `http` module directly, outsid ```typescript import * as http from "http"; +import { createServer } from "node:http"; // Manually create a server using the Node.js built-in module. const server = http.createServer((req, res) => { diff --git a/content/published/patterns/building-apis/make-http-client-request.mdx b/content/published/patterns/building-apis/make-http-client-request.mdx index bd7dc14b..88f1bbbe 100644 --- a/content/published/patterns/building-apis/make-http-client-request.mdx +++ b/content/published/patterns/building-apis/make-http-client-request.mdx @@ -14,7 +14,7 @@ tags: - request rule: description: >- - Use the Http.client module to make outgoing requests to keep the entire + Use the HttpClient module to make outgoing requests to keep the entire operation within the Effect ecosystem. author: PaulJPhilp related: @@ -25,18 +25,18 @@ lessonOrder: 6 ## Guideline -To call an external API from within your server, use the `Http.client` module. This creates an `Effect` that represents the outgoing request, keeping it fully integrated with the Effect runtime. +To call an external API from within your server, use the `HttpClient` module. This creates an `Effect` that represents the outgoing request, keeping it fully integrated with the Effect runtime. --- ## Rationale -An API server often needs to communicate with other services. While you could use the native `fetch` API, this breaks out of the Effect ecosystem and forfeits its most powerful features. Using the built-in `Http.client` is superior for several critical reasons: +An API server often needs to communicate with other services. While you could use the native `fetch` API, this breaks out of the Effect ecosystem and forfeits its most powerful features. Using the built-in `HttpClient` is superior for several critical reasons: -1. **Full Integration**: An `Http.client` request is a first-class `Effect`. This means it seamlessly composes with all other effects. You can add timeouts, retry logic (`Schedule`), or race it with other operations using the standard Effect operators you already know. -2. **Structured Concurrency**: This is a key benefit. If the original incoming request to your server is cancelled or times out, Effect will automatically interrupt the outgoing `Http.client` request. A raw `fetch` call would continue running in the background, wasting resources. +1. **Full Integration**: An `HttpClient` request is a first-class `Effect`. This means it seamlessly composes with all other effects. You can add timeouts, retry logic (`Schedule`), or race it with other operations using the standard Effect operators you already know. +2. **Structured Concurrency**: This is a key benefit. If the original incoming request to your server is cancelled or times out, Effect will automatically interrupt the outgoing `HttpClient` request. A raw `fetch` call would continue running in the background, wasting resources. 3. **Typed Errors**: The client provides a rich set of typed errors (e.g., `Http.error.RequestError`, `Http.error.ResponseError`). This allows you to write precise error handling logic to distinguish between a network failure and a non-2xx response from the external API. -4. **Testability**: The `Http.client` can be provided via a `Layer`, making it trivial to mock in tests. You can test your route's logic without making actual network calls, leading to faster and more reliable tests. +4. **Testability**: The `HttpClient` can be provided via a `Layer`, making it trivial to mock in tests. You can test your route's logic without making actual network calls, leading to faster and more reliable tests. --- @@ -46,23 +46,25 @@ This example creates a proxy endpoint. A request to `/proxy/posts/1` on our serv ```typescript import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; -import * as HttpRouter from "@effect/platform/HttpRouter"; -import * as HttpServer from "@effect/platform/HttpServer"; -import * as HttpResponse from "@effect/platform/HttpServerResponse"; -import { Console, Data, Duration, Effect, Fiber, Layer } from "effect"; +import * as HttpRouter from "effect/http/HttpRouter"; +import * as HttpServer from "effect/http/HttpServer"; +import * as HttpResponse from "effect/http/HttpServerResponse"; +import { Console, Data, Duration, Effect, Fiber, Layer, Context } from "effect"; class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ id: string; }> {} -export class Database extends Effect.Service()("Database", { - sync: () => ({ +export class Database extends Context.Service()("Database", { + make: Effect.sync(() => ({ getUser: (id: string) => id === "123" ? Effect.succeed({ name: "Paul" }) : Effect.fail(new UserNotFoundError({ id })), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} const userHandler = Effect.flatMap(HttpRouter.params, (p) => Effect.flatMap(Database, (db) => db.getUser(p["userId"] ?? "")).pipe( @@ -80,14 +82,14 @@ const server = NodeHttpServer.layer(() => require("node:http").createServer(), { const serverLayer = HttpServer.serve(app); -const mainLayer = Layer.merge(Database.Default, server); +const mainLayer = Layer.merge(Database.layer, server); const program = Effect.gen(function* () { yield* Effect.log("Server started on http://localhost:3457"); const layer = Layer.provide(serverLayer, mainLayer); // Launch server and run for a short duration to demonstrate - const serverFiber = yield* Layer.launch(layer).pipe(Effect.fork); + const serverFiber = yield* Layer.launch(layer).pipe(Effect.forkChild); // Wait a moment for server to start yield* Effect.sleep(Duration.seconds(1)); @@ -104,7 +106,7 @@ const program = Effect.gen(function* () { NodeRuntime.runMain( Effect.provide( program, - Layer.provide(serverLayer, Layer.merge(Database.Default, server)) + Layer.provide(serverLayer, Layer.merge(Database.layer, server)) ) as Effect.Effect ); ``` @@ -115,15 +117,17 @@ The anti-pattern is to use `fetch` inside a route handler, wrapped in `Effect.tr ```typescript import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { HttpRouter, HttpServerResponse } from "effect/http"; +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { createServer } from "node:http"; -const proxyRoute = Http.router.get( +const proxyRoute = HttpRouter.route("GET", "/proxy/posts/:id", - Effect.flatMap(Http.request.ServerRequest, (req) => + Effect.flatMap(HttpRouter.params, (params) => // Manually wrap fetch in an Effect Effect.tryPromise({ try: () => - fetch(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`), + fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`), catch: () => "FetchError", // Untyped error }).pipe( Effect.flatMap((res) => @@ -135,20 +139,22 @@ const proxyRoute = Http.router.get( }) : Effect.fail("BadStatusError") ), - Effect.map(Http.response.json), + Effect.map(HttpServerResponse.json), // A generic catch-all because we can't easily distinguish error types - Effect.catchAll(() => - Http.response.text("An unknown error occurred", { status: 500 }) + Effect.catch(() => + HttpServerResponse.text("An unknown error occurred", { status: 500 }) ) ) ) ); -const app = Http.router.empty.pipe(Http.router.addRoute(proxyRoute)); +const app = HttpRouter.addAll([proxyRoute]); + +const ServerLive = HttpRouter.serve(app).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +); -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); +Layer.launch(ServerLive).pipe(NodeRuntime.runMain); NodeRuntime.runMain(program); ``` diff --git a/content/published/patterns/building-apis/provide-dependencies-to-routes.mdx b/content/published/patterns/building-apis/provide-dependencies-to-routes.mdx index 8b68d77b..06dc243c 100644 --- a/content/published/patterns/building-apis/provide-dependencies-to-routes.mdx +++ b/content/published/patterns/building-apis/provide-dependencies-to-routes.mdx @@ -5,7 +5,7 @@ skillLevel: intermediate applicationPatternId: building-apis summary: >- Inject services like database connections into HTTP route handlers using Layer - and Effect.Service. + and Context.Service. tags: - http - server @@ -15,7 +15,7 @@ tags: - context rule: description: >- - Define dependencies with Effect.Service and provide them to your HTTP server + Define dependencies with Context.Service and provide them to your HTTP server using a Layer. author: PaulJPhilp related: @@ -27,7 +27,7 @@ lessonOrder: 7 ## Guideline -Define your application's services using `class MyService extends Effect.Service("MyService")`, provide a live implementation via a `Layer`, and use `Effect.provide` to make the service available to your entire HTTP application. +Define your application's services using `class MyService extends Context.Service("MyService")`, provide a live implementation via a `Layer`, and use `Effect.provide` to make the service available to your entire HTTP application. --- @@ -37,7 +37,7 @@ As applications grow, route handlers need to perform complex tasks like accessin Effect's dependency injection system (`Service` and `Layer`) solves this by decoupling a service's interface from its implementation. This is the cornerstone of building scalable, maintainable applications in Effect. -1. **Modern and Simple**: `Effect.Service` is the modern, idiomatic way to define services. It combines the service's definition and its access tag into a single, clean class structure, reducing boilerplate. +1. **Modern and Simple**: `Context.Service` is the modern, idiomatic way to define services. It combines the service's definition and its access tag into a single, clean class structure, reducing boilerplate. 2. **Testability**: By depending on a service interface, you can easily provide a mock implementation in your tests (e.g., `Database.Test`) instead of the real one (`Database.Live`), allowing for fast, isolated unit tests of your route logic. 3. **Decoupling**: Route handlers don't know or care _how_ the database connection is created or managed. They simply ask for the `Database` service from the context, and the runtime provides the configured implementation. 4. **Composability**: `Layer`s are composable. You can build complex dependency graphs (e.g., a `Database` layer that itself requires a `Config` layer) that Effect will automatically construct and wire up for you. @@ -49,22 +49,24 @@ Effect's dependency injection system (`Service` and `Layer`) solves this by deco This example defines a `Database` service. The route handler for `/users/:userId` requires this service to fetch a user. We then provide a "live" implementation of the `Database` to the entire server using a `Layer`. ```typescript -import * as HttpRouter from "@effect/platform/HttpRouter"; -import * as HttpResponse from "@effect/platform/HttpServerResponse"; -import * as HttpServer from "@effect/platform/HttpServer"; +import * as HttpRouter from "effect/http/HttpRouter"; +import * as HttpResponse from "effect/http/HttpServerResponse"; +import * as HttpServer from "effect/http/HttpServer"; import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; -import { Effect, Duration, Fiber } from "effect/index"; -import { Data } from "effect"; +import { Effect, Duration, Fiber } from "effect"; +import { Data, Context, Layer, Fiber } from "effect"; -// 1. Define the service interface using Effect.Service -export class Database extends Effect.Service()("Database", { - sync: () => ({ +// 1. Define the service interface using Context.Service +export class Database extends Context.Service()("Database", { + make: Effect.sync(() => ({ getUser: (id: string) => id === "123" ? Effect.succeed({ name: "Paul" }) : Effect.fail(new UserNotFoundError({ id })), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ id: string; @@ -84,7 +86,7 @@ const app = HttpRouter.empty.pipe( // Create the server effect with all dependencies const serverEffect = HttpServer.serveEffect(app).pipe( - Effect.provide(Database.Default), + Effect.provide(Database.layer), Effect.provide( NodeHttpServer.layer(() => require("node:http").createServer(), { port: 3458, @@ -96,7 +98,7 @@ const serverEffect = HttpServer.serveEffect(app).pipe( const program = Effect.gen(function* () { yield* Effect.logInfo("Starting server on port 3458..."); - const serverFiber = yield* Effect.scoped(serverEffect).pipe(Effect.fork); + const serverFiber = yield* Effect.scoped(serverEffect).pipe(Effect.forkChild); yield* Effect.logInfo("Server started successfully on http://localhost:3458"); yield* Effect.logInfo("Try: curl http://localhost:3458/users/123"); @@ -120,7 +122,9 @@ The anti-pattern is to manually instantiate and pass dependencies through functi ```typescript import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { HttpRouter, HttpServerResponse } from "effect/http"; +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { createServer } from "node:http"; // Manual implementation of a database client class LiveDatabase { @@ -134,13 +138,13 @@ class LiveDatabase { // The dependency must be passed explicitly to the route definition const createGetUserRoute = (db: LiveDatabase) => - Http.router.get( + HttpRouter.route("GET", "/users/:userId", - Effect.flatMap(Http.request.ServerRequest, (req) => - db.getUser(req.params.userId) + Effect.flatMap(HttpRouter.params, (params) => + db.getUser(params.userId) ).pipe( - Effect.map(Http.response.json), - Effect.catchAll(() => Http.response.empty({ status: 404 })) + Effect.map(HttpServerResponse.json), + Effect.catch(() => HttpServerResponse.empty({ status: 404 })) ) ); @@ -148,11 +152,13 @@ const createGetUserRoute = (db: LiveDatabase) => const db = new LiveDatabase(); const getUserRoute = createGetUserRoute(db); -const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute)); +const app = HttpRouter.addAll([getUserRoute]); + +const ServerLive = HttpRouter.serve(app).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +); -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); +Layer.launch(ServerLive).pipe(NodeRuntime.runMain); NodeRuntime.runMain(program); ``` diff --git a/content/published/patterns/building-apis/send-json-response.mdx b/content/published/patterns/building-apis/send-json-response.mdx index 6eab52e7..d0b0ac87 100644 --- a/content/published/patterns/building-apis/send-json-response.mdx +++ b/content/published/patterns/building-apis/send-json-response.mdx @@ -14,7 +14,7 @@ tags: - api rule: description: >- - Use Http.response.json to automatically serialize data structures into a + Use HttpServerResponse.json to automatically serialize data structures into a JSON response. author: PaulJPhilp related: @@ -25,37 +25,37 @@ lessonOrder: 4 ## Guideline -To return a JavaScript object or value as a JSON response, use the `Http.response.json(data)` constructor. +To return a JavaScript object or value as a JSON response, use the `HttpServerResponse.json(data)` constructor. --- ## Rationale -APIs predominantly communicate using JSON. The `Http` module provides a dedicated `Http.response.json` helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone. +APIs predominantly communicate using JSON. The `Http` module provides a dedicated `HttpServerResponse.json` helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone. -Using `Http.response.json` is superior because: +Using `HttpServerResponse.json` is superior because: 1. **Automatic Serialization**: It safely handles the `JSON.stringify` operation for you, including handling potential circular references or other serialization errors. 2. **Correct Headers**: It automatically sets the `Content-Type: application/json; charset=utf-8` header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs. 3. **Simplicity and Readability**: Your intent is made clear with a single, declarative function call. The code is cleaner and focuses on the data being sent, not the mechanics of HTTP. -4. **Composability**: It creates a standard `Http.response` object that works seamlessly with all other parts of the Effect `Http` module. +4. **Composability**: It creates a standard `HttpServerResponse` object that works seamlessly with all other parts of the Effect `effect/http` module. --- ## Good Example -This example defines a route that fetches a user object and returns it as a JSON response. The `Http.response.json` function handles all the necessary serialization and header configuration. +This example defines a route that fetches a user object and returns it as a JSON response. The `HttpServerResponse.json` function handles all the necessary serialization and header configuration. ```typescript import { Effect, Context, Duration, Layer } from "effect"; -import { NodeContext, NodeHttpServer } from "@effect/platform-node"; +import { NodeServices, NodeHttpServer } from "@effect/platform-node"; import { createServer } from "node:http"; const PORT = 3459; // Changed port to avoid conflicts // Define HTTP Server service -class JsonServer extends Effect.Service()("JsonServer", { - sync: () => ({ +class JsonServer extends Context.Service()("JsonServer", { + make: Effect.sync(() => ({ handleRequest: () => Effect.succeed({ status: 200, @@ -65,8 +65,10 @@ class JsonServer extends Effect.Service()("JsonServer", { timestamp: new Date().toISOString(), }), }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create and run the server const program = Effect.gen(function* () { @@ -92,7 +94,7 @@ const program = Effect.gen(function* () { }); // Start server with error handling - yield* Effect.async((resume) => { + yield* Effect.callback((resume) => { server.on("error", (error: NodeJS.ErrnoException) => { if (error.code === "EADDRINUSE") { resume(Effect.fail(new Error(`Port ${PORT} is already in use`))); @@ -116,14 +118,14 @@ const program = Effect.gen(function* () { yield* Effect.sync(() => server.close()); yield* Effect.logInfo("Server shutdown complete"); }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Server error: ${error.message}`); return error; }) ), // Merge layers and provide them in a single call to ensure proper lifecycle management - Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer)) + Effect.provide(Layer.merge(JsonServer.layer, NodeServices.layer)) ); // Run the program @@ -142,19 +144,21 @@ The anti-pattern is to manually serialize the data to a string and set the heade ```typescript import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"; +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { createServer } from "node:http"; -const getUserRoute = Http.router.get( +const getUserRoute = HttpRouter.route("GET", "/users/1", Effect.succeed({ id: 1, name: "Paul", team: "Effect" }).pipe( Effect.flatMap((user) => { // Manually serialize the object to a JSON string. const jsonString = JSON.stringify(user); // Create a text response with the string. - const response = Http.response.text(jsonString); + const response = HttpServerResponse.text(jsonString); // Manually set the Content-Type header. return Effect.succeed( - Http.response.setHeader( + HttpServerResponse.setHeader( response, "Content-Type", "application/json; charset=utf-8" @@ -164,13 +168,15 @@ const getUserRoute = Http.router.get( ) ); -const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute)); +const app = HttpRouter.addAll([getUserRoute]); + +const ServerLive = HttpRouter.serve(app).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +); -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); +Layer.launch(ServerLive).pipe(NodeRuntime.runMain); NodeRuntime.runMain(program); ``` -This manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the `setHeader` call, many clients will fail to parse the response correctly. The `Http.response.json` helper eliminates this entire class of potential bugs. +This manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the `setHeader` call, many clients will fail to parse the response correctly. The `HttpServerResponse.json` helper eliminates this entire class of potential bugs. diff --git a/content/published/patterns/building-apis/validate-request-body.mdx b/content/published/patterns/building-apis/validate-request-body.mdx index 88037009..85e763b7 100644 --- a/content/published/patterns/building-apis/validate-request-body.mdx +++ b/content/published/patterns/building-apis/validate-request-body.mdx @@ -16,7 +16,7 @@ tags: - body rule: description: >- - Use Http.request.schemaBodyJson with a Schema to automatically parse and + Use HttpServerRequest.schemaBodyJson with a Schema to automatically parse and validate request bodies. author: PaulJPhilp related: @@ -29,7 +29,7 @@ lessonOrder: 8 ## Guideline -To process an incoming request body, use `Http.request.schemaBodyJson(YourSchema)` to parse the JSON and validate its structure in a single, type-safe step. +To process an incoming request body, use `HttpServerRequest.schemaBodyJson(YourSchema)` to parse the JSON and validate its structure in a single, type-safe step. --- @@ -37,7 +37,7 @@ To process an incoming request body, use `Http.request.schemaBodyJson(YourSchema Accepting user-provided data is one of the most critical and sensitive parts of an API. You must never trust incoming data. The `Http` module's integration with `Schema` provides a robust, declarative solution for this. -Using `Http.request.schemaBodyJson` offers several major advantages: +Using `HttpServerRequest.schemaBodyJson` offers several major advantages: 1. **Automatic Validation and Error Handling**: If the incoming body does not match the schema, the server automatically rejects the request with a `400 Bad Request` status and a detailed JSON response explaining the validation errors. You don't have to write any of this boilerplate logic. 2. **Type Safety**: If the validation succeeds, the value produced by the `Effect` is fully typed according to your `Schema`. This eliminates `any` types and brings static analysis benefits to your request handlers. @@ -51,14 +51,14 @@ Using `Http.request.schemaBodyJson` offers several major advantages: This example defines a `POST` route to create a user. It uses a `CreateUser` schema to validate the request body. If validation passes, it returns a success message with the typed data. If it fails, the platform automatically sends a descriptive 400 error. ```typescript -import { Duration, Effect } from "effect"; +import { Duration, Effect, Context, Layer, Schema } from "effect"; import * as S from "effect/Schema"; import { createServer, IncomingMessage, ServerResponse } from "http"; // Define user schema const UserSchema = S.Struct({ name: S.String, - email: S.String.pipe(S.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), + email: S.String.pipe(S.check(S.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))), }); type User = S.Schema.Type; @@ -68,11 +68,13 @@ interface UserServiceInterface { } // Define user service -class UserService extends Effect.Service()("UserService", { - sync: () => ({ - validateUser: (data: unknown) => S.decodeUnknown(UserSchema)(data), - }), -}) {} +class UserService extends Context.Service()("UserService", { + make: Effect.sync(() => ({ + validateUser: (data: unknown) => S.decodeUnknownEffect(UserSchema)(data), + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Define HTTP server service interface interface HttpServerInterface { @@ -84,9 +86,9 @@ interface HttpServerInterface { } // Define HTTP server service -class HttpServer extends Effect.Service()("HttpServer", { +class HttpServer extends Context.Service()("HttpServer", { // Define effect-based implementation that uses dependencies - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const userService = yield* UserService; return { @@ -101,7 +103,7 @@ class HttpServer extends Effect.Service()("HttpServer", { try { // Read request body - const body = yield* Effect.async((resume) => { + const body = yield* Effect.callback((resume) => { let data = ""; request.on("data", (chunk) => { data += chunk; @@ -154,7 +156,7 @@ class HttpServer extends Effect.Service()("HttpServer", { ); // Start server - yield* Effect.async((resume) => { + yield* Effect.callback((resume) => { server.on("error", (error) => resume(Effect.fail(error))); server.listen(3456, () => { Effect.runFork( @@ -172,8 +174,11 @@ class HttpServer extends Effect.Service()("HttpServer", { }; }), // Specify dependencies - dependencies: [UserService.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(UserService.layer) + ) +} // Create program with proper error handling const program = Effect.gen(function* () { @@ -182,7 +187,7 @@ const program = Effect.gen(function* () { yield* Effect.logInfo("Starting HTTP server..."); yield* server.start().pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Server error: ${error}`); return yield* Effect.fail(error); @@ -194,7 +199,7 @@ const program = Effect.gen(function* () { ); // Run the server -Effect.runFork(Effect.provide(program, HttpServer.Default)); +Effect.runFork(Effect.provide(program, HttpServer.layer)); /* To test: @@ -212,12 +217,14 @@ The anti-pattern is to manually parse the JSON and then write imperative validat ```typescript import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"; +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; +import { createServer } from "node:http"; -const createUserRoute = Http.router.post( +const createUserRoute = HttpRouter.route("POST", "/users", - Http.request.json.pipe( - // Http.request.json returns Effect + HttpServerRequest.HttpServerRequest.pipe(Effect.flatMap((request) => request.json)).pipe( + // HttpServerRequest.HttpServerRequest.pipe(Effect.flatMap((request) => request.json)) returns Effect Effect.flatMap((body) => { // Manually check the type and properties of the body. if ( @@ -229,20 +236,22 @@ const createUserRoute = Http.router.post( typeof body.email === "string" ) { // The type is still not safely inferred here without casting. - return Http.response.text(`Successfully created user: ${body.name}`); + return HttpServerResponse.text(`Successfully created user: ${body.name}`); } else { // Manually create and return a generic error response. - return Http.response.text("Invalid request body", { status: 400 }); + return HttpServerResponse.text("Invalid request body", { status: 400 }); } }) ) ); -const app = Http.router.empty.pipe(Http.router.addRoute(createUserRoute)); +const app = HttpRouter.addAll([createUserRoute]); + +const ServerLive = HttpRouter.serve(app).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +); -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); +Layer.launch(ServerLive).pipe(NodeRuntime.runMain); NodeRuntime.runMain(program); ``` diff --git a/content/published/patterns/building-data-pipelines/pipeline-backpressure.mdx b/content/published/patterns/building-data-pipelines/pipeline-backpressure.mdx index b5bb1e52..b6a07cb3 100644 --- a/content/published/patterns/building-data-pipelines/pipeline-backpressure.mdx +++ b/content/published/patterns/building-data-pipelines/pipeline-backpressure.mdx @@ -124,7 +124,7 @@ const boundedQueueExample = Effect.gen(function* () { count++ } return count - }).pipe(Effect.catchAll(() => Effect.succeed(0))) + }).pipe(Effect.catch(() => Effect.succeed(0))) // Run both - producer will block when queue is full yield* Effect.all([producer, consumer], { concurrency: 2 }) diff --git a/content/published/patterns/building-data-pipelines/pipeline-dead-letter-queue.mdx b/content/published/patterns/building-data-pipelines/pipeline-dead-letter-queue.mdx index 69510e4c..6fa21936 100644 --- a/content/published/patterns/building-data-pipelines/pipeline-dead-letter-queue.mdx +++ b/content/published/patterns/building-data-pipelines/pipeline-dead-letter-queue.mdx @@ -152,7 +152,7 @@ const processWithRetryAndDLQ = ( for (let attempt = 1; attempt <= maxRetries; attempt++) { const result = yield* processOrder(order).pipe( Effect.map((r) => new Success(order, r)), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`Attempt ${attempt}/${maxRetries} failed for ${order.id}`) lastError = error @@ -210,7 +210,7 @@ const reprocessDLQ = ( for (const dlqItem of dlqItems) { const result = yield* processor(dlqItem.item).pipe( Effect.map(() => "success" as const), - Effect.catchAll(() => Effect.succeed("failed" as const)) + Effect.catch(() => Effect.succeed("failed" as const)) ) yield* Effect.log( diff --git a/content/published/patterns/building-data-pipelines/stream-from-file.mdx b/content/published/patterns/building-data-pipelines/stream-from-file.mdx index 8c41db8f..6b3486a7 100644 --- a/content/published/patterns/building-data-pipelines/stream-from-file.mdx +++ b/content/published/patterns/building-data-pipelines/stream-from-file.mdx @@ -46,9 +46,9 @@ The `Stream.fromReadable` constructor provides a bridge from Node.js's built-in This example demonstrates reading a text file, splitting it into individual lines, and processing each line. The combination of `Stream.fromReadable`, `Stream.decodeText`, and `Stream.splitLines` is a powerful and common pattern for handling text-based files. ```typescript -import { FileSystem } from "@effect/platform"; +import { FileSystem } from "effect"; import { NodeFileSystem } from "@effect/platform-node"; -import type { PlatformError } from "@effect/platform/Error"; +import type { PlatformError } from "effect/PlatformError"; import { Effect, Stream } from "effect"; import * as path from "node:path"; @@ -83,7 +83,7 @@ const program = Effect.gen(function* () { const filePath = path.join(__dirname, "large-file.txt"); yield* processFile(filePath, "line 1\nline 2\nline 3").pipe( - Effect.catchAll((error: PlatformError) => + Effect.catch((error: PlatformError) => Effect.logError(`Error processing file: ${error.message}`) ) ); diff --git a/content/published/patterns/building-data-pipelines/stream-manage-resources.mdx b/content/published/patterns/building-data-pipelines/stream-manage-resources.mdx index 05ece467..883c9ff5 100644 --- a/content/published/patterns/building-data-pipelines/stream-manage-resources.mdx +++ b/content/published/patterns/building-data-pipelines/stream-manage-resources.mdx @@ -49,8 +49,8 @@ What happens if a pipeline processing a file fails halfway through? In a naive i This example creates and writes to a temporary file. `Stream.acquireRelease` is used to acquire a readable stream from that file. The pipeline then processes the file but is designed to fail partway through. The logs demonstrate that the `release` effect (which deletes the file) is still executed, preventing any resource leaks. ```typescript -import { Effect, Layer } from "effect"; -import { FileSystem } from "@effect/platform/FileSystem"; +import { Effect, Layer, Context } from "effect"; +import { FileSystem } from "effect/FileSystem"; import { NodeFileSystem } from "@effect/platform-node"; import * as path from "node:path"; @@ -70,8 +70,8 @@ interface FileServiceType { readonly readFile: (filePath: string) => Effect.Effect; } -export class FileService extends Effect.Service()("FileService", { - sync: () => { +export class FileService extends Context.Service()("FileService", { + make: Effect.sync(() => { const filePath = path.join(__dirname, "temp-resource.txt"); return { createTempFile: () => Effect.succeed({ filePath }), @@ -80,8 +80,10 @@ export class FileService extends Effect.Service()("FileService", { readFile: (filePath: string) => Effect.succeed("data 1\ndata 2\nFAIL\ndata 4"), }; - }, -}) {} + }), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Process a single line const processLine = (line: string): Effect.Effect => @@ -110,7 +112,7 @@ const program = Effect.gen(function* () { // Process each line, continuing even if some fail for (const line of lines) { yield* processLine(line).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.log(`⚠️ Skipped line due to error: ${error.message}`) ) ); @@ -124,7 +126,7 @@ const program = Effect.gen(function* () { }); // Run the program with FileService layer -Effect.runPromise(Effect.provide(program, FileService.Default)).catch( +Effect.runPromise(Effect.provide(program, FileService.layer)).catch( (error) => { Effect.runSync(Effect.logError("Unexpected error: " + error)); } diff --git a/content/published/patterns/building-data-pipelines/stream-process-concurrently.mdx b/content/published/patterns/building-data-pipelines/stream-process-concurrently.mdx index eebcabf6..4b10395d 100644 --- a/content/published/patterns/building-data-pipelines/stream-process-concurrently.mdx +++ b/content/published/patterns/building-data-pipelines/stream-process-concurrently.mdx @@ -75,7 +75,7 @@ const programWithLogging = Effect.gen(function* () { yield* Effect.log(`\nTotal time: ${Math.round(durationMs / 1000)} seconds`); return duration; }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Program error: ${error}`); return null; diff --git a/content/published/patterns/building-data-pipelines/stream-retry-on-failure.mdx b/content/published/patterns/building-data-pipelines/stream-retry-on-failure.mdx index 15881a74..5d5554eb 100644 --- a/content/published/patterns/building-data-pipelines/stream-retry-on-failure.mdx +++ b/content/published/patterns/building-data-pipelines/stream-retry-on-failure.mdx @@ -87,7 +87,7 @@ const program = Effect.gen(function* () { (id) => processItem(id).pipe( Effect.retry(retryPolicy), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log( `❌ Item ${id} failed after all retries: ${error.message}` diff --git a/content/published/patterns/concurrency/add-caching-by-wrapping-a-layer.mdx b/content/published/patterns/concurrency/add-caching-by-wrapping-a-layer.mdx index b559f500..1df66ea1 100644 --- a/content/published/patterns/concurrency/add-caching-by-wrapping-a-layer.mdx +++ b/content/published/patterns/concurrency/add-caching-by-wrapping-a-layer.mdx @@ -51,17 +51,16 @@ This approach is powerful because: We have a `WeatherService` that makes slow API calls. We create a `WeatherService.cached` wrapper layer that adds an in-memory cache using a `Ref` and a `Map`. ```typescript -import { Effect, Layer, Ref } from "effect"; +import { Effect, Layer, Ref, Context } from "effect"; // 1. Define the service interface -class WeatherService extends Effect.Service()( - "WeatherService", - { - sync: () => ({ +class WeatherService extends Context.Service()("WeatherService", { + make: Effect.sync(() => ({ getForecast: (city: string) => Effect.succeed(`Sunny in ${city}`), - }), - } -) {} + })), + }) { + static readonly layer = Layer.effect(this, this.make) +} // 2. The "Live" implementation that is slow const WeatherServiceLive = Layer.succeed( diff --git a/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-deferred.mdx b/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-deferred.mdx index a7d7ca1b..9f2f369e 100644 --- a/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-deferred.mdx +++ b/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-deferred.mdx @@ -139,12 +139,12 @@ const program = Effect.gen(function* () { // Start initializing services in background const initFibers = yield* Effect.all( - services.map((service) => initializeService(service).pipe(Effect.fork)) + services.map((service) => initializeService(service).pipe(Effect.forkChild)) ); // Start workers that wait for services const workerFibers = yield* Effect.all( - [1, 2, 3].map((id) => createWorker(id, services).pipe(Effect.fork)) + [1, 2, 3].map((id) => createWorker(id, services).pipe(Effect.forkChild)) ); // Wait for all workers to complete @@ -182,7 +182,7 @@ const initializeServiceWithTimeout = ( const timeoutDeferred = yield* Deferred.make<"timeout">(); const timeoutFiber = yield* Effect.sleep(`${timeoutMs} millis`).pipe( Effect.andThen(() => Deferred.succeed(timeoutDeferred, "timeout" as const)), - Effect.fork + Effect.forkChild ); const result = yield* Effect.race( @@ -209,7 +209,7 @@ const createWorkerWithFallback = ( services.map((service) => Deferred.await(service.isReady).pipe( Effect.map(() => service.name), - Effect.catchAll(() => Effect.succeed("unavailable")) + Effect.catch(() => Effect.succeed("unavailable")) ) ) ); @@ -259,15 +259,15 @@ const initializeServiceWithErrorHandling = ( } catch (error) { return { success: false as const, error }; } - }).pipe(Effect.either); + }).pipe(Effect.result); - if (result._tag === "Right" && result.right.success) { + if (result._tag === "Success" && result.success.success) { yield* Effect.log(`[${service.name}] Ready`); yield* Deferred.succeed(service.isReady, undefined); } else { const error = new InitializationError( service.name, - result._tag === "Left" ? result.left : result.right.error + result._tag === "Failure" ? result.failure : result.success.error ); yield* Effect.log( @@ -289,11 +289,11 @@ const createWorkerWithErrorHandling = ( const result = yield* Effect.all( services.map((service) => Deferred.await(service.isReady)) - ).pipe(Effect.either); + ).pipe(Effect.result); - if (result._tag === "Left") { - yield* Effect.log(`[Worker ${id}] Service initialization failed: ${result.left.message}`); - yield* Effect.fail(result.left); + if (result._tag === "Failure") { + yield* Effect.log(`[Worker ${id}] Service initialization failed: ${result.failure.message}`); + yield* Effect.fail(result.failure); } yield* Effect.log(`[Worker ${id}] All services ready`); @@ -327,7 +327,7 @@ const createStagedStartup = (): Effect.Effect => yield* Effect.sleep("500 millis"); yield* Effect.log(`[Stage 1] Config loaded`); yield* Deferred.succeed(stages[0].ready, undefined); - }).pipe(Effect.fork); + }).pipe(Effect.forkChild); // Stage 2: Wait for config, then connect database yield* Effect.gen(function* () { @@ -336,7 +336,7 @@ const createStagedStartup = (): Effect.Effect => yield* Effect.sleep("1 second"); yield* Effect.log(`[Stage 2] Database connected`); yield* Deferred.succeed(stages[1].ready, undefined); - }).pipe(Effect.fork); + }).pipe(Effect.forkChild); // Stage 3: Wait for database, then start services yield* Effect.gen(function* () { @@ -345,14 +345,14 @@ const createStagedStartup = (): Effect.Effect => yield* Effect.sleep("500 millis"); yield* Effect.log(`[Stage 3] Services started`); yield* Deferred.succeed(stages[2].ready, undefined); - }).pipe(Effect.fork); + }).pipe(Effect.forkChild); // Stage 4: Wait for services, then ready to serve yield* Effect.gen(function* () { yield* Deferred.await(stages[2].ready); yield* Effect.log(`[Stage 4] Ready to serve!`); yield* Deferred.succeed(stages[3].ready, undefined); - }).pipe(Effect.fork); + }).pipe(Effect.forkChild); // Wait for final stage yield* Deferred.await(stages[3].ready); diff --git a/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-latch.mdx b/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-latch.mdx index 7c7f2e91..a96435c1 100644 --- a/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-latch.mdx +++ b/content/published/patterns/concurrency/concurrency-pattern-coordinate-with-latch.mdx @@ -129,7 +129,7 @@ const fanOutFanIn = Effect.gen(function* () { const workerFibers = yield* Effect.all( Array.from({ length: numWorkers }, (_, i) => - createWorker(i + 1).pipe(Effect.fork) + createWorker(i + 1).pipe(Effect.forkChild) ) ); @@ -218,7 +218,7 @@ const barrierSynchronization = (workers: WorkerConfig[]) => // Spawn all workers with barrier coordination const fibers = yield* Effect.all( workers.map((w) => - createBarrierWorker(w, barrierLatches).pipe(Effect.fork) + createBarrierWorker(w, barrierLatches).pipe(Effect.forkChild) ) ); @@ -294,7 +294,7 @@ const treeJoinCoordination = (config: TreeJoinConfig) => // Start all workers at stage 0 const fibers = yield* Effect.all( Array.from({ length: config.workersPerStage }, (_, i) => - createStageWorker(0, i + 1).pipe(Effect.fork) + createStageWorker(0, i + 1).pipe(Effect.forkChild) ) ); @@ -351,7 +351,7 @@ const coordinatedWithErrorHandling = ( // Execute all tasks const fibers = yield* Effect.all( - tasks.map((t) => executeTask(t).pipe(Effect.fork)) + tasks.map((t) => executeTask(t).pipe(Effect.forkChild)) ); // Wait for all to complete @@ -401,7 +401,7 @@ const coordinatedWithTimeout = ( ); yield* Effect.sleep(`${delay} millis`).pipe( - Effect.catchAll(() => Effect.void) + Effect.catch(() => Effect.void) ); yield* Ref.update(completed, (c) => c + 1); @@ -413,17 +413,17 @@ const coordinatedWithTimeout = ( // Spawn tasks const fibers = yield* Effect.all( Array.from({ length: taskCount }, (_, i) => - unreliableTask(i + 1).pipe(Effect.fork) + unreliableTask(i + 1).pipe(Effect.forkChild) ) ); // Wait with timeout const waitResult = yield* Latch.await(completionLatch).pipe( Effect.timeout(`${timeoutMs} millis`), - Effect.either + Effect.result ); - if (waitResult._tag === "Left") { + if (waitResult._tag === "Failure") { yield* Effect.log( `⚠ Coordination timeout after ${timeoutMs}ms` ); diff --git a/content/published/patterns/concurrency/concurrency-pattern-pubsub-event-broadcast.mdx b/content/published/patterns/concurrency/concurrency-pattern-pubsub-event-broadcast.mdx index 3f229aa5..873b0777 100644 --- a/content/published/patterns/concurrency/concurrency-pattern-pubsub-event-broadcast.mdx +++ b/content/published/patterns/concurrency/concurrency-pattern-pubsub-event-broadcast.mdx @@ -157,25 +157,25 @@ const program = Effect.gen(function* () { "SUBSCRIBER-1", pubsub, subscriber1Events - ).pipe(Effect.fork); + ).pipe(Effect.forkChild); const sub2Fiber = yield* createSubscriber( "SUBSCRIBER-2", pubsub, subscriber2Events - ).pipe(Effect.fork); + ).pipe(Effect.forkChild); const sub3Fiber = yield* createSubscriber( "SUBSCRIBER-3", pubsub, subscriber3Events - ).pipe(Effect.fork); + ).pipe(Effect.forkChild); // Wait for subscriptions to establish yield* Effect.sleep("100 millis"); // Start publisher - const publisherFiber = yield* publisher(pubsub, 5).pipe(Effect.fork); + const publisherFiber = yield* publisher(pubsub, 5).pipe(Effect.forkChild); // Wait for publisher to finish yield* Fiber.join(publisherFiber); @@ -185,9 +185,9 @@ const program = Effect.gen(function* () { // Shut down yield* PubSub.shutdown(pubsub); - yield* Fiber.join(sub1Fiber).pipe(Effect.catchAll(() => Effect.void)); - yield* Fiber.join(sub2Fiber).pipe(Effect.catchAll(() => Effect.void)); - yield* Fiber.join(sub3Fiber).pipe(Effect.catchAll(() => Effect.void)); + yield* Fiber.join(sub1Fiber).pipe(Effect.catch(() => Effect.void)); + yield* Fiber.join(sub2Fiber).pipe(Effect.catch(() => Effect.void)); + yield* Fiber.join(sub3Fiber).pipe(Effect.catch(() => Effect.void)); // Print summary const events1 = yield* Ref.get(subscriber1Events); @@ -413,7 +413,7 @@ const eventAggregator = ( const event = yield* sub.take(); yield* PubSub.publish(aggregate, event); } - }).pipe(Effect.fork) + }).pipe(Effect.forkChild) ); // Wait for all forwarders @@ -430,7 +430,7 @@ const aggregatedEventBus = Effect.gen(function* () { yield* eventAggregator( [source1, source2, source3], eventBus - ).pipe(Effect.fork); + ).pipe(Effect.forkChild); return { source1, source2, source3, eventBus }; }); diff --git a/content/published/patterns/concurrency/concurrency-pattern-queue-work-distribution.mdx b/content/published/patterns/concurrency/concurrency-pattern-queue-work-distribution.mdx index a9a0fa04..40a8536e 100644 --- a/content/published/patterns/concurrency/concurrency-pattern-queue-work-distribution.mdx +++ b/content/published/patterns/concurrency/concurrency-pattern-queue-work-distribution.mdx @@ -127,14 +127,14 @@ const consumer = ( while (true) { // Dequeue - will block if queue is empty - const item = yield* Queue.take(queue).pipe(Effect.either); + const item = yield* Queue.take(queue).pipe(Effect.result); - if (item._tag === "Left") { + if (item._tag === "Failure") { yield* Effect.log(`[CONSUMER ${consumerId}] Queue closed, stopping`); return; } - const workItem = item.right; + const workItem = item.success; const startTime = Date.now(); yield* Effect.log( @@ -168,11 +168,11 @@ const program = Effect.gen(function* () { console.log(`\n[MAIN] Starting producer-consumer pipeline with queue size 3\n`); // Spawn producer - const producerFiber = yield* producer(queue, 10).pipe(Effect.fork); + const producerFiber = yield* producer(queue, 10).pipe(Effect.forkChild); // Spawn 2 consumers - const consumer1 = yield* consumer(queue, 1, results).pipe(Effect.fork); - const consumer2 = yield* consumer(queue, 2, results).pipe(Effect.fork); + const consumer1 = yield* consumer(queue, 1, results).pipe(Effect.forkChild); + const consumer2 = yield* consumer(queue, 2, results).pipe(Effect.forkChild); // Wait for producer to finish yield* Fiber.join(producerFiber); @@ -347,15 +347,15 @@ const batchConsumer = ( for (let i = 0; i < batchSize; i++) { const item = yield* Queue.take(queue).pipe( Effect.timeout("100 millis"), - Effect.either + Effect.result ); - if (item._tag === "Left") { + if (item._tag === "Failure") { // Timeout - process whatever we have break; } - batch.push(item.right); + batch.push(item.success); } if (batch.length > 0) { diff --git a/content/published/patterns/concurrency/concurrency-pattern-race-timeout.mdx b/content/published/patterns/concurrency/concurrency-pattern-race-timeout.mdx index d2845070..5da53f4b 100644 --- a/content/published/patterns/concurrency/concurrency-pattern-race-timeout.mdx +++ b/content/published/patterns/concurrency/concurrency-pattern-race-timeout.mdx @@ -124,12 +124,12 @@ const program = Effect.gen(function* () { const slowOp = fetchFromSource({ name: "Slow Op", latencyMs: 2000 }).pipe( Effect.timeout("500 millis"), - Effect.either + Effect.result ); const timeoutResult = yield* slowOp; - if (timeoutResult._tag === "Left") { + if (timeoutResult._tag === "Failure") { console.log(`βœ— Operation timed out after 500ms\n`); } @@ -142,7 +142,7 @@ const program = Effect.gen(function* () { const raceWithFallback = primary.pipe( Effect.timeout("150 millis"), - Effect.catchAll(() => + Effect.catch(() => Effect.gen(function* () { yield* Effect.log(`[PRIMARY] Timed out, using fallback`); return yield* fallback; @@ -215,10 +215,10 @@ const circuitBreakerWithTimeout = ( // Execute with timeout const result = yield* op.pipe( Effect.timeout(`${timeoutMs} millis`), - Effect.either + Effect.result ); - if (result._tag === "Right") { + if (result._tag === "Success") { failureCount = 0; if (state === CircuitState.HalfOpen) { @@ -232,7 +232,7 @@ const circuitBreakerWithTimeout = ( } } - return result.right; + return result.success; } // Failure @@ -247,7 +247,7 @@ const circuitBreakerWithTimeout = ( ); } - yield* Effect.fail(result.left); + yield* Effect.fail(result.failure); }); }); ``` @@ -273,14 +273,14 @@ const retryWithTimeoutAndBackoff = ( for (let attempt = 0; attempt < config.maxRetries; attempt++) { const result = yield* effect.pipe( Effect.timeout(`${config.timeoutMs} millis`), - Effect.either + Effect.result ); - if (result._tag === "Right") { - return result.right; + if (result._tag === "Success") { + return result.success; } - lastError = result.left as Error; + lastError = result.failure as Error; if (attempt < config.maxRetries - 1) { const backoff = config.initialBackoffMs * Math.pow(2, attempt); @@ -329,16 +329,16 @@ const timeoutWithCleanup = ( const result = yield* effect.pipe( Effect.timeout(`${timeoutMs} millis`), Effect.ensuring(cleanup), - Effect.either + Effect.result ); - if (result._tag === "Left") { + if (result._tag === "Failure") { yield* Effect.fail( new Error(`Operation timed out after ${timeoutMs}ms`) ); } - return result.right; + return result.success; }); // Usage: Timeout with resource cleanup @@ -370,13 +370,13 @@ const firstToSucceed = ( let lastError: Error | undefined; for (const effect of effects) { - const result = yield* effect.pipe(Effect.either); + const result = yield* effect.pipe(Effect.result); - if (result._tag === "Right") { - return result.right; + if (result._tag === "Success") { + return result.success; } - lastError = result.left as Error; + lastError = result.failure as Error; } yield* Effect.fail( diff --git a/content/published/patterns/concurrency/concurrency-pattern-rate-limit-with-semaphore.mdx b/content/published/patterns/concurrency/concurrency-pattern-rate-limit-with-semaphore.mdx index e4f66cb1..36e0de43 100644 --- a/content/published/patterns/concurrency/concurrency-pattern-rate-limit-with-semaphore.mdx +++ b/content/published/patterns/concurrency/concurrency-pattern-rate-limit-with-semaphore.mdx @@ -151,7 +151,7 @@ const program = Effect.gen(function* () { // Execute all queries with concurrency limit const results = yield* Effect.all( queries.map((q) => - executor(q.id, Math.round(q.duration)).pipe(Effect.fork) + executor(q.id, Math.round(q.duration)).pipe(Effect.forkChild) ) ).pipe( Effect.andThen((fibers) => @@ -207,10 +207,10 @@ const createTimedRateLimitedExecutor = ( // Try to acquire with timeout const acquired = yield* Semaphore.acquire(semaphore).pipe( Effect.timeout(`${config.acquireTimeoutMs} millis`), - Effect.either + Effect.result ); - if (acquired._tag === "Left") { + if (acquired._tag === "Failure") { yield* Effect.log( `[Query ${queryId}] ❌ No connection available within timeout` ); @@ -262,7 +262,7 @@ const createPrioritySemaphore = ( // Try immediate acquisition const available = yield* semaphore.pipe( Effect.flatMap(() => Effect.succeed(true)), - Effect.catchAll(() => Effect.succeed(false)) + Effect.catch(() => Effect.succeed(false)) ); if (available) { diff --git a/content/published/patterns/concurrency/decouple-fibers-with-queue-pubsub.mdx b/content/published/patterns/concurrency/decouple-fibers-with-queue-pubsub.mdx index eb20fcd1..983205d9 100644 --- a/content/published/patterns/concurrency/decouple-fibers-with-queue-pubsub.mdx +++ b/content/published/patterns/concurrency/decouple-fibers-with-queue-pubsub.mdx @@ -78,7 +78,7 @@ const program = Effect.gen(function* () { // Producer is faster than consumer (500ms vs 1000ms) to demonstrate queue buffering. yield* Effect.sleep("500 millis"); } - }).pipe(Effect.fork); // Fork creates a new fiber that runs concurrently + }).pipe(Effect.forkChild); // Fork creates a new fiber that runs concurrently yield* Effect.logInfo("Started producer fiber"); @@ -98,7 +98,7 @@ const program = Effect.gen(function* () { yield* Effect.sleep("1 second"); yield* Effect.logInfo(`Completed ${job}`); } - }).pipe(Effect.fork); // Fork creates another independent fiber + }).pipe(Effect.forkChild); // Fork creates another independent fiber yield* Effect.logInfo("Started worker fiber"); @@ -152,7 +152,7 @@ const program = Effect.gen(function* () { } }) ), - Effect.fork + Effect.forkChild ); // Subscriber 2: The "Notifier" service @@ -165,7 +165,7 @@ const program = Effect.gen(function* () { } }) ), - Effect.fork + Effect.forkChild ); // Give subscribers time to start diff --git a/content/published/patterns/concurrency/getting-started/concurrency-fork-basics.mdx b/content/published/patterns/concurrency/getting-started/concurrency-fork-basics.mdx index ab122922..919831d1 100644 --- a/content/published/patterns/concurrency/getting-started/concurrency-fork-basics.mdx +++ b/content/published/patterns/concurrency/getting-started/concurrency-fork-basics.mdx @@ -4,7 +4,7 @@ id: concurrency-fork-basics skillLevel: beginner applicationPatternId: concurrency-getting-started summary: >- - Use Effect.fork to run work in the background while your main code continues. + Use Effect.forkChild to run work in the background while your main code continues. tags: - concurrency - fork @@ -12,7 +12,7 @@ tags: - fiber - getting-started rule: - description: Use Effect.fork to start background work, Effect.forkDaemon for fire-and-forget tasks. + description: Use Effect.forkChild to start background work, Effect.forkDetach for fire-and-forget tasks. author: PaulJPhilp related: - concurrency-understanding-fibers @@ -21,7 +21,7 @@ related: ## Guideline -Use `Effect.fork` to run effects in the background. The forked effect runs on its own fiber while your main code continues. +Use `Effect.forkChild` to run effects in the background. The forked effect runs on its own fiber while your main code continues. --- @@ -46,7 +46,7 @@ import { Effect, Fiber } from "effect" const basicFork = Effect.gen(function* () { // Start expensive work in background - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( Effect.gen(function* () { yield* Effect.log("Starting expensive computation...") yield* Effect.sleep("2 seconds") @@ -70,7 +70,7 @@ const basicFork = Effect.gen(function* () { const fireAndForget = Effect.gen(function* () { // This runs completely independently - yield* Effect.forkDaemon( + yield* Effect.forkDetach( Effect.gen(function* () { yield* Effect.log("Daemon: Starting background task") yield* Effect.sleep("5 seconds") @@ -109,7 +109,7 @@ const fanOut = Effect.gen(function* () { // Fork all tasks const fibers = yield* Effect.forEach(tasks, (n) => - Effect.fork( + Effect.forkChild( Effect.gen(function* () { yield* Effect.sleep(`${n * 100} millis`) yield* Effect.log(`Task ${n} complete`) @@ -132,8 +132,8 @@ Effect.runPromise(fanOut) | Method | Behavior | |--------|----------| -| `Effect.fork` | Child fiber, interrupted when parent completes | -| `Effect.forkDaemon` | Independent fiber, keeps running | +| `Effect.forkChild` | Child fiber, interrupted when parent completes | +| `Effect.forkDetach` | Independent fiber, keeps running | | `Effect.forkScoped` | Tied to scope, interrupted when scope closes | | `Effect.forkIn(scope)` | Fork into specific scope | @@ -142,16 +142,16 @@ Effect.runPromise(fanOut) ```typescript Effect.gen(function* () { // Fire and forget (logging, analytics) - yield* Effect.forkDaemon(sendAnalytics(event)) + yield* Effect.forkDetach(sendAnalytics(event)) // Background with timeout - const fiber = yield* Effect.fork(longRunningTask) + const fiber = yield* Effect.forkChild(longRunningTask) yield* Effect.sleep("5 seconds") yield* Fiber.interrupt(fiber) // Cancel if still running // Parallel fan-out/fan-in const fibers = yield* Effect.forEach(items, (item) => - Effect.fork(processItem(item)) + Effect.forkChild(processItem(item)) ) const results = yield* Fiber.joinAll(fibers) }) diff --git a/content/published/patterns/concurrency/getting-started/concurrency-understanding-fibers.mdx b/content/published/patterns/concurrency/getting-started/concurrency-understanding-fibers.mdx index 13e8a1f6..71a7e2b7 100644 --- a/content/published/patterns/concurrency/getting-started/concurrency-understanding-fibers.mdx +++ b/content/published/patterns/concurrency/getting-started/concurrency-understanding-fibers.mdx @@ -68,7 +68,7 @@ const withFork = Effect.gen(function* () { yield* Effect.log("Main fiber starting") // Fork creates a new fiber that runs independently - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( Effect.gen(function* () { yield* Effect.log("Child fiber running") yield* Effect.sleep("200 millis") @@ -102,7 +102,7 @@ Got result: child result // ============================================ const fiberOps = Effect.gen(function* () { - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( Effect.gen(function* () { yield* Effect.sleep("1 second") return "done" @@ -136,7 +136,7 @@ const fiberOps = Effect.gen(function* () { | Operation | What it does | |-----------|--------------| -| `Effect.fork(effect)` | Start effect on new fiber | +| `Effect.forkChild(effect)` | Start effect on new fiber | | `Fiber.join(fiber)` | Wait for fiber to complete | | `Fiber.interrupt(fiber)` | Cancel the fiber | | `Fiber.poll(fiber)` | Check status without waiting | diff --git a/content/published/patterns/concurrency/implement-graceful-shutdown.mdx b/content/published/patterns/concurrency/implement-graceful-shutdown.mdx index 6f43deb1..41fe9d89 100644 --- a/content/published/patterns/concurrency/implement-graceful-shutdown.mdx +++ b/content/published/patterns/concurrency/implement-graceful-shutdown.mdx @@ -56,14 +56,16 @@ import { Effect, Layer, Fiber, Context, Scope } from "effect"; import * as http from "http"; // 1. A service with a finalizer for cleanup -class Database extends Effect.Service()("Database", { - effect: Effect.gen(function* () { +class Database extends Context.Service()("Database", { + make: Effect.gen(function* () { yield* Effect.log("Acquiring DB connection"); return { query: () => Effect.succeed("data"), }; }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} // 2. The main server logic const server = Effect.gen(function* () { @@ -75,7 +77,7 @@ const server = Effect.gen(function* () { Effect.runFork( Effect.provide( db.query().pipe(Effect.map((data) => res.end(data))), - Database.Default + Database.layer ) ); }); @@ -91,7 +93,7 @@ const server = Effect.gen(function* () { ); // Start server with error handling - yield* Effect.async((resume) => { + yield* Effect.callback((resume) => { httpServer.once("error", (err: Error) => { resume(Effect.fail(new Error(`Failed to start server: ${err.message}`))); }); @@ -109,7 +111,7 @@ const server = Effect.gen(function* () { }); // 3. Provide the layer and launch with runFork -const app = Effect.provide(server.pipe(Effect.scoped), Database.Default); +const app = Effect.provide(server.pipe(Effect.scoped), Database.layer); // 4. Run the app and handle shutdown Effect.runPromise(app).catch((error) => { diff --git a/content/published/patterns/concurrency/manage-resource-lifecycles-with-scope.mdx b/content/published/patterns/concurrency/manage-resource-lifecycles-with-scope.mdx index 1afd2dbb..1ba9adbb 100644 --- a/content/published/patterns/concurrency/manage-resource-lifecycles-with-scope.mdx +++ b/content/published/patterns/concurrency/manage-resource-lifecycles-with-scope.mdx @@ -31,7 +31,7 @@ A `Scope` is a context that collects finalizers (cleanup effects). When you need ## Rationale -`Scope` is the fundamental building block for all resource management in Effect. While higher-level APIs like `Layer.scoped` and `Stream` are often sufficient, understanding `Scope` is key to advanced use cases. +`Scope` is the fundamental building block for all resource management in Effect. While higher-level APIs like `Layer.effect` and `Stream` are often sufficient, understanding `Scope` is key to advanced use cases. A `Scope` guarantees that any finalizers added to it will be executed when the scope is closed, regardless of whether the associated computation succeeds, fails, or is interrupted. This provides a rock-solid guarantee against resource leaks. @@ -44,7 +44,7 @@ This is especially critical in concurrent applications. When a parent fiber is i This example shows how to acquire a resource (like a file handle), use it, and have `Scope` guarantee its release. ```typescript -import { Effect, Scope } from "effect"; +import { Effect, Scope, Layer } from "effect"; // Simulate acquiring and releasing a resource const acquireFile = Effect.log("File opened").pipe( diff --git a/content/published/patterns/concurrency/poll-for-status-until-task-completes.mdx b/content/published/patterns/concurrency/poll-for-status-until-task-completes.mdx index 2270894a..375362a8 100644 --- a/content/published/patterns/concurrency/poll-for-status-until-task-completes.mdx +++ b/content/published/patterns/concurrency/poll-for-status-until-task-completes.mdx @@ -90,7 +90,7 @@ import { longRunningJob, repeatingPoller } from "./somewhere"; // ❌ WRONG: Manual fiber management is complex. const program = Effect.gen(function* () { // Manually fork the poller into the background - const pollerFiber = yield* Effect.fork(repeatingPoller); + const pollerFiber = yield* Effect.forkChild(repeatingPoller); try { // Run the main job diff --git a/content/published/patterns/concurrency/race-concurrent-effects.mdx b/content/published/patterns/concurrency/race-concurrent-effects.mdx index ec1b1620..f6c095c1 100644 --- a/content/published/patterns/concurrency/race-concurrent-effects.mdx +++ b/content/published/patterns/concurrency/race-concurrent-effects.mdx @@ -85,7 +85,7 @@ const programWithResults = Effect.gen(function* () { throw error; } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Handled error: ${error}`); return null; @@ -110,7 +110,7 @@ const programWithLogging = Effect.gen(function* () { return null; } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logInfo(`Handled error: ${error}`); return null; diff --git a/content/published/patterns/concurrency/run-background-tasks-with-fork.mdx b/content/published/patterns/concurrency/run-background-tasks-with-fork.mdx index f61e11d5..9674741f 100644 --- a/content/published/patterns/concurrency/run-background-tasks-with-fork.mdx +++ b/content/published/patterns/concurrency/run-background-tasks-with-fork.mdx @@ -1,10 +1,10 @@ --- -title: Run Background Tasks with Effect.fork +title: Run Background Tasks with Effect.forkChild id: run-background-tasks-with-fork skillLevel: advanced applicationPatternId: concurrency summary: >- - Use Effect.fork to start a computation in a background fiber, allowing the + Use Effect.forkChild to start a computation in a background fiber, allowing the parent fiber to continue its work without waiting. tags: - concurrency @@ -14,7 +14,7 @@ tags: - asynchronous rule: description: >- - Use Effect.fork to start a non-blocking background process and manage its + Use Effect.forkChild to start a non-blocking background process and manage its lifecycle via its Fiber. related: - run-effects-in-parallel-with-all @@ -25,13 +25,13 @@ lessonOrder: 7 ## Guideline -To start an `Effect` in the background without blocking the current execution flow, use `Effect.fork`. This immediately returns a `Fiber`, which is a handle to the running computation that you can use to manage its lifecycle (e.g., interrupt it or wait for its result). +To start an `Effect` in the background without blocking the current execution flow, use `Effect.forkChild`. This immediately returns a `Fiber`, which is a handle to the running computation that you can use to manage its lifecycle (e.g., interrupt it or wait for its result). --- ## Rationale -Unlike `Effect.all` or a direct `yield*`, which wait for the computation to complete, `Effect.fork` is a "fire and forget" operation. It starts the effect on a new, concurrent fiber and immediately returns control to the parent fiber. +Unlike `Effect.all` or a direct `yield*`, which wait for the computation to complete, `Effect.forkChild` is a "fire and forget" operation. It starts the effect on a new, concurrent fiber and immediately returns control to the parent fiber. This is essential for managing long-running background tasks like: @@ -62,10 +62,10 @@ const program = Effect.gen(function* () { yield* Effect.log("Forking the ticking clock into the background."); // Start the clock, but don't wait for it. - // Effect.fork creates a new fiber that runs concurrently with the main program + // Effect.forkChild creates a new fiber that runs concurrently with the main program // The main fiber continues immediately without waiting for the background task // This is essential for non-blocking background operations - const clockFiber = yield* Effect.fork(tickingClock); + const clockFiber = yield* Effect.forkChild(tickingClock); // At this point, we have two fibers running: // 1. The main fiber (this program) @@ -113,7 +113,7 @@ Effect.runPromise(program); ## Anti-Pattern -The anti-pattern is using `Effect.fork` when you immediately need the result of the computation. This is an overly complicated and less readable way of just running the effect directly. +The anti-pattern is using `Effect.forkChild` when you immediately need the result of the computation. This is an overly complicated and less readable way of just running the effect directly. ```typescript import { Effect, Fiber } from "effect"; @@ -122,7 +122,7 @@ const someEffect = Effect.succeed(42); // ❌ WRONG: This is unnecessarily complex. const program = Effect.gen(function* () { - const fiber = yield* Effect.fork(someEffect); + const fiber = yield* Effect.forkChild(someEffect); // You immediately wait for the result, defeating the purpose of forking. const result = yield* Fiber.join(fiber); return result; diff --git a/content/published/patterns/concurrency/understand-fibers-as-lightweight-threads.mdx b/content/published/patterns/concurrency/understand-fibers-as-lightweight-threads.mdx index d3e0fa78..4a9213c6 100644 --- a/content/published/patterns/concurrency/understand-fibers-as-lightweight-threads.mdx +++ b/content/published/patterns/concurrency/understand-fibers-as-lightweight-threads.mdx @@ -39,7 +39,7 @@ Effect's `Fiber`s are different. They are managed entirely by the Effect runtime This model, known as M:N threading (M fibers on N OS threads), allows for a massive level of concurrency that is impossible with traditional threads. It's what makes Effect so powerful for building highly concurrent applications like servers, data pipelines, and real-time systems. -When you use operators like `Effect.fork` or `Effect.all`, you are creating new fibers. +When you use operators like `Effect.forkChild` or `Effect.all`, you are creating new fibers. --- @@ -64,10 +64,10 @@ const program = Effect.gen(function* () { ); // Fork all of them into background fibers - // Effect.fork creates a new fiber for each task without blocking + // Effect.forkChild creates a new fiber for each task without blocking // This demonstrates fiber creation scalability - 100k fibers created almost instantly // Each fiber is much lighter than an OS thread (typically ~1KB vs ~8MB per thread) - const fibers = yield* Effect.forEach(tasks, Effect.fork); + const fibers = yield* Effect.forEach(tasks, Effect.forkChild); yield* Effect.log( "All fibers have been forked. Now waiting for them to complete..." diff --git a/content/published/patterns/core-concepts/access-config-in-context.mdx b/content/published/patterns/core-concepts/access-config-in-context.mdx index bfcdec31..c213bc95 100644 --- a/content/published/patterns/core-concepts/access-config-in-context.mdx +++ b/content/published/patterns/core-concepts/access-config-in-context.mdx @@ -34,15 +34,17 @@ This allows your business logic to declaratively state its dependency on a piece ## Good Example ```typescript -import { Config, Effect, Layer } from "effect"; +import { Config, Effect, Layer, Context } from "effect"; // Define config service -class AppConfig extends Effect.Service()("AppConfig", { - sync: () => ({ +class AppConfig extends Context.Service()("AppConfig", { + make: Effect.sync(() => ({ host: "localhost", port: 3000, - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create program that uses config const program = Effect.gen(function* () { @@ -51,7 +53,7 @@ const program = Effect.gen(function* () { }); // Run the program with default config -Effect.runPromise(Effect.provide(program, AppConfig.Default)); +Effect.runPromise(Effect.provide(program, AppConfig.layer)); ``` **Explanation:** diff --git a/content/published/patterns/core-concepts/beyond-the-date-type.mdx b/content/published/patterns/core-concepts/beyond-the-date-type.mdx index b2079df1..59fdba7b 100644 --- a/content/published/patterns/core-concepts/beyond-the-date-type.mdx +++ b/content/published/patterns/core-concepts/beyond-the-date-type.mdx @@ -81,7 +81,7 @@ const program = Effect.gen(function* () { // Run the program const programWithErrorHandling = program.pipe( Effect.provideService(Clock.Clock, Clock.make()), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Program error: ${error}`); return null; diff --git a/content/published/patterns/core-concepts/combinator-conditional.mdx b/content/published/patterns/core-concepts/combinator-conditional.mdx index 63a674aa..85996620 100644 --- a/content/published/patterns/core-concepts/combinator-conditional.mdx +++ b/content/published/patterns/core-concepts/combinator-conditional.mdx @@ -5,7 +5,7 @@ skillLevel: beginner applicationPatternId: core-concepts summary: >- Use combinators like if, when, and cond to express conditional logic - declaratively across Effect, Stream, Option, and Either. + declaratively across Effect, Stream, Option, and Result. tags: - conditional - if @@ -43,7 +43,7 @@ It also ensures that error handling and context propagation are preserved, and t ## Good Example ```typescript -import { Effect, Stream, Option, Either } from "effect"; +import { Effect, Stream, Option, Result } from "effect"; // Effect: Branch based on a condition const effect = Effect.if(true, { @@ -54,8 +54,8 @@ const effect = Effect.if(true, { // Option: Conditionally create an Option const option = true ? Option.some("yes") : Option.none(); // Option (Some("yes")) -// Either: Conditionally create an Either -const either = true ? Either.right("yes") : Either.left("error"); // Either (Right("yes")) +// Result: Conditionally create an Result +const either = true ? Result.succeed("yes") : Result.fail("error"); // Result (Right("yes")) // Stream: Conditionally emit a stream const stream = false ? Stream.fromIterable([1, 2]) : Stream.empty; // Stream (empty) diff --git a/content/published/patterns/core-concepts/combinator-error-handling.mdx b/content/published/patterns/core-concepts/combinator-error-handling.mdx index 47a1c791..811f2dd5 100644 --- a/content/published/patterns/core-concepts/combinator-error-handling.mdx +++ b/content/published/patterns/core-concepts/combinator-error-handling.mdx @@ -1,19 +1,19 @@ --- -title: 'Handling Errors with catchAll, orElse, and match' +title: 'Handling Errors with catch, orElse, and match' id: combinator-error-handling skillLevel: intermediate applicationPatternId: core-concepts summary: >- - Use catchAll, orElse, and match to recover from errors, provide fallbacks, or - transform errors in Effect, Either, and Option. + Use catch, orElse, and match to recover from errors, provide fallbacks, or + transform errors in Effect, Result, and Option. tags: - error-handling - - catchAll + - catch - orElse - match - combinator - effect - - either + - result - option rule: description: >- @@ -27,11 +27,11 @@ author: PaulJPhilp lessonOrder: 5 --- -# Handling Errors with `catchAll`, `orElse`, and `match` +# Handling Errors with `catch`, `orElse`, and `match` ## Guideline -Use combinators like `catchAll`, `orElse`, and `match` to handle errors declaratively. +Use combinators like `catch`, `orElse`, and `match` to handle errors declaratively. These allow you to recover from failures, provide fallback values, or transform errors, all while preserving composability and type safety. ## Rationale @@ -42,20 +42,20 @@ By using combinators, you keep error recovery logic close to where errors may oc ## Good Example ```typescript -import { Effect, Option, Either } from "effect"; +import { Effect, Option, Result } from "effect"; // Effect: Recover from any error const effect = Effect.fail("fail!").pipe( - Effect.catchAll((err) => Effect.succeed(`Recovered from: ${err}`)) + Effect.catch((err) => Effect.succeed(`Recovered from: ${err}`)) ); // Effect // Option: Provide a fallback if value is None const option = Option.none().pipe(Option.orElse(() => Option.some("default"))); // Option -// Either: Provide a fallback if value is Left -const either = Either.left("error").pipe( - Either.orElse(() => Either.right("fallback")) -); // Either +// Result: Provide a fallback if value is Left +const either = Result.fail("error").pipe( + Result.orElse(() => Result.succeed("fallback")) +); // Result // Effect: Pattern match on success or failure const matchEffect = Effect.fail("fail!").pipe( diff --git a/content/published/patterns/core-concepts/combinator-filter.mdx b/content/published/patterns/core-concepts/combinator-filter.mdx index 1e65d8f4..ee51547f 100644 --- a/content/published/patterns/core-concepts/combinator-filter.mdx +++ b/content/published/patterns/core-concepts/combinator-filter.mdx @@ -5,7 +5,7 @@ skillLevel: beginner applicationPatternId: core-concepts summary: >- Use filter to keep or discard results based on a predicate, across Effect, - Stream, Option, and Either. + Stream, Option, and Result. tags: - filter - combinator @@ -31,7 +31,7 @@ lessonOrder: 13 ## Guideline Use the `filter` combinator to keep only those values that satisfy a predicate. -This works for `Effect`, `Stream`, `Option`, and `Either`, allowing you to express conditional logic declaratively and safely. +This works for `Effect`, `Stream`, `Option`, and `Result`, allowing you to express conditional logic declaratively and safely. ## Rationale @@ -41,7 +41,7 @@ It keeps your code composable and type-safe, and ensures that failures or empty ## Good Example ```typescript -import { Effect, Stream, Option, Either } from "effect"; +import { Effect, Stream, Option, Result } from "effect"; // Effect: Only succeed if the value is even, fail otherwise const effect = Effect.succeed(4).pipe( @@ -56,12 +56,12 @@ const option = Option.some(4).pipe( Option.filter((n): n is number => n % 2 === 0) ); // Option -// Either: Use map and flatMap to filter -const either = Either.right(4).pipe( - Either.flatMap((n) => - n % 2 === 0 ? Either.right(n) : Either.left("Number is not even") +// Result: Use map and flatMap to filter +const either = Result.succeed(4).pipe( + Result.flatMap((n) => + n % 2 === 0 ? Result.succeed(n) : Result.fail("Number is not even") ) -); // Either +); // Result // Stream: Only emit even numbers const stream = Stream.fromIterable([1, 2, 3, 4]).pipe( @@ -70,9 +70,9 @@ const stream = Stream.fromIterable([1, 2, 3, 4]).pipe( ``` **Explanation:** -`filter` applies a predicate to the value(s) inside the structure. If the predicate fails, the result is a failure (`Effect.fail`, `Either.left`), `Option.none`, or an empty stream. +`filter` applies a predicate to the value(s) inside the structure. If the predicate fails, the result is a failure (`Effect.fail`, `Result.fail`), `Option.none`, or an empty stream. ## Anti-Pattern -Using `map` with a conditional that returns `Option` or `Either`, then manually flattening, instead of using `filter`. +Using `map` with a conditional that returns `Option` or `Result`, then manually flattening, instead of using `filter`. This leads to unnecessary complexity and less readable code. diff --git a/content/published/patterns/core-concepts/combinator-flatmap.mdx b/content/published/patterns/core-concepts/combinator-flatmap.mdx index 22308995..aa5f70b7 100644 --- a/content/published/patterns/core-concepts/combinator-flatmap.mdx +++ b/content/published/patterns/core-concepts/combinator-flatmap.mdx @@ -30,7 +30,7 @@ lessonOrder: 2 ## Guideline -Use the `flatMap` combinator to chain together computations where each step may itself return an `Effect`, `Stream`, `Option`, or `Either`. +Use the `flatMap` combinator to chain together computations where each step may itself return an `Effect`, `Stream`, `Option`, or `Result`. `flatMap` ensures that the result is always "flattened"β€”you never get nested types. ## Rationale @@ -41,7 +41,7 @@ It allows you to express workflows where each step may fail, be optional, or pro ## Good Example ```typescript -import { Effect, Stream, Option, Either } from "effect"; +import { Effect, Stream, Option, Result } from "effect"; // Effect: Chain two effectful computations const effect = Effect.succeed(2).pipe( @@ -51,10 +51,10 @@ const effect = Effect.succeed(2).pipe( // Option: Chain two optional computations const option = Option.some(2).pipe(Option.flatMap((n) => Option.some(n * 10))); // Option -// Either: Chain two computations that may fail -const either = Either.right(2).pipe( - Either.flatMap((n) => Either.right(n * 10)) -); // Either +// Result: Chain two computations that may fail +const either = Result.succeed(2).pipe( + Result.flatMap((n) => Result.succeed(n * 10)) +); // Result // Stream: Chain streams (flattening) const stream = Stream.fromIterable([1, 2]).pipe( diff --git a/content/published/patterns/core-concepts/combinator-foreach-all.mdx b/content/published/patterns/core-concepts/combinator-foreach-all.mdx index 1e9c0ed5..f5334a88 100644 --- a/content/published/patterns/core-concepts/combinator-foreach-all.mdx +++ b/content/published/patterns/core-concepts/combinator-foreach-all.mdx @@ -44,7 +44,7 @@ These combinators let you express "do this for every item" declaratively, withou ## Good Example ```typescript -import { Effect, Either, Option, Stream } from "effect"; +import { Effect, Result, Option, Stream } from "effect"; // Effect: Apply an effectful function to each item in an array const numbers = [1, 2, 3]; @@ -59,9 +59,9 @@ const allEffect = Effect.all(effects, { concurrency: "unbounded" }); // Effect<[ const options = [Option.some(1), Option.none(), Option.some(3)]; const filtered = options.filter(Option.isSome).map((o) => o.value); // [1, 3] -// Either: Collect all Right values from a collection of Eithers -const eithers = [Either.right(1), Either.left("fail"), Either.right(3)]; -const rights = eithers.filter(Either.isRight); // [Either.Right(1), Either.Right(3)] +// Result: Collect all Right values from a collection of Results +const eithers = [Result.succeed(1), Result.fail("fail"), Result.succeed(3)]; +const rights = eithers.filter(Result.isSuccess); // [Result.Right(1), Result.Right(3)] // Stream: Map and flatten a stream of arrays const stream = Stream.fromIterable([ diff --git a/content/published/patterns/core-concepts/combinator-map.mdx b/content/published/patterns/core-concepts/combinator-map.mdx index afe4ef58..dd89dd30 100644 --- a/content/published/patterns/core-concepts/combinator-map.mdx +++ b/content/published/patterns/core-concepts/combinator-map.mdx @@ -4,7 +4,7 @@ id: combinator-map skillLevel: beginner applicationPatternId: core-concepts summary: >- - Use map to transform the result of an Effect, Stream, Option, or Either in a + Use map to transform the result of an Effect, Stream, Option, or Result in a declarative, type-safe way. tags: - map @@ -13,11 +13,11 @@ tags: - effect - stream - option - - either + - result rule: description: >- Use map to apply a pure function to the value inside an Effect, Stream, - Option, or Either. + Option, or Result. related: - combinator-flatmap - combinator-filter @@ -29,7 +29,7 @@ lessonOrder: 20 ## Guideline -Use the `map` combinator to apply a pure function to the value inside an `Effect`, `Stream`, `Option`, or `Either`. +Use the `map` combinator to apply a pure function to the value inside an `Effect`, `Stream`, `Option`, or `Result`. This lets you transform results without changing the structure or error-handling behavior of the original type. ## Rationale @@ -41,7 +41,7 @@ The same mental model applies across all major Effect types. ## Good Example ```typescript -import { Effect, Stream, Option, Either } from "effect"; +import { Effect, Stream, Option, Result } from "effect"; // Effect: Transform the result of an effect const effect = Effect.succeed(2).pipe(Effect.map((n) => n * 10)); // Effect @@ -49,8 +49,8 @@ const effect = Effect.succeed(2).pipe(Effect.map((n) => n * 10)); // Effect n * 10)); // Option -// Either: Transform a value that may be an error -const either = Either.right(2).pipe(Either.map((n) => n * 10)); // Either +// Result: Transform a value that may be an error +const either = Result.succeed(2).pipe(Result.map((n) => n * 10)); // Result // Stream: Transform every value in a stream const stream = Stream.fromIterable([1, 2, 3]).pipe(Stream.map((n) => n * 10)); // Stream diff --git a/content/published/patterns/core-concepts/combinator-sequencing.mdx b/content/published/patterns/core-concepts/combinator-sequencing.mdx index b950da3c..567ae080 100644 --- a/content/published/patterns/core-concepts/combinator-sequencing.mdx +++ b/content/published/patterns/core-concepts/combinator-sequencing.mdx @@ -5,7 +5,7 @@ skillLevel: intermediate applicationPatternId: core-concepts summary: >- Use andThen, tap, and flatten to sequence computations, run side effects, and - flatten nested structures in Effect, Stream, Option, and Either. + flatten nested structures in Effect, Stream, Option, and Result. tags: - sequencing - andThen @@ -41,7 +41,7 @@ Use sequencing combinators to run computations in order, perform side effects, o - `tap` runs a side-effecting computation with the result, without changing the value. - `flatten` removes one level of nesting from nested structures. -These work for `Effect`, `Stream`, `Option`, and `Either`. +These work for `Effect`, `Stream`, `Option`, and `Result`. ## Rationale @@ -57,7 +57,7 @@ All while preserving composability, error handling, and type safety. ## Good Example ```typescript -import { Effect, Stream, Option, Either } from "effect"; +import { Effect, Stream, Option, Result } from "effect"; // andThen: Run one effect, then another, ignore the first result const logThenCompute = Effect.log("Starting...").pipe( diff --git a/content/published/patterns/core-concepts/combinator-zip.mdx b/content/published/patterns/core-concepts/combinator-zip.mdx index 0e86150b..e67b8024 100644 --- a/content/published/patterns/core-concepts/combinator-zip.mdx +++ b/content/published/patterns/core-concepts/combinator-zip.mdx @@ -5,7 +5,7 @@ skillLevel: beginner applicationPatternId: core-concepts summary: >- Use zip to combine two computations, pairing their results together in Effect, - Stream, Option, or Either. + Stream, Option, or Result. tags: - zip - combinator @@ -31,7 +31,7 @@ lessonOrder: 3 ## Guideline Use the `zip` combinator to combine two computations, pairing their results together. -This works for `Effect`, `Stream`, `Option`, and `Either`, and is useful when you want to run two computations and work with both results. +This works for `Effect`, `Stream`, `Option`, and `Result`, and is useful when you want to run two computations and work with both results. ## Rationale @@ -41,7 +41,7 @@ It preserves error handling and context, and keeps your code declarative and typ ## Good Example ```typescript -import { Effect, Either, Option, Stream } from "effect"; +import { Effect, Result, Option, Stream } from "effect"; // Effect: Combine two effects and get both results const effectA = Effect.succeed(1); @@ -53,10 +53,10 @@ const optionA = Option.some(1); const optionB = Option.some("hello"); const zippedOption = Option.all([optionA, optionB]); // Option<[number, string]> -// Either: Combine two eithers, only Right if both are Right -const eitherA = Either.right(1); -const eitherB = Either.right("hello"); -const zippedEither = Either.all([eitherA, eitherB]); // Either +// Result: Combine two eithers, only Right if both are Right +const eitherA = Result.succeed(1); +const eitherB = Result.succeed("hello"); +const zippedEither = Result.all([eitherA, eitherB]); // Result<[number, string], never> // Stream: Pair up values from two streams const streamA = Stream.fromIterable([1, 2, 3]); diff --git a/content/published/patterns/core-concepts/comparing-data-by-value-with-structural-equality.mdx b/content/published/patterns/core-concepts/comparing-data-by-value-with-structural-equality.mdx index 8d597b3e..c25019be 100644 --- a/content/published/patterns/core-concepts/comparing-data-by-value-with-structural-equality.mdx +++ b/content/published/patterns/core-concepts/comparing-data-by-value-with-structural-equality.mdx @@ -4,7 +4,7 @@ id: comparing-data-by-value-with-structural-equality skillLevel: beginner applicationPatternId: core-concepts summary: >- - Use Data.struct and Equal.equals to safely compare objects by their value + Use plain objects and Equal.equals to safely compare objects by their value instead of their reference, avoiding common JavaScript pitfalls. tags: - equality @@ -15,7 +15,7 @@ tags: - structural-equality rule: description: >- - Use Data.struct or implement the Equal interface for value-based comparison + Use plain objects or implement the Equal interface for value-based comparison of objects and classes. related: - high-performance-collections-with-chunk @@ -28,7 +28,7 @@ lessonOrder: 5 To compare objects or classes by their contents rather than by their memory reference, use one of two methods: -1. **For plain data objects:** Define them with `Data.struct`. +1. **For plain data objects:** Use plain objects β€” `Equal.equals` compares them structurally by default in Effect v4. 2. **For classes:** Extend `Data.Class` or implement the `Equal.Equal` interface. Then, compare instances using the `Equal.equals(a, b)` function. @@ -43,16 +43,16 @@ In JavaScript, comparing two non-primitive values with `===` checks for _referen { a: 1 } === { a: 1 } // false! ``` -Effect solves this with **structural equality**. All of Effect's built-in data structures (`Option`, `Either`, `Chunk`, etc.) can be compared by their structure and values. By using helpers like `Data.struct`, you can easily give your own data structures this same powerful and predictable behavior. +Effect solves this with **structural equality**. All of Effect's built-in data structures (`Option`, `Result`, `Chunk`, etc.) can be compared by their structure and values. Plain objects and arrays are compared structurally by default in Effect v4, giving your own data structures this same powerful and predictable behavior. --- ## Good Example -We define two points using `Data.struct`. Even though `p1` and `p2` are different instances in memory, `Equal.equals` correctly reports them as equal because their contents match. +We define two points as plain objects. Even though `p1` and `p2` are different instances in memory, `Equal.equals` correctly reports them as equal because their contents match. ```typescript -import { Data, Equal, Effect } from "effect"; +import { Equal, Effect } from "effect"; // Define a Point type with structural equality interface Point { @@ -61,7 +61,7 @@ interface Point { readonly y: number; } -const Point = Data.tagged("Point"); +const Point = (args: Omit): Point => ({ _tag: "Point", ...args }); // Create a program to demonstrate structural equality const program = Effect.gen(function* () { diff --git a/content/published/patterns/core-concepts/constructor-fail-none-left.mdx b/content/published/patterns/core-concepts/constructor-fail-none-left.mdx index b59f180f..9cc7e6c2 100644 --- a/content/published/patterns/core-concepts/constructor-fail-none-left.mdx +++ b/content/published/patterns/core-concepts/constructor-fail-none-left.mdx @@ -5,7 +5,7 @@ skillLevel: beginner applicationPatternId: core-concepts summary: >- Use fail, none, and left to represent errors or absence in Effect, Option, or - Either, making failures explicit and type-safe. + Result, making failures explicit and type-safe. tags: - fail - none @@ -13,12 +13,12 @@ tags: - constructor - effect - option - - either + - result - error - absence rule: description: >- - Use fail, none, and left to create Effect, Option, or Either that represent + Use fail, none, and left to create Effect, Option, or Result that represent failure or absence. related: - constructor-succeed-some-right @@ -31,7 +31,7 @@ lessonOrder: 14 ## Guideline -Use the `fail`, `none`, and `left` constructors to represent errors or absence in the Effect, Option, or Either world. +Use the `fail`, `none`, and `left` constructors to represent errors or absence in the Effect, Option, or Result world. This makes failures explicit, type-safe, and composable. ## Rationale @@ -42,7 +42,7 @@ This leads to more robust and maintainable code. ## Good Example ```typescript -import { Effect, Option, Either } from "effect"; +import { Effect, Option, Result } from "effect"; // Effect: Represent a failure with an error value const effect = Effect.fail("Something went wrong"); // Effect @@ -50,17 +50,17 @@ const effect = Effect.fail("Something went wrong"); // Effect -// Either: Represent a failure with a left value -const either = Either.left("Invalid input"); // Either +// Result: Represent a failure with a left value +const either = Result.fail("Invalid input"); // Result ``` **Explanation:** - `Effect.fail(error)` creates an effect that always fails with `error`. - `Option.none()` creates an option that is always absent. -- `Either.left(error)` creates an either that always represents failure. +- `Result.fail(error)` creates an either that always represents failure. ## Anti-Pattern -Throwing exceptions, returning `null` or `undefined`, or using error codes outside the Effect, Option, or Either world. +Throwing exceptions, returning `null` or `undefined`, or using error codes outside the Effect, Option, or Result world. This makes error handling ad hoc, less type-safe, and harder to compose. diff --git a/content/published/patterns/core-concepts/constructor-from-nullable-option-either.mdx b/content/published/patterns/core-concepts/constructor-from-nullable-option-either.mdx index 4a20afb5..e573545d 100644 --- a/content/published/patterns/core-concepts/constructor-from-nullable-option-either.mdx +++ b/content/published/patterns/core-concepts/constructor-from-nullable-option-either.mdx @@ -1,27 +1,27 @@ --- -title: 'Converting from Nullable, Option, or Either' +title: 'Converting from Nullable, Option, or Result' id: constructor-from-nullable-option-either skillLevel: beginner applicationPatternId: core-concepts summary: >- - Use fromNullable, fromOption, and fromEither to convert nullable values, - Option, or Either into Effects or Streams, enabling safe and composable + Use fromNullishOr, fromOption, and fromResult to convert nullable values, + Option, or Result into Effects or Streams, enabling safe and composable interop. tags: - - fromNullable + - fromNullishOr - fromOption - - fromEither + - fromResult - constructor - effect - stream - option - - either + - result - interop - conversion rule: description: >- - Use fromNullable, fromOption, and fromEither to lift nullable values, - Option, or Either into Effects or Streams for safe, typeful interop. + Use fromNullishOr, fromOption, and fromResult to lift nullable values, + Option, or Result into Effects or Streams for safe, typeful interop. related: - constructor-succeed-some-right - constructor-fail-none-left @@ -29,25 +29,25 @@ author: PaulJPhilp lessonOrder: 7 --- -# Converting from Nullable, Option, or Either +# Converting from Nullable, Option, or Result ## Guideline -Use the `fromNullable`, `fromOption`, and `fromEither` constructors to convert nullable values, `Option`, or `Either` into Effects or Streams. +Use the `fromNullishOr`, `fromOption`, and `fromResult` constructors to convert nullable values, `Option`, or `Result` into Effects or Streams. This enables safe, typeful interop with legacy code, APIs, or libraries that use `null`, `undefined`, or their own option/either types. ## Rationale -Converting to Effect, Stream, Option, or Either lets you use all the combinators, error handling, and resource safety of the Effect ecosystem, while avoiding the pitfalls of `null` and `undefined`. +Converting to Effect, Stream, Option, or Result lets you use all the combinators, error handling, and resource safety of the Effect ecosystem, while avoiding the pitfalls of `null` and `undefined`. ## Good Example ```typescript -import { Effect, Option, Either } from "effect"; +import { Effect, Option, Result } from "effect"; // Option: Convert a nullable value to an Option const nullableValue: string | null = Math.random() > 0.5 ? "hello" : null; -const option = Option.fromNullable(nullableValue); // Option +const option = Option.fromNullishOr(nullableValue); // Option // Effect: Convert an Option to an Effect that may fail const someValue = Option.some(42); @@ -56,20 +56,20 @@ const effectFromOption = Option.match(someValue, { onSome: (value) => Effect.succeed(value), }); // Effect -// Effect: Convert an Either to an Effect -const either = Either.right("success"); -const effectFromEither = Either.match(either, { - onLeft: (error) => Effect.fail(error), - onRight: (value) => Effect.succeed(value), +// Effect: Convert a Result to an Effect +const result = Result.succeed("success"); +const effectFromResult = Result.match(result, { + onFailure: (error) => Effect.fail(error), + onSuccess: (value) => Effect.succeed(value), }); // Effect ``` **Explanation:** -- `Effect.fromNullable` lifts a nullable value into an Effect, failing if the value is `null` or `undefined`. +- `Effect.fromNullishOr` lifts a nullable value into an Effect, failing if the value is `null` or `undefined`. - `Effect.fromOption` lifts an Option into an Effect, failing if the Option is `none`. -- `Effect.fromEither` lifts an Either into an Effect, failing if the Either is `left`. +- `Effect.fromResult` lifts a Result into an Effect, failing if the Result is `Failure`. ## Anti-Pattern -Passing around `null`, `undefined`, or custom option/either types without converting them, which leads to unsafe, non-composable code and harder error handling. +Passing around `null`, `undefined`, or custom option/result types without converting them, which leads to unsafe, non-composable code and harder error handling. diff --git a/content/published/patterns/core-concepts/constructor-succeed-some-right.mdx b/content/published/patterns/core-concepts/constructor-succeed-some-right.mdx index bb6214d9..4e791ebb 100644 --- a/content/published/patterns/core-concepts/constructor-succeed-some-right.mdx +++ b/content/published/patterns/core-concepts/constructor-succeed-some-right.mdx @@ -5,7 +5,7 @@ skillLevel: beginner applicationPatternId: core-concepts summary: >- Use succeed, some, and right to lift plain values into Effect, Option, or - Either, making them composable and type-safe. + Result, making them composable and type-safe. tags: - succeed - some @@ -13,11 +13,11 @@ tags: - constructor - effect - option - - either + - result - lifting rule: description: >- - Use succeed, some, and right to create Effect, Option, or Either from plain + Use succeed, some, and right to create Effect, Option, or Result from plain values. related: - constructor-fail-none-left @@ -30,7 +30,7 @@ lessonOrder: 15 ## Guideline -Use the `succeed`, `some`, and `right` constructors to lift plain values into the Effect, Option, or Either world. +Use the `succeed`, `some`, and `right` constructors to lift plain values into the Effect, Option, or Result world. This is the foundation for building composable, type-safe programs. ## Rationale @@ -40,7 +40,7 @@ Lifting values into these structures allows you to compose them with other effec ## Good Example ```typescript -import { Effect, Option, Either } from "effect"; +import { Effect, Option, Result } from "effect"; // Effect: Lift a value into an Effect that always succeeds const effect = Effect.succeed(42); // Effect @@ -48,17 +48,17 @@ const effect = Effect.succeed(42); // Effect // Option: Lift a value into an Option that is always Some const option = Option.some("hello"); // Option -// Either: Lift a value into an Either that is always Right -const either = Either.right({ id: 1 }); // Either +// Result: Lift a value into an Result that is always Right +const either = Result.succeed({ id: 1 }); // Result<{ id: number }, never> ``` **Explanation:** - `Effect.succeed(value)` creates an effect that always succeeds with `value`. - `Option.some(value)` creates an option that is always present. -- `Either.right(value)` creates an either that always represents success. +- `Result.succeed(value)` creates an either that always represents success. ## Anti-Pattern -Passing plain values around outside the Effect, Option, or Either world, or using `null`/`undefined` to represent absence or success. +Passing plain values around outside the Effect, Option, or Result world, or using `null`/`undefined` to represent absence or success. This leads to less composable, less type-safe code and makes error handling harder. diff --git a/content/published/patterns/core-concepts/constructor-sync-async.mdx b/content/published/patterns/core-concepts/constructor-sync-async.mdx index c4b3baf3..85235942 100644 --- a/content/published/patterns/core-concepts/constructor-sync-async.mdx +++ b/content/published/patterns/core-concepts/constructor-sync-async.mdx @@ -53,7 +53,7 @@ function legacyReadFile( setTimeout(() => cb(null, "file contents"), 10); } -const effectAsync = Effect.async((resume) => { +const effectAsync = Effect.callback((resume) => { legacyReadFile("file.txt", (err, data) => { if (err) resume(Effect.fail(err)); else resume(Effect.succeed(data!)); @@ -64,7 +64,7 @@ const effectAsync = Effect.async((resume) => { **Explanation:** - `Effect.sync` is for synchronous computations that are guaranteed not to throw. -- `Effect.async` is for integrating callback-based APIs, converting them into Effects. +- `Effect.callback` is for integrating callback-based APIs, converting them into Effects. ## Anti-Pattern diff --git a/content/published/patterns/core-concepts/control-flow-with-combinators.mdx b/content/published/patterns/core-concepts/control-flow-with-combinators.mdx index 5ffadae9..0e0f2c74 100644 --- a/content/published/patterns/core-concepts/control-flow-with-combinators.mdx +++ b/content/published/patterns/core-concepts/control-flow-with-combinators.mdx @@ -47,20 +47,20 @@ const attemptAdminAction = (user: { isAdmin: boolean }) => const program = Effect.gen(function* () { // Try with admin user yield* Effect.logInfo("\nTrying with admin user..."); - const adminResult = yield* Effect.either( + const adminResult = yield* Effect.result( attemptAdminAction({ isAdmin: true }) ); yield* Effect.logInfo( - `Admin result: ${adminResult._tag === "Right" ? adminResult.right : adminResult.left}` + `Admin result: ${adminResult._tag === "Success" ? adminResult.success : adminResult.failure}` ); // Try with non-admin user yield* Effect.logInfo("\nTrying with non-admin user..."); - const userResult = yield* Effect.either( + const userResult = yield* Effect.result( attemptAdminAction({ isAdmin: false }) ); yield* Effect.logInfo( - `User result: ${userResult._tag === "Right" ? userResult.right : userResult.left}` + `User result: ${userResult._tag === "Success" ? userResult.success : userResult.failure}` ); }); diff --git a/content/published/patterns/core-concepts/create-reusable-runtime-from-layers.mdx b/content/published/patterns/core-concepts/create-reusable-runtime-from-layers.mdx index 1ef31ca0..f75cb0f7 100644 --- a/content/published/patterns/core-concepts/create-reusable-runtime-from-layers.mdx +++ b/content/published/patterns/core-concepts/create-reusable-runtime-from-layers.mdx @@ -38,16 +38,18 @@ long-running applications. ## Good Example ```typescript -import { Effect, Layer, Runtime } from "effect"; +import { Effect, Layer, Runtime, Context } from "effect"; -class GreeterService extends Effect.Service()("Greeter", { - sync: () => ({ +class GreeterService extends Context.Service()("Greeter", { + make: Effect.sync(() => ({ greet: (name: string) => Effect.sync(() => `Hello ${name}`), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} const runtime = Effect.runSync( - Layer.toRuntime(GreeterService.Default).pipe(Effect.scoped) + Layer.toRuntime(GreeterService.layer).pipe(Effect.scoped) ); // In a server, you would reuse `run` for every request. diff --git a/content/published/patterns/core-concepts/data-array.mdx b/content/published/patterns/core-concepts/data-array.mdx index 0b156806..3db95848 100644 --- a/content/published/patterns/core-concepts/data-array.mdx +++ b/content/published/patterns/core-concepts/data-array.mdx @@ -1,13 +1,12 @@ --- -title: Working with Immutable Arrays using Data.array +title: Working with Immutable Arrays and Structural Equality id: data-array skillLevel: beginner applicationPatternId: core-concepts summary: >- - Use Data.array to create immutable, type-safe arrays that support value-based + Use readonly arrays to create immutable, type-safe collections that support value-based equality and safe functional operations. tags: - - Data.array - array - structural-equality - immutable @@ -15,7 +14,7 @@ tags: - effect rule: description: >- - Use Data.array to define arrays whose equality is based on their contents, + Use readonly arrays to define collections whose equality is based on their contents, enabling safe, predictable comparisons and functional operations. related: - use-chunk-for-high-performance-collections @@ -25,26 +24,26 @@ author: PaulJPhilp lessonOrder: 24 --- -# Working with Immutable Arrays using `Data.array` +# Working with Immutable Arrays and Structural Equality ## Guideline -Use `Data.array` to create immutable, type-safe arrays that support value-based equality and safe functional operations. +Use readonly arrays to create immutable, type-safe collections that support value-based equality and safe functional operations. This is useful for modeling ordered collections where immutability and structural equality are important. ## Rationale JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic and concurrent code. -`Data.array` provides immutable arrays with structural equality, making them ideal for functional programming and safe domain modeling. +`Equal.equals` compares plain arrays structurally in Effect v4, making them ideal for functional programming and safe domain modeling. ## Good Example ```typescript -import { Data, Equal } from "effect"; +import { Equal } from "effect"; // Create two structurally equal arrays -const arr1 = Data.array([1, 2, 3]); -const arr2 = Data.array([1, 2, 3]); +const arr1 = [1, 2, 3] as const; +const arr2 = [1, 2, 3] as const; // Compare by value, not reference const areEqual = Equal.equals(arr1, arr2); // true @@ -55,15 +54,15 @@ const set = HashSet.make(arr1); console.log(HashSet.has(set, arr2)); // true // Functional operations (map, filter, etc.) -const doubled = arr1.map((n) => n * 2); // Data.array([2, 4, 6]) +const doubled = arr1.map((n) => n * 2); // [2, 4, 6] ``` **Explanation:** -- `Data.array` creates immutable arrays with value-based equality. +- Plain readonly arrays have value-based equality with `Equal.equals` in Effect v4. - Useful for modeling ordered collections in a safe, functional way. - Supports all standard array operations, but with immutability and structural equality. ## Anti-Pattern -Using plain JavaScript arrays for value-based logic, as keys in sets/maps, or in concurrent code, which can lead to bugs due to mutability and reference-based comparison. +Mutating shared arrays in concurrent code. Prefer readonly arrays (or `Chunk` for high-performance collections) and compare with `Equal.equals` instead of `===`. diff --git a/content/published/patterns/core-concepts/data-case.mdx b/content/published/patterns/core-concepts/data-case.mdx index 774b4329..ba2feb78 100644 --- a/content/published/patterns/core-concepts/data-case.mdx +++ b/content/published/patterns/core-concepts/data-case.mdx @@ -1,13 +1,13 @@ --- -title: Modeling Tagged Unions with Data.case +title: Modeling Tagged Unions with Data.taggedEnum id: data-case skillLevel: intermediate applicationPatternId: core-concepts summary: >- - Use Data.case to create tagged unions (algebraic data types) for robust, + Use Data.taggedEnum to create tagged unions (algebraic data types) for robust, type-safe domain modeling and pattern matching. tags: - - Data.case + - Data.taggedEnum - tagged-union - ADT - domain-modeling @@ -16,7 +16,7 @@ tags: - effect rule: description: >- - Use Data.case to define tagged unions (ADTs) for modeling domain-specific + Use Data.taggedEnum to define tagged unions (ADTs) for modeling domain-specific states and enabling exhaustive pattern matching. related: - data-struct @@ -25,17 +25,17 @@ author: PaulJPhilp lessonOrder: 9 --- -# Modeling Tagged Unions with `Data.case` +# Modeling Tagged Unions with `Data.taggedEnum` ## Guideline -Use `Data.case` to create tagged unions (algebraic data types, or ADTs) for robust, type-safe domain modeling. +Use `Data.taggedEnum` to create tagged unions (algebraic data types, or ADTs) for robust, type-safe domain modeling. Tagged unions make it easy to represent and exhaustively handle all possible states of your domain entities. ## Rationale Modeling domain logic with tagged unions ensures that all cases are handled, prevents illegal states, and enables safe, exhaustive pattern matching. -`Data.case` provides a concise, type-safe way to define and use ADTs in your application. +`Data.taggedEnum` provides a concise, type-safe way to define and use ADTs in your application. ## Good Example @@ -70,7 +70,7 @@ function handleState(state: State): string { **Explanation:** -- `Data.case` creates tagged constructors for each state. +- `Data.taggedEnum` creates tagged constructors for each state. - The `_tag` property enables exhaustive pattern matching. - Use for domain modeling, state machines, and error types. diff --git a/content/published/patterns/core-concepts/data-cause.mdx b/content/published/patterns/core-concepts/data-cause.mdx index 8ea210bd..af9c96bd 100644 --- a/content/published/patterns/core-concepts/data-cause.mdx +++ b/content/published/patterns/core-concepts/data-cause.mdx @@ -51,13 +51,13 @@ const program = Effect.try({ // Catch all causes and inspect them const handled = program.pipe( - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.sync(() => { - if (Cause.isDie(cause)) { + if (Cause.hasDies(cause)) { console.error("Defect (die):", Cause.pretty(cause)); - } else if (Cause.isFailure(cause)) { + } else if (Cause.hasFails(cause)) { console.error("Expected error:", Cause.pretty(cause)); - } else if (Cause.isInterrupted(cause)) { + } else if (Cause.hasInterrupts(cause)) { console.error("Interrupted:", Cause.pretty(cause)); } // Handle or rethrow as needed diff --git a/content/published/patterns/core-concepts/data-class.mdx b/content/published/patterns/core-concepts/data-class.mdx index 05550fb1..0930edd0 100644 --- a/content/published/patterns/core-concepts/data-class.mdx +++ b/content/published/patterns/core-concepts/data-class.mdx @@ -44,15 +44,15 @@ This is essential for using your types in sets, maps, and for sorting or dedupli import { Data, Equal, HashSet } from "effect"; // Define custom data types with structural equality -const user1 = Data.struct({ id: 1, name: "Alice" }); -const user2 = Data.struct({ id: 1, name: "Alice" }); -const user3 = Data.struct({ id: 2, name: "Bob" }); +const user1 = { id: 1, name: "Alice" }; +const user2 = { id: 1, name: "Alice" }; +const user3 = { id: 2, name: "Bob" }; -// Data.struct provides automatic structural equality +// Plain objects have automatic structural equality in Effect v4 console.log(Equal.equals(user1, user2)); // true (same structure) console.log(Equal.equals(user1, user3)); // false (different values) -// Use in a HashSet (works because Data.struct implements Equal) +// Use in a HashSet (works because plain objects have structural equality in Effect v4) const set = HashSet.make(user1); console.log(HashSet.has(set, user2)); // true (structural equality) diff --git a/content/published/patterns/core-concepts/data-either.mdx b/content/published/patterns/core-concepts/data-either.mdx index a742ca6a..61e43383 100644 --- a/content/published/patterns/core-concepts/data-either.mdx +++ b/content/published/patterns/core-concepts/data-either.mdx @@ -1,20 +1,20 @@ --- -title: Understand the Either Data Type +title: Understand the Result Data Type id: data-either skillLevel: beginner applicationPatternId: core-concepts summary: >- - Use Either to represent computations that can fail, allowing you to + Use Result to represent computations that can fail, allowing you to accumulate multiple errors instead of short-circuiting on the first one. tags: - - Either + - Result - error-handling - data-type - domain - effect rule: description: >- - Use Either to model computations that may fail, making errors explicit and + Use Result to model computations that may fail, making errors explicit and type-safe. related: - data-option @@ -23,50 +23,50 @@ author: PaulJPhilp lessonOrder: 1 --- -# Accumulate Multiple Errors with `Either` +# Accumulate Multiple Errors with `Result` ## Guideline -Use the `Either` data type to represent computations that can fail (`Left`) or succeed (`Right`). +Use the `Result` data type to represent computations that can fail (`Left`) or succeed (`Right`). This makes error handling explicit, type-safe, and composable. ## Rationale -`Either` is a foundational data type for error handling in functional programming. +`Result` is a foundational data type for error handling in functional programming. It allows you to accumulate errors, model domain-specific failures, and avoid exceptions and unchecked errors. ## Good Example ```typescript -import { Either } from "effect"; +import { Result } from "effect"; // Create a Right (success) or Left (failure) -const success = Either.right(42); // Either -const failure = Either.left("Something went wrong"); // Either +const success = Result.succeed(42); // Result +const failure = Result.fail("Something went wrong"); // Result -// Pattern match on Either +// Pattern match on Result const result = success.pipe( - Either.match({ - onLeft: (err) => `Error: ${err}`, - onRight: (value) => `Value: ${value}`, + Result.match({ + onFailure: (err) => `Error: ${err}`, + onSuccess: (value) => `Value: ${value}`, }) ); // string -// Combine multiple Eithers and accumulate errors -const e1 = Either.right(1); -const e2 = Either.left("fail1"); -const e3 = Either.left("fail2"); +// Combine multiple Results and accumulate errors +const e1 = Result.succeed(1); +const e2 = Result.fail("fail1"); +const e3 = Result.fail("fail2"); -const all = [e1, e2, e3].filter(Either.isRight).map(Either.getRight); // [1] -const errors = [e1, e2, e3].filter(Either.isLeft).map(Either.getLeft); // ["fail1", "fail2"] +const all = [e1, e2, e3].filter(Result.isSuccess).map(Result.getSuccess); // [1] +const errors = [e1, e2, e3].filter(Result.isFailure).map(Result.getFailure); // ["fail1", "fail2"] ``` **Explanation:** -- `Either.right(value)` represents success. -- `Either.left(error)` represents failure. +- `Result.succeed(value)` represents success. +- `Result.fail(error)` represents failure. - Pattern matching ensures all cases are handled. -- You can accumulate errors or results from multiple Eithers. +- You can accumulate errors or results from multiple Results. ## Anti-Pattern diff --git a/content/published/patterns/core-concepts/data-option.mdx b/content/published/patterns/core-concepts/data-option.mdx index 846ab377..3e125476 100644 --- a/content/published/patterns/core-concepts/data-option.mdx +++ b/content/published/patterns/core-concepts/data-option.mdx @@ -45,7 +45,7 @@ const someValue = Option.some(42); // Option const noValue = Option.none(); // Option // Safely convert a nullable value to Option -const fromNullable = Option.fromNullable(Math.random() > 0.5 ? "hello" : null); // Option +const fromNullishOr = Option.fromNullishOr(Math.random() > 0.5 ? "hello" : null); // Option // Pattern match on Option const result = someValue.pipe( @@ -65,7 +65,7 @@ function findUser(id: number): Option.Option<{ id: number; name: string }> { - `Option.some(value)` represents a present value. - `Option.none()` represents absence. -- `Option.fromNullable` safely lifts nullable values into Option. +- `Option.fromNullishOr` safely lifts nullable values into Option. - Pattern matching ensures all cases are handled. ## Anti-Pattern diff --git a/content/published/patterns/core-concepts/data-struct.mdx b/content/published/patterns/core-concepts/data-struct.mdx index 332a2316..2895c31c 100644 --- a/content/published/patterns/core-concepts/data-struct.mdx +++ b/content/published/patterns/core-concepts/data-struct.mdx @@ -1,20 +1,19 @@ --- -title: Comparing Data by Value with Data.struct +title: Comparing Data by Value with Structural Equality id: data-struct skillLevel: beginner applicationPatternId: core-concepts summary: >- - Use Data.struct to create immutable, structurally-typed objects that can be + Use plain objects to create immutable, structurally-typed values that can be compared by value, not by reference. tags: - - Data.struct - structural-equality - immutable - data-type - effect rule: description: >- - Use Data.struct to define objects whose equality is based on their contents, + Use plain objects to define values whose equality is based on their contents, enabling safe and predictable comparisons. related: - data-tuple @@ -23,26 +22,26 @@ author: PaulJPhilp lessonOrder: 4 --- -# Comparing Data by Value with `Data.struct` +# Comparing Data by Value with Structural Equality ## Guideline -Use `Data.struct` to create immutable, structurally-typed objects whose equality is based on their contents, not their reference. +Use plain objects to create immutable, structurally-typed values whose equality is based on their contents, not their reference. This enables safe, predictable comparisons and is ideal for domain modeling. ## Rationale -JavaScript objects are compared by reference, which can lead to subtle bugs when modeling value objects. -`Data.struct` ensures that two objects with the same contents are considered equal, supporting value-based logic and collections. +JavaScript objects are compared by reference, which can lead to subtle bugs when modeling value objects. +In Effect v4, `Equal.equals` performs structural comparison on plain objects by default, so two objects with the same contents are considered equal, supporting value-based logic and collections. ## Good Example ```typescript -import { Data, Equal } from "effect"; +import { Equal } from "effect"; // Create two structurally equal objects -const user1 = Data.struct({ id: 1, name: "Alice" }); -const user2 = Data.struct({ id: 1, name: "Alice" }); +const user1 = { id: 1, name: "Alice" }; +const user2 = { id: 1, name: "Alice" }; // Compare by value, not reference const areEqual = Equal.equals(user1, user2); // true @@ -55,7 +54,7 @@ console.log(HashSet.has(set, user2)); // true **Explanation:** -- `Data.struct` creates immutable objects with value-based equality. +- Plain objects have value-based equality with `Equal.equals` in Effect v4. - Use for domain entities, value objects, and when storing objects in sets or as map keys. - Avoids bugs from reference-based comparison. diff --git a/content/published/patterns/core-concepts/data-tuple.mdx b/content/published/patterns/core-concepts/data-tuple.mdx index ed4ee2f2..349782c9 100644 --- a/content/published/patterns/core-concepts/data-tuple.mdx +++ b/content/published/patterns/core-concepts/data-tuple.mdx @@ -1,13 +1,12 @@ --- -title: Working with Tuples using Data.tuple +title: Working with Tuples and Structural Equality id: data-tuple skillLevel: beginner applicationPatternId: core-concepts summary: >- - Use Data.tuple to create immutable, type-safe tuples that support value-based + Use plain tuples to create immutable, type-safe values that support value-based equality and pattern matching. tags: - - Data.tuple - tuple - structural-equality - immutable @@ -15,7 +14,7 @@ tags: - effect rule: description: >- - Use Data.tuple to define tuples whose equality is based on their contents, + Use plain tuples to define values whose equality is based on their contents, enabling safe and predictable comparisons and pattern matching. related: - data-struct @@ -24,26 +23,26 @@ author: PaulJPhilp lessonOrder: 25 --- -# Working with Tuples using `Data.tuple` +# Working with Tuples and Structural Equality ## Guideline -Use `Data.tuple` to create immutable, type-safe tuples that support value-based equality and pattern matching. +Use plain tuples to create immutable, type-safe values that support value-based equality and pattern matching. This is useful for modeling fixed-size, heterogeneous collections of values in a safe and expressive way. ## Rationale -JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic. -`Data.tuple` provides immutable tuples with structural equality, making them ideal for domain modeling and functional programming patterns. +JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic. +In Effect v4, `Equal.equals` compares plain tuples structurally, making them ideal for domain modeling and functional programming patterns. ## Good Example ```typescript -import { Data, Equal } from "effect"; +import { Equal } from "effect"; // Create two structurally equal tuples -const t1 = Data.tuple(1, "Alice"); -const t2 = Data.tuple(1, "Alice"); +const t1 = [1, "Alice"] as const; +const t2 = [1, "Alice"] as const; // Compare by value, not reference const areEqual = Equal.equals(t1, t2); // true @@ -59,10 +58,10 @@ const [id, name] = t1; // id: number, name: string **Explanation:** -- `Data.tuple` creates immutable tuples with value-based equality. +- Plain tuples have value-based equality with `Equal.equals` in Effect v4. - Useful for modeling pairs, coordinates, or any fixed-size, heterogeneous data. - Supports safe pattern matching and collection operations. ## Anti-Pattern -Using plain arrays for value-based logic or as keys in sets/maps, which compares by reference and can lead to incorrect behavior. +Prefer `as const` tuples or readonly arrays for value-based logic. Reference comparison (`===`) on distinct instances will not behave as expected. diff --git a/content/published/patterns/core-concepts/execute-with-runsync.mdx b/content/published/patterns/core-concepts/execute-with-runsync.mdx index eacfb81a..e94861b4 100644 --- a/content/published/patterns/core-concepts/execute-with-runsync.mdx +++ b/content/published/patterns/core-concepts/execute-with-runsync.mdx @@ -83,7 +83,7 @@ const program3 = Effect.gen(function* () { }, }); }).pipe( - Effect.catchAll((error) => Effect.logInfo(`Error occurred: ${error.message}`)) + Effect.catch((error) => Effect.logInfo(`Error occurred: ${error.message}`)) ); // Run with error handling diff --git a/content/published/patterns/core-concepts/optional-pattern-optional-chains.mdx b/content/published/patterns/core-concepts/optional-pattern-optional-chains.mdx index bfed4472..59187373 100644 --- a/content/published/patterns/core-concepts/optional-pattern-optional-chains.mdx +++ b/content/published/patterns/core-concepts/optional-pattern-optional-chains.mdx @@ -404,9 +404,9 @@ const complexChain = pipe( // Or with effect.gen syntax const withGen = Effect.gen(function* () { - const user = yield* Option.fromNullable(findUser("user-42")); - const profile = yield* Option.fromNullable(getProfile(user.id)); - const settings = yield* Option.fromNullable(getSettings(user.id)); + const user = yield* Option.fromNullishOr(findUser("user-42")); + const profile = yield* Option.fromNullishOr(getProfile(user.id)); + const settings = yield* Option.fromNullishOr(getSettings(user.id)); return { user, profile, settings }; }); diff --git a/content/published/patterns/core-concepts/process-streaming-data-with-stream.mdx b/content/published/patterns/core-concepts/process-streaming-data-with-stream.mdx index 9b49fa5a..5489e721 100644 --- a/content/published/patterns/core-concepts/process-streaming-data-with-stream.mdx +++ b/content/published/patterns/core-concepts/process-streaming-data-with-stream.mdx @@ -80,7 +80,7 @@ const userStream: Stream.Stream = Stream.paginateEffect( fetchUserPage(page).pipe( Effect.map( (response) => - [response.users, Option.fromNullable(response.nextPage)] as const + [response.users, Option.fromNullishOr(response.nextPage)] as const ) ) ).pipe( @@ -95,7 +95,7 @@ const program = Stream.runForEach(userStream, (user: User) => ); const programWithErrorHandling = program.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Stream processing error: ${error}`); return null; diff --git a/content/published/patterns/core-concepts/provide-config-layer.mdx b/content/published/patterns/core-concepts/provide-config-layer.mdx index e5185652..76eab267 100644 --- a/content/published/patterns/core-concepts/provide-config-layer.mdx +++ b/content/published/patterns/core-concepts/provide-config-layer.mdx @@ -33,13 +33,15 @@ Integrating configuration as a `Layer` plugs it directly into Effect's dependenc ## Good Example ```typescript -import { Effect, Layer } from "effect"; +import { Effect, Layer, Context } from "effect"; -class ServerConfig extends Effect.Service()("ServerConfig", { - sync: () => ({ +class ServerConfig extends Context.Service()("ServerConfig", { + make: Effect.sync(() => ({ port: process.env.PORT ? parseInt(process.env.PORT) : 8080, - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} const program = Effect.gen(function* () { const config = yield* ServerConfig; @@ -48,9 +50,9 @@ const program = Effect.gen(function* () { const programWithErrorHandling = Effect.provide( program, - ServerConfig.Default + ServerConfig.layer ).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Program error: ${error}`); return null; diff --git a/content/published/patterns/core-concepts/representing-time-spans-with-duration.mdx b/content/published/patterns/core-concepts/representing-time-spans-with-duration.mdx index 69e31486..43f3363d 100644 --- a/content/published/patterns/core-concepts/representing-time-spans-with-duration.mdx +++ b/content/published/patterns/core-concepts/representing-time-spans-with-duration.mdx @@ -56,7 +56,7 @@ const program = Effect.log("Starting...").pipe( ); // Durations can also be compared -const isLonger = Duration.greaterThan(fiveSeconds, oneHundredMillis); // true +const isLonger = Duration.isGreaterThan(fiveSeconds, oneHundredMillis); // true // Demonstrate the duration functionality const demonstration = Effect.gen(function* () { @@ -83,7 +83,7 @@ const demonstration = Effect.gen(function* () { const oneMinute = Duration.minutes(1); yield* Effect.logInfo(`One minute: ${Duration.toMillis(oneMinute)}ms`); - const isMinuteLonger = Duration.greaterThan(oneMinute, fiveSeconds); + const isMinuteLonger = Duration.isGreaterThan(oneMinute, fiveSeconds); yield* Effect.logInfo(`Is 1 minute longer than 5 seconds? ${isMinuteLonger}`); }); diff --git a/content/published/patterns/core-concepts/solve-promise-problems-with-effect.mdx b/content/published/patterns/core-concepts/solve-promise-problems-with-effect.mdx index a5147197..ea98e094 100644 --- a/content/published/patterns/core-concepts/solve-promise-problems-with-effect.mdx +++ b/content/published/patterns/core-concepts/solve-promise-problems-with-effect.mdx @@ -51,7 +51,7 @@ Understanding that Effect was built specifically to solve these problems is key This code is type-safe, testable, and cancellable. The signature `Effect.Effect` tells us everything we need to know. ```typescript -import { Effect, Data } from "effect"; +import { Effect, Data, Context, Layer } from "effect"; interface DbErrorType { readonly _tag: "DbError"; @@ -64,15 +64,17 @@ interface User { name: string; } -class HttpClient extends Effect.Service()("HttpClient", { - sync: () => ({ +class HttpClient extends Context.Service()("HttpClient", { + make: Effect.sync(() => ({ findById: (id: number): Effect.Effect => Effect.try({ try: () => ({ name: `User ${id}` }), catch: () => DbError({ message: "Failed to find user" }), }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} const findUser = (id: number) => Effect.gen(function* () { @@ -88,7 +90,7 @@ const program = Effect.gen(function* () { yield* Effect.logInfo("1. Demonstrating type-safe error handling:"); const result1 = yield* findUser(123).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logInfo(`Handled error: ${error.message}`); return { name: "Default User" }; @@ -129,7 +131,7 @@ const program = Effect.gen(function* () { yield* Effect.logInfo("\nβœ… All operations completed successfully!"); }); -Effect.runPromise(Effect.provide(program, HttpClient.Default)); +Effect.runPromise(Effect.provide(program, HttpClient.layer)); ``` --- diff --git a/content/published/patterns/core-concepts/understand-effect-channels.mdx b/content/published/patterns/core-concepts/understand-effect-channels.mdx index 03742502..6d89dc6b 100644 --- a/content/published/patterns/core-concepts/understand-effect-channels.mdx +++ b/content/published/patterns/core-concepts/understand-effect-channels.mdx @@ -56,7 +56,7 @@ This turns the TypeScript compiler into a powerful assistant that ensures you've This function signature is a self-documenting contract. It clearly states that to get a `User`, you must provide a `Database` service, and the operation might fail with a `UserNotFoundError`. ```typescript -import { Effect, Data } from "effect"; +import { Effect, Data, Context, Layer } from "effect"; // Define the types for our channels interface User { @@ -64,16 +64,18 @@ interface User { } // The 'A' type class UserNotFoundError extends Data.TaggedError("UserNotFoundError") {} // The 'E' type -// Define the Database service using Effect.Service -export class Database extends Effect.Service()("Database", { +// Define the Database service using Context.Service +export class Database extends Context.Service()("Database", { // Provide a default implementation - sync: () => ({ + make: Effect.sync(() => ({ findUser: (id: number) => id === 1 ? Effect.succeed({ name: "Paul" }) : Effect.fail(new UserNotFoundError()), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // This function's signature shows all three channels const getUser = ( @@ -89,7 +91,7 @@ const program = getUser(1); // Run the program with the default implementation const programWithLogging = Effect.gen(function* () { - const result = yield* Effect.provide(program, Database.Default); + const result = yield* Effect.provide(program, Database.layer); yield* Effect.log(`Result: ${JSON.stringify(result)}`); // { name: 'Paul' } return result; }); diff --git a/content/published/patterns/core-concepts/understand-layers-for-dependency-injection.mdx b/content/published/patterns/core-concepts/understand-layers-for-dependency-injection.mdx index 9bbadcad..054a8732 100644 --- a/content/published/patterns/core-concepts/understand-layers-for-dependency-injection.mdx +++ b/content/published/patterns/core-concepts/understand-layers-for-dependency-injection.mdx @@ -51,28 +51,33 @@ This approach has several key benefits: Here, we define a `Notifier` service that requires a `Logger` to be built. The `NotifierLive` layer's type signature, `Layer`, clearly documents this dependency. ```typescript -import { Effect } from "effect"; +import { Effect, Context, Layer } from "effect"; // Define the Logger service with a default implementation -export class Logger extends Effect.Service()("Logger", { +export class Logger extends Context.Service()("Logger", { // Provide a synchronous implementation - sync: () => ({ + make: Effect.sync(() => ({ log: (msg: string) => Effect.log(`LOG: ${msg}`), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Define the Notifier service that depends on Logger -export class Notifier extends Effect.Service()("Notifier", { +export class Notifier extends Context.Service()("Notifier", { // Provide an implementation that requires Logger - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const logger = yield* Logger; return { notify: (msg: string) => logger.log(`Notifying: ${msg}`), }; }), // Specify dependencies - dependencies: [Logger.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(Logger.layer) + ) +} // Create a program that uses both services const program = Effect.gen(function* () { @@ -81,7 +86,7 @@ const program = Effect.gen(function* () { }); // Run the program with the default implementations -Effect.runPromise(Effect.provide(program, Notifier.Default)); +Effect.runPromise(Effect.provide(program, Notifier.layer)); ``` --- diff --git a/content/published/patterns/core-concepts/use-chunk-for-high-performance-collections.mdx b/content/published/patterns/core-concepts/use-chunk-for-high-performance-collections.mdx index be224e59..9f2a5673 100644 --- a/content/published/patterns/core-concepts/use-chunk-for-high-performance-collections.mdx +++ b/content/published/patterns/core-concepts/use-chunk-for-high-performance-collections.mdx @@ -81,7 +81,7 @@ function* largeDataSource() { // ❌ DANGEROUS: `Chunk.fromIterable` will try to pull all 1,000,000 items // from the generator and load them into memory at once before the stream // even starts. This can lead to high memory usage or a crash. -const programWithChunk = Stream.fromChunk( +const programWithChunk = Stream.fromArray( Chunk.fromIterable(largeDataSource()) ).pipe( Stream.map((n) => n * 2), diff --git a/content/published/patterns/core-concepts/use-pipe-for-composition.mdx b/content/published/patterns/core-concepts/use-pipe-for-composition.mdx index 5a57c1a2..455f8075 100644 --- a/content/published/patterns/core-concepts/use-pipe-for-composition.mdx +++ b/content/published/patterns/core-concepts/use-pipe-for-composition.mdx @@ -77,7 +77,7 @@ const demo = Effect.gen(function* () { Effect.flatMap((n) => n > 0 ? Effect.succeed(n) : Effect.fail(new Error("Negative number")) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed("Handled error: " + error.message) ), Effect.tap((result) => Effect.log(`Error handled: ${result}`)) diff --git a/content/published/patterns/core-concepts/wrap-asynchronous-computations.mdx b/content/published/patterns/core-concepts/wrap-asynchronous-computations.mdx index 295f152c..6a2cd7c5 100644 --- a/content/published/patterns/core-concepts/wrap-asynchronous-computations.mdx +++ b/content/published/patterns/core-concepts/wrap-asynchronous-computations.mdx @@ -34,7 +34,7 @@ you to leverage the massive `async/await` ecosystem safely. ## Good Example ```typescript -import { Effect, Data } from "effect"; +import { Effect, Data, Context, Layer } from "effect"; // Define error type using Data.TaggedError class HttpError extends Data.TaggedError("HttpError")<{ @@ -42,23 +42,23 @@ class HttpError extends Data.TaggedError("HttpError")<{ }> {} // Define HTTP client service -export class HttpClient extends Effect.Service()("HttpClient", { +export class HttpClient extends Context.Service()("HttpClient", { // Provide default implementation - sync: () => ({ + make: Effect.sync(() => ({ getUrl: (url: string) => Effect.tryPromise({ try: () => fetch(url), catch: (error) => new HttpError({ message: `Failed to fetch ${url}: ${error}` }), }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Mock HTTP client for demonstration -export class MockHttpClient extends Effect.Service()( - "MockHttpClient", - { - sync: () => ({ +export class MockHttpClient extends Context.Service()("MockHttpClient", { + make: Effect.sync(() => ({ getUrl: (url: string) => Effect.gen(function* () { yield* Effect.logInfo(`Fetching URL: ${url}`); @@ -81,9 +81,10 @@ export class MockHttpClient extends Effect.Service()( }); } }), - }), - } -) {} + })), + }) { + static readonly layer = Layer.effect(this, this.make) +} // Demonstrate wrapping asynchronous computations const program = Effect.gen(function* () { @@ -96,7 +97,7 @@ const program = Effect.gen(function* () { const response1 = yield* client .getUrl("https://api.example.com/success") .pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Request failed: ${error.message}`); return new Response("Error response", { status: 500 }); @@ -108,7 +109,7 @@ const program = Effect.gen(function* () { // Example 2: Failed request with error handling yield* Effect.logInfo("\n2. Failed request with error handling:"); const response2 = yield* client.getUrl("https://api.example.com/error").pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Request failed: ${error.message}`); return new Response("Fallback response", { status: 200 }); @@ -127,7 +128,7 @@ const program = Effect.gen(function* () { ], { concurrency: 2 } ).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`One or more requests failed: ${error.message}`); return []; @@ -142,7 +143,7 @@ const program = Effect.gen(function* () { }); // Run with mock implementation -Effect.runPromise(Effect.provide(program, MockHttpClient.Default)); +Effect.runPromise(Effect.provide(program, MockHttpClient.layer)); ``` **Explanation:** diff --git a/content/published/patterns/core-concepts/wrap-synchronous-computations.mdx b/content/published/patterns/core-concepts/wrap-synchronous-computations.mdx index 35aedbb0..6b9c8c99 100644 --- a/content/published/patterns/core-concepts/wrap-synchronous-computations.mdx +++ b/content/published/patterns/core-concepts/wrap-synchronous-computations.mdx @@ -87,7 +87,7 @@ const program = Effect.gen(function* () { const invalidJson = '{"name": "Paul", "age":}'; yield* parseJson(invalidJson).pipe( Effect.tapError((error) => Effect.log(`Parsing failed: ${error.message}`)), - Effect.catchAll(() => Effect.succeed({ name: "default", age: 0 })) + Effect.catch(() => Effect.succeed({ name: "default", age: 0 })) ); yield* Effect.log("Continued after error (with recovery)"); @@ -96,10 +96,10 @@ const program = Effect.gen(function* () { const division1 = yield* divide(10, 2); yield* Effect.log(`10 / 2 = ${division1}`); - // Use tapError to log, then catchAll to recover + // Use tapError to log, then catch to recover const division2 = yield* divide(10, 0).pipe( Effect.tapError((error) => Effect.log(`Division error: ${error.message}`)), - Effect.catchAll(() => Effect.succeed(-1)) + Effect.catch(() => Effect.succeed(-1)) ); yield* Effect.log(`10 / 0 = ${division2} (error handled)`); diff --git a/content/published/patterns/core-concepts/write-sequential-code-with-gen.mdx b/content/published/patterns/core-concepts/write-sequential-code-with-gen.mdx index fa785a0c..c905bc90 100644 --- a/content/published/patterns/core-concepts/write-sequential-code-with-gen.mdx +++ b/content/published/patterns/core-concepts/write-sequential-code-with-gen.mdx @@ -131,7 +131,7 @@ const program = Effect.gen(function* () { // Example 1: Sequential operations with Effect.gen yield* Effect.logInfo("\n1. Sequential operations with Effect.gen:"); const userData = yield* getUserDataWithGen(123).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Failed to get user data: ${error}`); return null; @@ -151,7 +151,7 @@ const program = Effect.gen(function* () { // Example 2: Compare with traditional promise-like chaining yield* Effect.logInfo("\n2. Same logic without Effect.gen (for comparison):"); const userData2 = yield* getUserDataWithoutGen(456).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Failed to get user data: ${error}`); return null; @@ -177,7 +177,7 @@ const program = Effect.gen(function* () { return null; } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Caught error: ${error}`); return { user: null, posts: [] }; diff --git a/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx b/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx index d2b0a9af..81e0ba7a 100644 --- a/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx +++ b/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx @@ -1,20 +1,20 @@ --- -title: Accumulate Multiple Errors with Either +title: Accumulate Multiple Errors with Result id: accumulate-multiple-errors-with-either skillLevel: intermediate applicationPatternId: domain-modeling summary: >- - Use Either to represent computations that can fail, allowing you to + Use Result to represent computations that can fail, allowing you to accumulate multiple errors instead of short-circuiting on the first one. tags: - - either + - result - validation - error-accumulation - schema - data rule: description: >- - Use Either to accumulate multiple validation errors instead of failing on + Use Result to accumulate multiple validation errors instead of failing on the first one. related: - define-contracts-with-schema @@ -25,7 +25,7 @@ lessonOrder: 1 ## Guideline -When you need to perform multiple validation checks and collect all failures, use the `Either` data type. `Either` represents a value that can be one of two possibilities: a `Left` (typically for failure) or a `Right` (typically for success). +When you need to perform multiple validation checks and collect all failures, use the `Result` data type. `Result` represents a value that can be one of two possibilities: a `Left` (typically for failure) or a `Right` (typically for success). --- @@ -35,16 +35,16 @@ The `Effect` error channel is designed to short-circuit. The moment an `Effect` However, for tasks like validating a user's input, this is poor user experience. You want to show the user all of their mistakes at once. -`Either` is the solution. Since it's a pure data structure, you can run multiple checks that each return an `Either`, and then combine the results to accumulate all the `Left` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation. +`Result` is the solution. Since it's a pure data structure, you can run multiple checks that each return an `Result`, and then combine the results to accumulate all the `Left` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation. --- ## Good Example -Using `Schema.decode` with the `allErrors: true` option demonstrates this pattern perfectly. The underlying mechanism uses `Either` to collect all parsing errors into an array instead of stopping at the first one. +Using `Schema.decodeEffect` with the `allErrors: true` option demonstrates this pattern perfectly. The underlying mechanism uses `Result` to collect all parsing errors into an array instead of stopping at the first one. ```typescript -import { Effect, Schema, Data, Either } from "effect"; +import { Effect, Schema, Data, Result } from "effect"; // Define validation error type class ValidationError extends Data.TaggedError("ValidationError")<{ @@ -61,16 +61,16 @@ type User = { // Define schema with custom validation const UserSchema = Schema.Struct({ name: Schema.String.pipe( - Schema.minLength(3), - Schema.filter((name) => /^[A-Za-z\s]+$/.test(name), { + Schema.check(Schema.isMinLength(3)), + Schema.check(Schema.makeFilter((name) => /^[A-Za-z\s]+$/.test(name), { message: () => "name must contain only letters and spaces", - }) + })) ), email: Schema.String.pipe( - Schema.pattern(/@/), - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, { + Schema.check(Schema.isPattern(/@/)), + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, { message: () => "email must be a valid email address", - }) + })) ), }); @@ -93,7 +93,7 @@ const invalidInputs: User[] = [ // Validate a single user const validateUser = (input: User) => Effect.gen(function* () { - const result = yield* Schema.decode(UserSchema)(input, { errors: "all" }); + const result = yield* Schema.decodeEffect(UserSchema)(input, { errors: "all" }); return result; }); @@ -102,20 +102,20 @@ const program = Effect.gen(function* () { yield* Effect.log("Validating users...\n"); for (const input of invalidInputs) { - const result = yield* Effect.either(validateUser(input)); + const result = yield* Effect.result(validateUser(input)); yield* Effect.log(`Validating user: ${input.name} <${input.email}>`); // Handle success and failure cases separately for clarity - // Using Either.match which is the idiomatic way to handle Either values - yield* Either.match(result, { - onLeft: (error) => + // Using Result.match which is the idiomatic way to handle Result values + yield* Result.match(result, { + onFailure: (error) => Effect.gen(function* () { yield* Effect.log("❌ Validation failed:"); yield* Effect.log(error.message); yield* Effect.log(""); // Empty line for readability }), - onRight: (user) => + onSuccess: (user) => Effect.gen(function* () { yield* Effect.log(`βœ… User is valid: ${JSON.stringify(user)}`); yield* Effect.log(""); // Empty line for readability diff --git a/content/published/patterns/domain-modeling/brand-validate-parse.mdx b/content/published/patterns/domain-modeling/brand-validate-parse.mdx index 9670b035..cd180240 100644 --- a/content/published/patterns/domain-modeling/brand-validate-parse.mdx +++ b/content/published/patterns/domain-modeling/brand-validate-parse.mdx @@ -45,7 +45,7 @@ type Email = string & Brand.Brand<"Email">; // Create a Schema for Email validation const EmailSchema = Schema.String.pipe( - Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/), // Simple email regex + Schema.check(Schema.isPattern(/^[^@]+@[^@]+\.[^@]+$/)), // Simple email regex Schema.brand("Email" as const) // Attach the brand ); @@ -68,7 +68,7 @@ parseEmail("user@example.com").pipe( **Explanation:** - `Schema` is used to define validation logic for the branded type. -- `Brand.schema()` attaches the brand to the schema, so only validated values can be constructed as `Email`. +- `Schema.brand("Email")` attaches the brand to the schema, so only validated values can be constructed as `Email`. - This pattern ensures both compile-time and runtime safety. ## Anti-Pattern diff --git a/content/published/patterns/domain-modeling/define-contracts-with-schema.mdx b/content/published/patterns/domain-modeling/define-contracts-with-schema.mdx index a6856520..0bccbfa7 100644 --- a/content/published/patterns/domain-modeling/define-contracts-with-schema.mdx +++ b/content/published/patterns/domain-modeling/define-contracts-with-schema.mdx @@ -38,7 +38,7 @@ compile-time static types and runtime validation. ## Good Example ```typescript -import { Schema, Effect, Data } from "effect"; +import { Schema, Effect, Data, Context, Layer } from "effect"; // Define User schema and type const UserSchema = Schema.Struct({ @@ -54,14 +54,16 @@ class UserNotFound extends Data.TaggedError("UserNotFound")<{ }> {} // Create database service implementation -export class Database extends Effect.Service()("Database", { - sync: () => ({ +export class Database extends Context.Service()("Database", { + make: Effect.sync(() => ({ getUser: (id: number) => id === 1 ? Effect.succeed({ id: 1, name: "John" }) : Effect.fail(new UserNotFound({ id })), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create a program that demonstrates schema and error handling const program = Effect.gen(function* () { @@ -79,7 +81,7 @@ const program = Effect.gen(function* () { const user = yield* db.getUser(999); yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); }).pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { if (error instanceof UserNotFound) { return Effect.logInfo(`Error: User with id ${error.id} not found`); } @@ -91,17 +93,17 @@ const program = Effect.gen(function* () { yield* Effect.logInfo("\nTrying to decode invalid user data..."); const invalidUser = { id: "not-a-number", name: 123 } as any; yield* Effect.gen(function* () { - const user = yield* Schema.decode(UserSchema)(invalidUser); + const user = yield* Schema.decodeEffect(UserSchema)(invalidUser); yield* Effect.logInfo(`Decoded user: ${JSON.stringify(user)}`); }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logInfo(`Validation failed:\n${JSON.stringify(error, null, 2)}`) ) ); }); // Run the program -Effect.runPromise(Effect.provide(program, Database.Default)); +Effect.runPromise(Effect.provide(program, Database.layer)); ``` **Explanation:** diff --git a/content/published/patterns/domain-modeling/define-tagged-errors.mdx b/content/published/patterns/domain-modeling/define-tagged-errors.mdx index f2792c3a..a3934105 100644 --- a/content/published/patterns/domain-modeling/define-tagged-errors.mdx +++ b/content/published/patterns/domain-modeling/define-tagged-errors.mdx @@ -63,7 +63,7 @@ const program = Effect.gen(function* () { const user = yield* findUser(1); yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.logInfo(`Error finding user: ${error._tag} - ${error.cause}`) ) ); @@ -90,5 +90,5 @@ Tagged errors allow you to handle errors in a type-safe, self-documenting way. ## Anti-Pattern Using generic `Error` objects or strings in the error channel. This loses all -type information, forcing consumers to use `catchAll` and perform unsafe +type information, forcing consumers to use `catch` and perform unsafe checks. diff --git a/content/published/patterns/domain-modeling/distinguish-not-found-from-errors.mdx b/content/published/patterns/domain-modeling/distinguish-not-found-from-errors.mdx index 21339839..008a0eb0 100644 --- a/content/published/patterns/domain-modeling/distinguish-not-found-from-errors.mdx +++ b/content/published/patterns/domain-modeling/distinguish-not-found-from-errors.mdx @@ -67,7 +67,7 @@ const findUserInDb = ( }); // We wrap the potentially null result in an Option - return Option.fromNullable(dbResult); + return Option.fromNullishOr(dbResult); }); // The caller can now handle all three cases explicitly. @@ -80,7 +80,7 @@ const program = (id: number) => onSome: (user) => Effect.logInfo(`Result: Found user ${user.name}.`), }) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.logInfo("Error: Could not connect to the database.") ) ); diff --git a/content/published/patterns/domain-modeling/domain-modeling-option-basics.mdx b/content/published/patterns/domain-modeling/domain-modeling-option-basics.mdx index 3fe2f7f5..1e880c5c 100644 --- a/content/published/patterns/domain-modeling/domain-modeling-option-basics.mdx +++ b/content/published/patterns/domain-modeling/domain-modeling-option-basics.mdx @@ -56,8 +56,8 @@ const hasValue = Option.some(42) const noValue = Option.none() // From nullable - null/undefined becomes None -const fromNull = Option.fromNullable(null) // None -const fromValue = Option.fromNullable("hello") // Some("hello") +const fromNull = Option.fromNullishOr(null) // None +const fromValue = Option.fromNullishOr("hello") // Some("hello") // ============================================ // 2. Checking and extracting values diff --git a/content/published/patterns/domain-modeling/model-optional-values-with-option.mdx b/content/published/patterns/domain-modeling/model-optional-values-with-option.mdx index 3e2590cd..9349429e 100644 --- a/content/published/patterns/domain-modeling/model-optional-values-with-option.mdx +++ b/content/published/patterns/domain-modeling/model-optional-values-with-option.mdx @@ -57,7 +57,7 @@ const users: User[] = [ // This function safely returns an Option, not a User or null. const findUserById = (id: number): Option.Option => { const user = users.find((u) => u.id === id); - return Option.fromNullable(user); // A useful helper for existing APIs + return Option.fromNullishOr(user); // A useful helper for existing APIs }; // The caller MUST handle both cases. diff --git a/content/published/patterns/domain-modeling/parse-with-schema-decode.mdx b/content/published/patterns/domain-modeling/parse-with-schema-decode.mdx index 3c742d35..374ec896 100644 --- a/content/published/patterns/domain-modeling/parse-with-schema-decode.mdx +++ b/content/published/patterns/domain-modeling/parse-with-schema-decode.mdx @@ -1,10 +1,10 @@ --- -title: Parse and Validate Data with Schema.decode +title: Parse and Validate Data with Schema.decodeEffect id: parse-with-schema-decode skillLevel: intermediate applicationPatternId: domain-modeling summary: >- - Use Schema.decode(schema) to create an Effect that parses and validates + Use Schema.decodeEffect(schema) to create an Effect that parses and validates unknown data, which integrates seamlessly with Effect's error handling. tags: - schema @@ -12,24 +12,24 @@ tags: - parsing - data rule: - description: Parse and validate data with Schema.decode. + description: Parse and validate data with Schema.decodeEffect. related: - define-config-schema author: effect_website lessonOrder: 9 --- -# Parse and Validate Data with Schema.decode +# Parse and Validate Data with Schema.decodeEffect ## Guideline When you need to parse or validate data against a `Schema`, use the -`Schema.decode(schema)` function. It takes an `unknown` input and returns an +`Schema.decodeEffect(schema)` function. It takes an `unknown` input and returns an `Effect`. ## Rationale -Unlike the older `Schema.parse` which throws, `Schema.decode` is fully +Unlike the older `Schema.parse` which throws, `Schema.decodeEffect` is fully integrated into the Effect ecosystem, allowing you to handle validation failures gracefully with operators like `Effect.catchTag`. @@ -48,7 +48,7 @@ const UserSchema = Schema.Struct({ const processUserInput = (input: unknown) => Effect.gen(function* () { - const user = yield* Schema.decodeUnknown(UserSchema)(input); + const user = yield* Schema.decodeUnknownEffect(UserSchema)(input); return `Welcome, ${user.name}!`; }).pipe( Effect.catchTag("ParseError", () => Effect.succeed("Invalid user data.")) @@ -76,7 +76,7 @@ Effect.runPromise(program); ``` **Explanation:** -`Schema.decode` integrates parsing and validation into the Effect workflow, +`Schema.decodeEffect` integrates parsing and validation into the Effect workflow, making error handling composable and type-safe. ## Anti-Pattern diff --git a/content/published/patterns/domain-modeling/transform-data-with-schema.mdx b/content/published/patterns/domain-modeling/transform-data-with-schema.mdx index 9616a634..b72ac8a5 100644 --- a/content/published/patterns/domain-modeling/transform-data-with-schema.mdx +++ b/content/published/patterns/domain-modeling/transform-data-with-schema.mdx @@ -27,7 +27,7 @@ lessonOrder: 10 ## Guideline -To convert data from one type to another as part of the validation process, use `Schema.transform`. This allows you to define a schema that parses an input type (e.g., `string`) and outputs a different, richer domain type (e.g., `Date`). +To convert data from one type to another as part of the validation process, use purpose-built codecs like `Schema.DateFromString`, or `Schema.decodeTo` with `SchemaTransformation.transform` for custom conversions. This allows you to define a schema that parses an input type (e.g., `string`) and outputs a different, richer domain type (e.g., `Date`). --- @@ -35,9 +35,9 @@ To convert data from one type to another as part of the validation process, use Often, the data you receive from external sources (like an API) isn't in the ideal format for your application's domain model. For example, dates are sent as ISO strings, but you want to work with `Date` objects. -`Schema.transform` integrates this conversion directly into the parsing step. It takes two functions: one to `decode` the input type into the domain type, and one to `encode` it back. This makes your schema the single source of truth for both the shape and the type transformation of your data. +`Schema.DateFromString` integrates this conversion directly into the parsing step. For custom conversions, `Schema.decodeTo` takes a target schema plus a `SchemaTransformation.transform` with `decode`/`encode` functions. This makes your schema the single source of truth for both the shape and the type transformation of your data. -For transformations that can fail (like creating a branded type), you can use `Schema.transformOrFail`, which allows the decoding step to return an `Either`. +For transformations that can fail (like creating a branded type), combine `Schema.brand` with `Schema.check` refinements, which keeps validation declarative. --- @@ -48,36 +48,23 @@ This schema parses a string but produces a `Date` object, making the final data ```typescript import { Schema, Effect } from "effect"; -// Define types for better type safety -type RawEvent = { - name: string; - timestamp: string; -}; - -type ParsedEvent = { - name: string; - timestamp: Date; -}; - -// Define the schema for our event +// Define the schema for our event: the timestamp field parses an ISO string +// directly into a Date object. const ApiEventSchema = Schema.Struct({ name: Schema.String, - timestamp: Schema.String, + timestamp: Schema.DateFromString }); // Example input -const rawInput: RawEvent = { +const rawInput = { name: "User Login", - timestamp: "2025-06-22T20:08:42.000Z", + timestamp: "2025-06-22T20:08:42.000Z" }; -// Parse and transform +// Parse (decoding also transforms) const program = Effect.gen(function* () { - const parsed = yield* Schema.decode(ApiEventSchema)(rawInput); - return { - name: parsed.name, - timestamp: new Date(parsed.timestamp), - } as ParsedEvent; + const event = yield* Schema.decodeEffect(ApiEventSchema)(rawInput); + return event; }); const programWithLogging = Effect.gen(function* () { @@ -91,7 +78,7 @@ const programWithLogging = Effect.gen(function* () { throw error; } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Program error: ${error}`); return null; @@ -104,25 +91,19 @@ Effect.runPromise(programWithLogging); ## Good Example 2: Creating a Branded Type -`transformOrFail` is perfect for creating branded types, as the validation can fail. +`Schema.brand` combined with `Schema.check` is perfect for creating branded types, as the validation can fail. ```typescript -import { Schema, Effect, Brand, Either } from "effect"; - -type Email = string & Brand.Brand<"Email">; -const Email = Schema.string.pipe( - Schema.transformOrFail( - Schema.brand("Email"), - (s, _, ast) => - s.includes("@") - ? Either.right(s as Email) - : Either.left(Schema.ParseError.create(ast, "Invalid email format")), - (email) => Either.right(email) - ) +import { Schema } from "effect"; + +const Email = Schema.NonEmptyString.pipe( + Schema.check(Schema.isPattern(/@/)), + Schema.brand("Email") ); +type Email = typeof Email.Type; -const result = Schema.decode(Email)("paul@example.com"); // Succeeds -const errorResult = Schema.decode(Email)("invalid-email"); // Fails +const result = Schema.decodeUnknownResult(Email)("paul@example.com"); // Success +const errorResult = Schema.decodeUnknownResult(Email)("invalid-email"); // Failure ``` --- @@ -143,7 +124,7 @@ const RawApiEventSchema = Schema.Struct({ const rawInput = { name: "User Login", timestamp: "2025-06-22T20:08:42.000Z" }; // The logic is now split into two distinct, less cohesive steps. -const program = Schema.decode(RawApiEventSchema)(rawInput).pipe( +const program = Schema.decodeEffect(RawApiEventSchema)(rawInput).pipe( Effect.map((rawEvent) => ({ ...rawEvent, timestamp: new Date(rawEvent.timestamp), // Manual transformation after parsing. diff --git a/content/published/patterns/domain-modeling/use-gen-for-business-logic.mdx b/content/published/patterns/domain-modeling/use-gen-for-business-logic.mdx index ba53bea2..f47c2bb3 100644 --- a/content/published/patterns/domain-modeling/use-gen-for-business-logic.mdx +++ b/content/published/patterns/domain-modeling/use-gen-for-business-logic.mdx @@ -100,7 +100,7 @@ const program = Effect.gen(function* () { email: "paul@example.com", password: "securepassword123", }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Failed to create user: ${error.message}`); return { id: -1, email: "error" }; @@ -115,7 +115,7 @@ const program = Effect.gen(function* () { email: "invalid@example.com", password: "123", // Too short }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Failed to create user: ${error.message}`); return { id: -1, email: "error" }; diff --git a/content/published/patterns/error-management/control-repetition-with-schedule.mdx b/content/published/patterns/error-management/control-repetition-with-schedule.mdx index d2151a85..84b7c46e 100644 --- a/content/published/patterns/error-management/control-repetition-with-schedule.mdx +++ b/content/published/patterns/error-management/control-repetition-with-schedule.mdx @@ -71,7 +71,7 @@ const exponentialBackoff = Schedule.exponential("100 millis"); const withJitter = Schedule.jittered(exponentialBackoff); // 3. Limit the schedule to a maximum of 5 repetitions -const limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5)); +const limitedWithJitter = Schedule.max([withJitter, Schedule.recurs(5)]); // --- Using the Schedule --- const program = Effect.gen(function* () { diff --git a/content/published/patterns/error-management/error-handling-pattern-accumulation.mdx b/content/published/patterns/error-management/error-handling-pattern-accumulation.mdx index 01215868..b86ee643 100644 --- a/content/published/patterns/error-management/error-handling-pattern-accumulation.mdx +++ b/content/published/patterns/error-management/error-handling-pattern-accumulation.mdx @@ -92,7 +92,7 @@ Solutions: This example demonstrates error accumulation patterns. ```typescript -import { Effect, Data, Cause } from "effect"; +import { Effect, Data, Cause, Stream } from "effect"; interface ValidationError { field: string; @@ -438,7 +438,7 @@ const aggregateErrors = (effects: Array>) => ), { discard: false } // Collect all results ).pipe( - Effect.catchAll((causes) => + Effect.catch((causes) => // causes contains all accumulated errors Effect.gen(function* () { yield* Effect.log(`Collected ${causes.length} errors`); diff --git a/content/published/patterns/error-management/error-handling-pattern-custom-strategies.mdx b/content/published/patterns/error-management/error-handling-pattern-custom-strategies.mdx index 691733e7..04d25ea9 100644 --- a/content/published/patterns/error-management/error-handling-pattern-custom-strategies.mdx +++ b/content/published/patterns/error-management/error-handling-pattern-custom-strategies.mdx @@ -258,7 +258,7 @@ const program = Effect.gen(function* () { return "retry-with-backoff"; }) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[SUCCESS] Got response`); @@ -330,7 +330,7 @@ const program = Effect.gen(function* () { return null; // Signal to retry }) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[ERROR] Unhandled: ${error}`); @@ -338,7 +338,7 @@ const program = Effect.gen(function* () { }) ) ).pipe( - Effect.catchAll(() => Effect.succeed(null)) + Effect.catch(() => Effect.succeed(null)) ); if (result3 !== null) { @@ -478,7 +478,7 @@ const program = Effect.gen(function* () { return yield* Effect.fail(error); }) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[STEP 3] Final fallback`); @@ -515,7 +515,7 @@ const applyErrorHandlers = ( return sorted.reduce( (current, handler) => current.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (handler.canHandle(error as Error)) { return yield* handler.handle(error as Error); diff --git a/content/published/patterns/error-management/error-handling-pattern-propagation.mdx b/content/published/patterns/error-management/error-handling-pattern-propagation.mdx index 22f29a3b..1da2acaf 100644 --- a/content/published/patterns/error-management/error-handling-pattern-propagation.mdx +++ b/content/published/patterns/error-management/error-handling-pattern-propagation.mdx @@ -35,7 +35,7 @@ Error propagation preserves context: - **Error mapping**: Transform errors between layers - **Recovery points**: Decide where to handle errors -Pattern: Use `mapError()`, `tapError()`, `catchAll()`, `Cause.prettyPrint()` +Pattern: Use `mapError()`, `tapError()`, `catch()`, `Cause.prettyPrint()` --- @@ -199,7 +199,7 @@ const program = Effect.gen(function* () { }, }; }), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[ERROR CAUGHT] ${error.operation}`); yield* Effect.log(`[CONTEXT] ${JSON.stringify(error.context, null, 2)}`); @@ -263,7 +263,7 @@ const program = Effect.gen(function* () { ).pipe( Effect.tap(() => Effect.log(`[SUCCESS] Connected on attempt ${i}`)) ).pipe( - Effect.catchAll(() => Effect.succeed(null)) + Effect.catch(() => Effect.succeed(null)) ); if (result !== null) { @@ -279,7 +279,7 @@ const program = Effect.gen(function* () { }); const networkResult = yield* withRetryContext.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[EXHAUSTED] All retries failed`); return "fallback"; @@ -327,7 +327,7 @@ const program = Effect.gen(function* () { code: "PROFILE_LOAD_FAILED", originalError: bizError.originalError.message, })), - Effect.catchAll((userError) => + Effect.catch((userError) => Effect.gen(function* () { yield* Effect.log(`[USER MESSAGE] ${userError.message}`); yield* Effect.log(`[CODE] ${userError.code}`); @@ -362,7 +362,7 @@ const program = Effect.gen(function* () { ], { concurrency: 3 } ).pipe( - Effect.catchAll((errors) => + Effect.catch((errors) => Effect.gen(function* () { yield* Effect.log(`[CONCURRENT] Caught aggregated errors`); @@ -407,7 +407,7 @@ const inspectCauseChain = (effect: Effect.Effect) => return error; }), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log( `[CAUSE] ${JSON.stringify(error, null, 2)}` @@ -446,7 +446,7 @@ const applyRecoveryStrategies = ( return sorted.reduce( (current, strategy) => current.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (strategy.canRecover(error as Error)) { yield* Effect.log( @@ -497,7 +497,7 @@ const trackErrorPropagation = ( layers.push("layer-2"); return error; }), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { const duration = Date.now() - startTime; diff --git a/content/published/patterns/error-management/error-management-extract-cause.mdx b/content/published/patterns/error-management/error-management-extract-cause.mdx index 7972ca89..b0f1faf6 100644 --- a/content/published/patterns/error-management/error-management-extract-cause.mdx +++ b/content/published/patterns/error-management/error-management-extract-cause.mdx @@ -24,7 +24,7 @@ lessonOrder: 4 ## Guideline -When you catch with `Effect.catchAllCause`, use `Cause.failures(cause)` to get expected typed errors and `Cause.defects(cause)` to get unexpected defects. Handle them differently: recover or retry failures; log and escalate defects. +When you catch with `Effect.catchCause`, use `Cause.failures(cause)` to get expected typed errors and `Cause.defects(cause)` to get unexpected defects. Handle them differently: recover or retry failures; log and escalate defects. ## Rationale @@ -36,7 +36,7 @@ Failures are part of your domain model (e.g., `UserNotFound`, `ValidationError`) import { Effect, Cause } from "effect" const program = Effect.die("unexpected bug").pipe( - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.gen(function* () { const defects = Cause.defects(cause) const failures = Cause.failures(cause) @@ -56,4 +56,4 @@ Effect.runPromise(program) ## Anti-Pattern -Treating all errors the same, or using `Effect.catchAll` (without Cause) and losing the ability to distinguish failures from defects. +Treating all errors the same, or using `Effect.catch` (without Cause) and losing the ability to distinguish failures from defects. diff --git a/content/published/patterns/error-management/error-management-hello-world.mdx b/content/published/patterns/error-management/error-management-hello-world.mdx index e3c4726b..4ae9fc64 100644 --- a/content/published/patterns/error-management/error-management-hello-world.mdx +++ b/content/published/patterns/error-management/error-management-hello-world.mdx @@ -3,15 +3,15 @@ title: Your First Error Handler id: error-management-hello-world skillLevel: beginner applicationPatternId: error-management -summary: Learn the basics of handling errors in Effect with catchAll and catchTag. +summary: Learn the basics of handling errors in Effect with catch and catchTag. tags: - error-handling - - catchAll + - catch - catchTag - getting-started rule: description: >- - Use catchAll or catchTag to recover from errors and keep your program + Use catch or catchTag to recover from errors and keep your program running. author: PaulJPhilp related: @@ -22,7 +22,7 @@ lessonOrder: 4 ## Guideline -Handle errors in Effect using `catchAll` to catch any error, or `catchTag` to handle specific error types. +Handle errors in Effect using `catch` to catch any error, or `catchTag` to handle specific error types. --- @@ -69,11 +69,11 @@ const findUser = (id: string): Effect.Effect<{ id: string; name: string }, NotFo : Effect.fail(new NotFoundError({ resource: `user:${id}` })) // ============================================ -// 3. Handle ALL errors with catchAll +// 3. Handle ALL errors with catch // ============================================ const withFallback = fetchData("invalid-url").pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { console.log(`Failed: ${error.url}, using fallback`) return Effect.succeed("Fallback data") }) @@ -116,7 +116,7 @@ const robustFetchUser = (url: string, id: string) => // ============================================ const program = Effect.gen(function* () { - // catchAll example + // catch example const data = yield* withFallback yield* Effect.log(`Got data: ${data}`) @@ -136,7 +136,7 @@ Effect.runPromise(program) | Method | Use When | |--------|----------| -| `catchAll` | Handle any error the same way | +| `catch` | Handle any error the same way | | `catchTag` | Handle one specific error type | | `catchTags` | Handle multiple error types differently | | `orElse` | Provide an alternative Effect | @@ -146,7 +146,7 @@ Effect.runPromise(program) ```typescript // Only typed errors get caught -Effect.fail(new MyError()) // βœ… Caught by catchAll/catchTag +Effect.fail(new MyError()) // βœ… Caught by catch/catchTag // Defects (bugs) are NOT caught Effect.die("crash") // ❌ Not caught - propagates up diff --git a/content/published/patterns/error-management/handle-errors-with-catch.mdx b/content/published/patterns/error-management/handle-errors-with-catch.mdx index f5005248..d60c260f 100644 --- a/content/published/patterns/error-management/handle-errors-with-catch.mdx +++ b/content/published/patterns/error-management/handle-errors-with-catch.mdx @@ -1,10 +1,10 @@ --- -title: 'Handle Errors with catchTag, catchTags, and catchAll' +title: 'Handle Errors with catchTag, catchTags, and catch' id: handle-errors-with-catch skillLevel: intermediate applicationPatternId: error-management summary: >- - Use catchTag for type-safe recovery from specific tagged errors, and catchAll + Use catchTag for type-safe recovery from specific tagged errors, and catch to recover from any possible failure. tags: - error-handling @@ -12,20 +12,20 @@ tags: - tagged-error - recovery rule: - description: 'Handle errors with catchTag, catchTags, and catchAll.' + description: 'Handle errors with catchTag, catchTags, and catch.' related: - define-tagged-errors author: effect_website lessonOrder: 5 --- -# Handle Errors with catchTag, catchTags, and catchAll +# Handle Errors with catchTag, catchTags, and catch ## Guideline To recover from failures, use the `catch*` family of functions. `Effect.catchTag` for specific tagged errors, `Effect.catchTags` for multiple, -and `Effect.catchAll` for any error. +and `Effect.catch` for any error. ## Rationale @@ -36,7 +36,7 @@ scenarios with different logic in a type-safe way. ## Good Example ```typescript -import { Data, Effect } from "effect"; +import { Data, Effect, Context, Layer } from "effect"; // Define domain types interface User { @@ -60,8 +60,8 @@ class NotFoundError extends Data.TaggedError("NotFoundError")<{ }> {} // Define UserService -class UserService extends Effect.Service()("UserService", { - sync: () => ({ +class UserService extends Context.Service()("UserService", { + make: Effect.sync(() => ({ // Fetch user data fetchUser: ( id: string @@ -103,8 +103,10 @@ class UserService extends Effect.Service()("UserService", { yield* Effect.logInfo(message); return message; }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Compose operations with error handling using catchTags const processUser = ( @@ -156,7 +158,7 @@ const runTests = Effect.gen(function* () { }); // Run the program -Effect.runPromise(Effect.provide(runTests, UserService.Default)); +Effect.runPromise(Effect.provide(runTests, UserService.layer)); ``` **Explanation:** diff --git a/content/published/patterns/error-management/handle-flaky-operations-with-retry-timeout.mdx b/content/published/patterns/error-management/handle-flaky-operations-with-retry-timeout.mdx index 50ea4bbc..4aba27c6 100644 --- a/content/published/patterns/error-management/handle-flaky-operations-with-retry-timeout.mdx +++ b/content/published/patterns/error-management/handle-flaky-operations-with-retry-timeout.mdx @@ -50,7 +50,7 @@ Combining these two patterns is a best practice for any interaction with an exte This program attempts to fetch data from a flaky API. It will retry the request up to 3 times with increasing delays if it fails. It will also give up entirely if any single attempt takes longer than 2 seconds. ```typescript -import { Data, Duration, Effect, Schedule } from "effect"; +import { Data, Duration, Effect, Schedule, Context, Layer } from "effect"; // Define domain types interface ApiResponse { @@ -69,8 +69,8 @@ class TimeoutError extends Data.TaggedError("TimeoutError")<{ }> {} // Define API service -class ApiService extends Effect.Service()("ApiService", { - sync: () => ({ +class ApiService extends Context.Service()("ApiService", { + make: Effect.sync(() => ({ // Flaky API call that might fail or be slow fetchData: (): Effect.Effect => Effect.gen(function* () { @@ -100,18 +100,15 @@ class ApiService extends Effect.Service()("ApiService", { ); return response; }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Define retry policy: exponential backoff, up to 3 retries -const retryPolicy = Schedule.exponential(Duration.millis(100)).pipe( - Schedule.compose(Schedule.recurs(3)), - Schedule.tapInput((error: ApiError | TimeoutError) => - Effect.logWarning( +const retryPolicy = Schedule.max([Schedule.exponential(Duration.millis(100)), Schedule.recurs(3)]).pipe(Schedule.tap(({ input: error }: { input: ApiError | TimeoutError }) => Effect.logWarning( `Retrying after error: ${error._tag} (Attempt ${error.attempt})` - ) - ) -); + ))); // Create program with proper error handling const program = Effect.gen(function* () { @@ -154,7 +151,7 @@ const program = Effect.gen(function* () { }); // Run the program -Effect.runPromise(Effect.provide(program, ApiService.Default)); +Effect.runPromise(Effect.provide(program, ApiService.layer)); ``` --- diff --git a/content/published/patterns/error-management/handle-unexpected-errors-with-cause.mdx b/content/published/patterns/error-management/handle-unexpected-errors-with-cause.mdx index e4804a08..a9696ede 100644 --- a/content/published/patterns/error-management/handle-unexpected-errors-with-cause.mdx +++ b/content/published/patterns/error-management/handle-unexpected-errors-with-cause.mdx @@ -4,7 +4,7 @@ id: handle-unexpected-errors-with-cause skillLevel: advanced applicationPatternId: error-management summary: >- - Use Effect.catchAllCause or Effect.runFork to inspect the Cause of a failure, + Use Effect.catchCause or Effect.runFork to inspect the Cause of a failure, distinguishing between expected errors (Fail) and unexpected defects (Die). tags: - error-handling @@ -28,7 +28,7 @@ lessonOrder: 3 ## Guideline To build truly resilient applications, differentiate between known business -errors (`Fail`) and unknown defects (`Die`). Use `Effect.catchAllCause` to +errors (`Fail`) and unknown defects (`Die`). Use `Effect.catchCause` to inspect the full `Cause` of a failure. ## Rationale @@ -40,7 +40,7 @@ thrown exception). They should be handled differently. ## Good Example ```typescript -import { Cause, Effect, Data, Schedule, Duration } from "effect"; +import { Cause, Effect, Data, Schedule, Duration, Context, Layer } from "effect"; // Define domain types interface DatabaseConfig { @@ -68,10 +68,8 @@ class ValidationError extends Data.TaggedError("ValidationError")<{ }> {} // Define database service -class DatabaseService extends Effect.Service()( - "DatabaseService", - { - sync: () => ({ +class DatabaseService extends Context.Service()("DatabaseService", { + make: Effect.sync(() => ({ // Connect to database with proper error handling connect: ( config: DatabaseConfig @@ -106,13 +104,14 @@ class DatabaseService extends Effect.Service()( yield* Effect.logInfo("Database connection successful"); return { success: true }; }), - }), - } -) {} + })), + }) { + static readonly layer = Layer.effect(this, this.make) +} // Define user service -class UserService extends Effect.Service()("UserService", { - sync: () => ({ +class UserService extends Context.Service()("UserService", { + make: Effect.sync(() => ({ // Parse user data with validation parseUser: (input: unknown): Effect.Effect => Effect.gen(function* () { @@ -158,12 +157,14 @@ class UserService extends Effect.Service()("UserService", { throw e; } }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Define test service -class TestService extends Effect.Service()("TestService", { - sync: () => { +class TestService extends Context.Service()("TestService", { + make: Effect.sync(() => { // Create instance methods const printCause = ( prefix: string, @@ -172,8 +173,8 @@ class TestService extends Effect.Service()("TestService", { Effect.gen(function* () { yield* Effect.logInfo(`\n=== ${prefix} ===`); - if (Cause.isDie(cause)) { - const defect = Cause.failureOption(cause); + if (Cause.hasDies(cause)) { + const defect = Cause.findErrorOption(cause); if (defect._tag === "Some") { const error = defect.value as Error; yield* Effect.logError("Defect (unexpected error)"); @@ -182,8 +183,8 @@ class TestService extends Effect.Service()("TestService", { `Stack: ${error.stack?.split("\n")[1]?.trim() ?? "N/A"}` ); } - } else if (Cause.isFailure(cause)) { - const error = Cause.failureOption(cause); + } else if (Cause.hasFails(cause)) { + const error = Cause.findErrorOption(cause); yield* Effect.logWarning("Expected failure"); yield* Effect.logWarning(`Error: ${JSON.stringify(error)}`); } @@ -204,7 +205,7 @@ class TestService extends Effect.Service()("TestService", { readonly cause: Cause.Cause; }; - const result = yield* Effect.catchAllCause(program, (cause) => + const result = yield* Effect.catchCause(program, (cause) => Effect.succeed({ _tag: "error" as const, cause } as TestError) ); @@ -223,8 +224,10 @@ class TestService extends Effect.Service()("TestService", { printCause, runScenario, }; - }, -}) {} + }), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create program with proper error handling const program = Effect.gen(function* () { @@ -243,7 +246,7 @@ const program = Effect.gen(function* () { Schedule.exponential(100) ).pipe( Effect.timeout(Duration.seconds(5)), - Effect.catchAll(() => Effect.fail("Connection timeout")) + Effect.catch(() => Effect.fail("Connection timeout")) ); return result; }) @@ -257,7 +260,7 @@ const program = Effect.gen(function* () { db.connect({ url: "invalid" }), Schedule.recurs(3) ).pipe( - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.gen(function* () { yield* Effect.logError("Failed after 3 retries"); yield* Effect.logError(Cause.pretty(cause)); @@ -292,11 +295,11 @@ const program = Effect.gen(function* () { [ db.connect({ url: "" }).pipe( Effect.timeout(Duration.seconds(1)), - Effect.catchAll(() => Effect.succeed({ success: true })) + Effect.catch(() => Effect.succeed({ success: true })) ), users.parseUser({ id: "invalid" }).pipe( Effect.timeout(Duration.seconds(1)), - Effect.catchAll(() => + Effect.catch(() => Effect.succeed({ id: "timeout", name: "Timeout" }) ) ), @@ -317,10 +320,10 @@ const program = Effect.gen(function* () { Effect.runPromise( Effect.provide( Effect.provide( - Effect.provide(program, TestService.Default), - DatabaseService.Default + Effect.provide(program, TestService.layer), + DatabaseService.layer ), - UserService.Default + UserService.layer ) ); ``` @@ -331,5 +334,5 @@ failures, logging or escalating as appropriate. ## Anti-Pattern -Using a simple `Effect.catchAll` can dangerously conflate expected errors and +Using a simple `Effect.catch` can dangerously conflate expected errors and unexpected defects, masking critical bugs as recoverable errors. diff --git a/content/published/patterns/error-management/mapping-errors-to-fit-your-domain.mdx b/content/published/patterns/error-management/mapping-errors-to-fit-your-domain.mdx index a4ae4e94..8c91a22e 100644 --- a/content/published/patterns/error-management/mapping-errors-to-fit-your-domain.mdx +++ b/content/published/patterns/error-management/mapping-errors-to-fit-your-domain.mdx @@ -77,7 +77,7 @@ const program = Effect.gen(function* () { yield* Effect.logInfo("This won't be reached due to Effect error handling"); } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (error instanceof RepositoryError) { yield* Effect.logInfo(`Repository error occurred: ${error._tag}`); diff --git a/content/published/patterns/error-management/pattern-catchtag.mdx b/content/published/patterns/error-management/pattern-catchtag.mdx index 9f41f774..05123b2e 100644 --- a/content/published/patterns/error-management/pattern-catchtag.mdx +++ b/content/published/patterns/error-management/pattern-catchtag.mdx @@ -77,4 +77,4 @@ const effect2 = Effect.fail(new NotFoundError() as MyError).pipe( ## Anti-Pattern -Catching all errors generically (e.g., with `catchAll`) and using manual type checks or property inspection, which is less safe and more error-prone than using tagged error combinators. +Catching all errors generically (e.g., with `catch`) and using manual type checks or property inspection, which is less safe and more error-prone than using tagged error combinators. diff --git a/content/published/patterns/error-management/pattern-match.mdx b/content/published/patterns/error-management/pattern-match.mdx index 9df07f40..2e72533a 100644 --- a/content/published/patterns/error-management/pattern-match.mdx +++ b/content/published/patterns/error-management/pattern-match.mdx @@ -5,18 +5,18 @@ skillLevel: beginner applicationPatternId: error-management summary: >- Use match to handle both success and failure cases in a single, declarative - place for Effect, Option, and Either. + place for Effect, Option, and Result. tags: - match - pattern-matching - effect - option - - either + - result - error-handling - branching rule: description: >- - Use match to pattern match on the result of an Effect, Option, or Either, + Use match to pattern match on the result of an Effect, Option, or Result, handling both success and failure cases declaratively. related: - pattern-matchtag @@ -31,7 +31,7 @@ lessonOrder: 2 ## Guideline Use the `match` combinator to handle both success and failure cases in a single, declarative place. -This works for `Effect`, `Option`, and `Either`, and is the foundation for robust, readable error handling and branching. +This works for `Effect`, `Option`, and `Result`, and is the foundation for robust, readable error handling and branching. ## Rationale @@ -41,7 +41,7 @@ It avoids scattered if/else or switch statements and makes your intent explicit. ## Good Example ```typescript -import { Effect, Option, Either } from "effect"; +import { Effect, Option, Result } from "effect"; // Effect: Handle both success and failure const effect = Effect.fail("Oops!").pipe( @@ -59,11 +59,11 @@ const option = Option.some(42).pipe( }) ); // string -// Either: Handle Left and Right cases -const either = Either.left("fail").pipe( - Either.match({ - onLeft: (err) => `Error: ${err}`, - onRight: (value) => `Value: ${value}`, +// Result: Handle Left and Right cases +const either = Result.fail("fail").pipe( + Result.match({ + onFailure: (err) => `Error: ${err}`, + onSuccess: (value) => `Value: ${value}`, }) ); // string ``` @@ -71,7 +71,7 @@ const either = Either.left("fail").pipe( **Explanation:** - `Effect.match` lets you handle both the error and success channels in one place. -- `Option.match` and `Either.match` let you handle all possible cases for these types, making your code exhaustive and safe. +- `Option.match` and `Result.match` let you handle all possible cases for these types, making your code exhaustive and safe. ## Anti-Pattern diff --git a/content/published/patterns/error-management/pattern-option-either-checks.mdx b/content/published/patterns/error-management/pattern-option-either-checks.mdx index 31d0518e..3c688d74 100644 --- a/content/published/patterns/error-management/pattern-option-either-checks.mdx +++ b/content/published/patterns/error-management/pattern-option-either-checks.mdx @@ -1,23 +1,23 @@ --- -title: Checking Option and Either Cases +title: Checking Option and Result Cases id: pattern-option-either-checks skillLevel: beginner applicationPatternId: error-management summary: >- - Use isSome, isNone, isLeft, and isRight to check Option and Either cases for + Use isSome, isNone, isFailure, and isSuccess to check Option and Result cases for simple, type-safe branching. tags: - isSome - isNone - - isLeft - - isRight + - isFailure + - isSuccess - pattern-matching - option - - either + - result - checks rule: description: >- - Use isSome, isNone, isLeft, and isRight to check Option and Either cases for + Use isSome, isNone, isFailure, and isSuccess to check Option and Result cases for simple, type-safe conditional logic. related: - pattern-option-either-match @@ -26,11 +26,11 @@ author: PaulJPhilp lessonOrder: 1 --- -# Checking Option and Either Cases +# Checking Option and Result Cases ## Guideline -Use the `isSome`, `isNone`, `isLeft`, and `isRight` predicates to check the case of an `Option` or `Either` for simple, type-safe branching. +Use the `isSome`, `isNone`, `isFailure`, and `isSuccess` predicates to check the case of an `Option` or `Result` for simple, type-safe branching. These are useful when you need to perform quick checks or filter collections based on presence or success. ## Rationale @@ -40,7 +40,7 @@ These predicates provide a concise, type-safe way to check which case you have, ## Good Example ```typescript -import { Option, Either } from "effect"; +import { Option, Result } from "effect"; // Option: Check if value is Some or None const option = Option.some(42); @@ -52,15 +52,15 @@ if (Option.isSome(option)) { console.log("No value present"); } -// Either: Check if value is Right or Left -const either = Either.left("error"); +// Result: Check if value is Right or Left +const either = Result.fail("error"); -if (Either.isRight(either)) { - // either.right is available here - console.log("Success:", either.right); -} else if (Either.isLeft(either)) { - // either.left is available here - console.log("Failure:", either.left); +if (Result.isSuccess(either)) { + // either.success is available here + console.log("Success:", either.success); +} else if (Result.isFailure(either)) { + // either.failure is available here + console.log("Failure:", either.failure); } // Filtering a collection of Options @@ -71,7 +71,7 @@ const presentValues = options.filter(Option.isSome).map((o) => o.value); // [1, **Explanation:** - `Option.isSome` and `Option.isNone` let you check for presence or absence. -- `Either.isRight` and `Either.isLeft` let you check for success or failure. +- `Result.isSuccess` and `Result.isFailure` let you check for success or failure. - These are especially useful for filtering or quick conditional logic. ## Anti-Pattern diff --git a/content/published/patterns/error-management/pattern-option-either-match.mdx b/content/published/patterns/error-management/pattern-option-either-match.mdx index 8e58dc4f..e61220d8 100644 --- a/content/published/patterns/error-management/pattern-option-either-match.mdx +++ b/content/published/patterns/error-management/pattern-option-either-match.mdx @@ -1,12 +1,12 @@ --- id: pattern-option-either-match -title: Pattern Match on Option and Either +title: Pattern Match on Option and Result summary: Use declarative match() combinators to handle optional and error-prone values skillLevel: beginner applicationPatternId: error-management tags: - option - - either + - result - pattern-matching - match - declarative @@ -18,14 +18,14 @@ related: author: Effect Patterns Hub rule: description: >- - Use Option.match() and Either.match() for declarative pattern matching on + Use Option.match() and Result.match() for declarative pattern matching on optional and error-prone values lessonOrder: 3 --- ## Guideline -When you need to handle `Option` or `Either` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Either) in a single expression. +When you need to handle `Option` or `Result` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Result) in a single expression. Use `.match()` when: - You need to handle both success and failure cases @@ -35,7 +35,7 @@ Use `.match()` when: ## Rationale -The `.match()` combinator is superior to manual checks (`isSome()`, `isLeft()`) because: +The `.match()` combinator is superior to manual checks (`isSome()`, `isFailure()`) because: 1. **Declarative**: Expresses intent clearly - "match on these cases" 2. **Type-safe**: TypeScript ensures all cases are handled @@ -69,23 +69,23 @@ console.log(displayUser(1)); // "Hello, Alice!" console.log(displayUser(999)); // "Guest User" ``` -### Basic Either Matching +### Basic Result Matching ```typescript -import { Either } from "effect"; +import { Result } from "effect"; -const validateAge = (age: number): Either.Either => { +const validateAge = (age: number): Result.Result => { return age >= 18 - ? Either.right(age) - : Either.left("Must be 18 or older"); + ? Result.succeed(age) + : Result.fail("Must be 18 or older"); }; // Using .match() for error handling const processAge = (age: number): string => validateAge(age).pipe( - Either.match({ - onLeft: (error) => `Validation failed: ${error}`, - onRight: (validAge) => `Age ${validAge} is valid`, + Result.match({ + onFailure: (error) => `Validation failed: ${error}`, + onSuccess: (validAge) => `Age ${validAge} is valid`, }) ); @@ -95,10 +95,10 @@ console.log(processAge(15)); // "Validation failed: Must be 18 or older" ### Advanced: Nested Matching -When dealing with nested Option and Either, use nested `.match()` calls: +When dealing with nested Option and Result, use nested `.match()` calls: ```typescript -import { Option, Either } from "effect"; +import { Option, Result } from "effect"; interface UserProfile { name: string; @@ -107,22 +107,22 @@ interface UserProfile { const getUserProfile = ( id: number -): Option.Option> => { +): Option.Option> => { if (id === 0) return Option.none(); // User not found - if (id === 1) return Option.some(Either.left("Profile incomplete")); - return Option.some(Either.right({ name: "Bob", age: 25 })); + if (id === 1) return Option.some(Result.fail("Profile incomplete")); + return Option.some(Result.succeed({ name: "Bob", age: 25 })); }; -// Nested matching - first on Option, then on Either +// Nested matching - first on Option, then on Result const displayProfile = (id: number): string => getUserProfile(id).pipe( Option.match({ onNone: () => "User not found", onSome: (result) => result.pipe( - Either.match({ - onLeft: (error) => `Error: ${error}`, - onRight: (profile) => `${profile.name} (${profile.age})`, + Result.match({ + onFailure: (error) => `Error: ${error}`, + onSuccess: (profile) => `${profile.name} (${profile.age})`, }) ), }) @@ -150,9 +150,9 @@ if (Option.isSome(name)) { // ❌ ANTI-PATTERN: Nested ternaries const ageResult = validateAge(25); const message = ageResult.pipe( - Either.match({ - onLeft: () => "Invalid", - onRight: (age) => age >= 21 ? "Can drink" : "Cannot drink", + Result.match({ + onFailure: () => "Invalid", + onSuccess: (age) => age >= 21 ? "Can drink" : "Cannot drink", }) ); diff --git a/content/published/patterns/error-management/retry-based-on-specific-errors.mdx b/content/published/patterns/error-management/retry-based-on-specific-errors.mdx index 10a342a2..9ca95cf3 100644 --- a/content/published/patterns/error-management/retry-based-on-specific-errors.mdx +++ b/content/published/patterns/error-management/retry-based-on-specific-errors.mdx @@ -26,7 +26,7 @@ lessonOrder: 11 ## Guideline -To selectively retry an operation, use `Effect.retry` with a `Schedule` that includes a predicate. The most common way is to use `Schedule.whileInput((error) => ...)`, which will continue retrying only as long as the predicate returns `true` for the error that occurred. +To selectively retry an operation, use `Effect.retry` with a `Schedule` that includes a predicate. The most common way is to use `Schedule.while(({ input: error }) => ...)`, which will continue retrying only as long as the predicate returns `true` for the error that occurred. --- @@ -78,7 +78,7 @@ const isRetryableError = (e: ServerBusyError | NotFoundError) => // A policy that retries 3 times, but only if the error is retryable const selectiveRetryPolicy = Schedule.recurs(3).pipe( - Schedule.whileInput(isRetryableError), + Schedule.while((meta) => isRetryableError(meta.input)), Schedule.addDelay(() => "100 millis") ); @@ -94,7 +94,7 @@ const program = Effect.gen(function* () { return null; } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (error instanceof NotFoundError) { yield* Effect.logInfo("Failed with NotFoundError - not retrying"); @@ -116,7 +116,7 @@ const demonstrateNotFound = Effect.gen(function* () { const result = yield* alwaysNotFound.pipe( Effect.retry(selectiveRetryPolicy), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`); return null; diff --git a/content/published/patterns/error-management/scheduling-pattern-exponential-backoff.mdx b/content/published/patterns/error-management/scheduling-pattern-exponential-backoff.mdx index 13c87072..d1a40c72 100644 --- a/content/published/patterns/error-management/scheduling-pattern-exponential-backoff.mdx +++ b/content/published/patterns/error-management/scheduling-pattern-exponential-backoff.mdx @@ -127,14 +127,14 @@ const exponentialBackoffWithJitter = (config: BackoffConfig) => { let lastError: Error | undefined; for (attempt = 0; attempt < config.maxRetries; attempt++) { - const result = yield* effect.pipe(Effect.either); + const result = yield* effect.pipe(Effect.result); - if (result._tag === "Right") { + if (result._tag === "Success") { yield* Effect.log(`[SUCCESS] Succeeded on attempt ${attempt + 1}`); - return result.right; + return result.success; } - lastError = result.left; + lastError = result.failure; if (attempt < config.maxRetries - 1) { const delay = calculateDelay(); @@ -189,18 +189,18 @@ Output demonstrates increasing delays with jitter: Use Effect's `Schedule` API for declarative exponential backoff: ```typescript -import { Effect, Schedule } from "effect"; +import { Duration, Effect, Schedule } from "effect"; const exponentialBackoffSchedule = (baseDelayMs: number, maxDelayMs: number) => Schedule.exponential(baseDelayMs).pipe( // Add jitter: Β±20% randomization - Schedule.jittered(0.2, 0.2), + Schedule.jittered, // Cap maximum delay - Schedule.mapDelay((delay) => Math.min(delay, maxDelayMs)), + Schedule.modifyDelay((meta) => + Effect.succeed(Duration.millis(Math.min(Duration.toMillis(meta.duration), maxDelayMs))) + ), // Log each retry attempt - Schedule.tapInput((error) => - Effect.log(`[RETRY] Retrying after error: ${error.message}`) - ) + Schedule.tap(({ input: error }) => Effect.log(`[RETRY] Retrying after error: ${error.message}`)) ); // Use in Effect.retry @@ -208,7 +208,7 @@ const robustApiCall = flakyApiCall().pipe( Effect.retry( exponentialBackoffSchedule(100, 5000).pipe( // Max 5 retries - Schedule.upTo(5) + Schedule.upTo({ times: 5 }) ) ), Effect.tap(() => Effect.log("[SUCCESS] API call succeeded")) @@ -237,10 +237,10 @@ const exponentialBackoffWithDeadline = (config: DeadlineConfig) => Date.now() - startTime > config.deadlineMs; while (!isDeadlineExceeded() && attempt < config.maxRetries) { - const result = yield* flakyApiCall().pipe(Effect.either); + const result = yield* flakyApiCall().pipe(Effect.result); - if (result._tag === "Right") { - return result.right; + if (result._tag === "Success") { + return result.success; } if (isDeadlineExceeded()) { @@ -323,13 +323,13 @@ const exponentialBackoffAdaptive = (config: AdaptiveBackoffConfig) => let lastError: Error | undefined; while (true) { - const result = yield* flakyApiCall().pipe(Effect.either); + const result = yield* flakyApiCall().pipe(Effect.result); - if (result._tag === "Right") { - return result.right; + if (result._tag === "Success") { + return result.success; } - lastError = result.left; + lastError = result.failure; const errorType = classifyError(lastError); if (errorType === ErrorType.Permanent) { @@ -430,9 +430,9 @@ const exponentialBackoffWithCircuitBreaker = ( } if (state === CircuitState.Closed || state === CircuitState.HalfOpen) { - const result = yield* flakyApiCall().pipe(Effect.either); + const result = yield* flakyApiCall().pipe(Effect.result); - if (result._tag === "Right") { + if (result._tag === "Success") { if (state === CircuitState.HalfOpen) { successCount++; if (successCount >= config.successThreshold) { @@ -443,7 +443,7 @@ const exponentialBackoffWithCircuitBreaker = ( failureCount = 0; } } - return result.right; + return result.success; } failureCount++; @@ -460,7 +460,7 @@ const exponentialBackoffWithCircuitBreaker = ( yield* Effect.sleep(`${Math.round(delay)} millis`); attempt++; } else { - yield* Effect.fail(result.left); + yield* Effect.fail(result.failure); } } } @@ -505,5 +505,5 @@ const exponentialBackoffWithCircuitBreaker = ( - [Retry Effects with Configuration](./retry-effects-with-configuration.mdx) - Configure retry strategies - [Handle Errors with Try-Catch Pattern](./handle-errors-with-try-catch-pattern.mdx) - Error handling basics -- [Understand Failure Handling with Either](./understand-failure-handling-with-either.mdx) - Either/Result pattern +- [Understand Failure Handling with Result](./understand-failure-handling-with-either.mdx) - Result/Result pattern - [Scheduling Pattern 1: Repeat on Fixed Interval](./scheduling-pattern-repeat-effect-on-fixed-interval.mdx) - Fixed intervals diff --git a/content/published/patterns/getting-started/getting-started-handle-errors.mdx b/content/published/patterns/getting-started/getting-started-handle-errors.mdx index 3cd74db6..aee0f685 100644 --- a/content/published/patterns/getting-started/getting-started-handle-errors.mdx +++ b/content/published/patterns/getting-started/getting-started-handle-errors.mdx @@ -1,20 +1,20 @@ --- -title: "Handle Your First Error with Effect.fail and catchAll" +title: "Handle Your First Error with Effect.fail and catch" id: getting-started-handle-errors skillLevel: beginner applicationPatternId: getting-started lessonOrder: 4 summary: >- Learn how to create Effects that can fail and how to recover from those - failures using Effect.fail and Effect.catchAll. + failures using Effect.fail and Effect.catch. tags: - getting-started - error-handling - fail - - catchAll + - catch - beginner rule: - description: Handle errors with Effect.fail and catchAll. + description: Handle errors with Effect.fail and catch. related: - getting-started-hello-world - combinator-error-handling @@ -22,12 +22,12 @@ related: author: Paul Philp --- -# Handle Your First Error with Effect.fail and catchAll +# Handle Your First Error with Effect.fail and catch ## Guideline Use `Effect.fail` to create an Effect that fails with an error, and -`Effect.catchAll` to recover from that failure. +`Effect.catch` to recover from that failure. ## Rationale @@ -66,7 +66,7 @@ const unsafeResult = divide(10, 0); // With error handling - recover with a default value const safeResult = pipe( divide(10, 0), - Effect.catchAll((error) => { + Effect.catch((error) => { console.log(`Caught error: ${error}`); return Effect.succeed(0); // Return 0 as fallback }) @@ -100,7 +100,7 @@ const safeDivide = (a: number, b: number): Effect.Effect { + Effect.catch((error) => { switch (error._tag) { case "DivisionByZero": return Effect.succeed(0); @@ -116,7 +116,7 @@ const result = pipe( | Function | Purpose | |----------|---------| | `Effect.fail(error)` | Create an Effect that fails | -| `Effect.catchAll(handler)` | Catch all errors and recover | +| `Effect.catch(handler)` | Catch all errors and recover | | `Effect.catchTag(tag, handler)` | Catch specific error by tag | | `Effect.orElse(fallback)` | Try a fallback Effect on error | | `Effect.orElseSucceed(value)` | Return a fallback value on error | @@ -151,7 +151,7 @@ console.log(result); // "Hello, Guest!" ## Key Points 1. **Effect.fail** creates a failing Effect - the error is part of the type -2. **Effect.catchAll** handles any error and must return an Effect +2. **Effect.catch** handles any error and must return an Effect 3. **Tagged errors** (with `_tag`) let you handle specific errors with `catchTag` 4. **Errors are values** - not thrown exceptions - making them composable diff --git a/content/published/patterns/getting-started/getting-started-retry-on-failure.mdx b/content/published/patterns/getting-started/getting-started-retry-on-failure.mdx index 5b993d14..1718150e 100644 --- a/content/published/patterns/getting-started/getting-started-retry-on-failure.mdx +++ b/content/published/patterns/getting-started/getting-started-retry-on-failure.mdx @@ -83,14 +83,14 @@ const quick = Effect.retry(operation, Schedule.recurs(5)); // Retry 3 times with 1 second between const spaced = Effect.retry( - operation, - Schedule.spaced("1 second").pipe(Schedule.intersect(Schedule.recurs(3))) + operation, + Schedule.max([Schedule.spaced("1 second"), Schedule.recurs(3)]) ); // Retry with exponential backoff (1s, 2s, 4s, 8s...) const exponential = Effect.retry( operation, - Schedule.exponential("1 second").pipe(Schedule.intersect(Schedule.recurs(5))) + Schedule.max([Schedule.exponential("1 second"), Schedule.recurs(5)]) ); ``` @@ -121,7 +121,7 @@ const fetchWithRetry = (userId: string) => Effect.retry( Schedule.recurs(3).pipe(Schedule.addDelay(() => "500 millis")) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed({ error: `Failed after retries: ${error._tag}` }) ) ); diff --git a/content/published/patterns/getting-started/getting-started-transform-with-map.mdx b/content/published/patterns/getting-started/getting-started-transform-with-map.mdx index d53cb8a0..14d438c9 100644 --- a/content/published/patterns/getting-started/getting-started-transform-with-map.mdx +++ b/content/published/patterns/getting-started/getting-started-transform-with-map.mdx @@ -112,5 +112,5 @@ Effect.runSync(Effect.all([getUserName, getDisplayName])); ## What's Next? - Learn `Effect.flatMap` for chaining Effects that return other Effects -- Learn error handling with `Effect.catchAll` +- Learn error handling with `Effect.catch` diff --git a/content/published/patterns/making-http-requests/build-a-basic-http-server.mdx b/content/published/patterns/making-http-requests/build-a-basic-http-server.mdx index a7f4bbaa..9371e1bf 100644 --- a/content/published/patterns/making-http-requests/build-a-basic-http-server.mdx +++ b/content/published/patterns/making-http-requests/build-a-basic-http-server.mdx @@ -49,7 +49,7 @@ This architecture ensures that your request handling logic is fully testable, be This example creates a simple server with a `Greeter` service. The server starts, creates a runtime containing the `Greeter`, and then uses that runtime to handle requests. ```typescript -import { HttpServer, HttpServerResponse } from "@effect/platform"; +import { HttpServer, HttpServerResponse } from "effect/http"; import { NodeHttpServer } from "@effect/platform-node"; import { Duration, Effect, Fiber, Layer } from "effect"; import { createServer } from "node:http"; @@ -67,7 +67,7 @@ const serverLayer = HttpServer.serve(app).pipe(Layer.provide(ServerLive)); const program = Effect.gen(function* () { yield* Effect.logInfo("Server starting on http://localhost:3001"); - const fiber = yield* Layer.launch(serverLayer).pipe(Effect.fork); + const fiber = yield* Layer.launch(serverLayer).pipe(Effect.forkChild); yield* Effect.sleep(Duration.seconds(2)); yield* Fiber.interrupt(fiber); yield* Effect.logInfo("Server shutdown complete"); diff --git a/content/published/patterns/making-http-requests/create-a-testable-http-client-service.mdx b/content/published/patterns/making-http-requests/create-a-testable-http-client-service.mdx index 2573ae14..8a7935ba 100644 --- a/content/published/patterns/making-http-requests/create-a-testable-http-client-service.mdx +++ b/content/published/patterns/making-http-requests/create-a-testable-http-client-service.mdx @@ -47,7 +47,7 @@ By abstracting the HTTP client into a service, you decouple your application's l ### 1. Define the Service ```typescript -import { Effect, Data, Layer } from "effect"; +import { Effect, Data, Layer, Context } from "effect"; interface HttpErrorType { readonly _tag: "HttpError"; @@ -60,14 +60,16 @@ interface HttpClientType { readonly get: (url: string) => Effect.Effect; } -class HttpClient extends Effect.Service()("HttpClient", { - sync: () => ({ +class HttpClient extends Context.Service()("HttpClient", { + make: Effect.sync(() => ({ get: (url: string): Effect.Effect => Effect.tryPromise(() => fetch(url).then((res) => res.json() as T) - ).pipe(Effect.catchAll((error) => Effect.fail(HttpError({ error })))), - }), -}) {} + ).pipe(Effect.catch((error) => Effect.fail(HttpError({ error })))), + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Test implementation const TestLayer = Layer.succeed( @@ -107,15 +109,17 @@ interface HttpClientType { readonly get: (url: string) => Effect.Effect; } -class HttpClient extends Effect.Service()("HttpClient", { - sync: () => ({ +class HttpClient extends Context.Service()("HttpClient", { + make: Effect.sync(() => ({ get: (url: string): Effect.Effect => Effect.tryPromise({ try: () => fetch(url).then((res) => res.json()), catch: (error) => HttpError({ error }), }), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Test implementation const TestLayer = Layer.succeed( diff --git a/content/published/patterns/making-http-requests/http-caching.mdx b/content/published/patterns/making-http-requests/http-caching.mdx index 5481f443..c92ab7e5 100644 --- a/content/published/patterns/making-http-requests/http-caching.mdx +++ b/content/published/patterns/making-http-requests/http-caching.mdx @@ -38,7 +38,7 @@ Caching provides: ```typescript import { Effect, Ref, HashMap, Option, Duration } from "effect" -import { HttpClient, HttpClientResponse } from "@effect/platform" +import { HttpClient, HttpClientResponse } from "effect/http" // ============================================ // 1. Simple in-memory cache @@ -93,7 +93,7 @@ const makeCache = () => interface CachedHttpClient { readonly get: ( url: string, - options?: { ttl?: Duration.DurationInput } + options?: { ttl?: Duration.Input } ) => Effect.Effect readonly invalidate: (url: string) => Effect.Effect } @@ -103,8 +103,8 @@ const makeCachedHttpClient = Effect.gen(function* () { const cache = yield* makeCache() const client: CachedHttpClient = { - get: (url: string, options?: { ttl?: Duration.DurationInput }) => { - const ttl = options?.ttl ? Duration.toMillis(Duration.decode(options.ttl)) : 60000 + get: (url: string, options?: { ttl?: Duration.Input }) => { + const ttl = options?.ttl ? Duration.toMillis(Duration.fromInputUnsafe(options.ttl)) : 60000 return Effect.gen(function* () { // Check cache first @@ -118,7 +118,7 @@ const makeCachedHttpClient = Effect.gen(function* () { // Fetch from network const response = yield* httpClient.get(url) - const data = yield* HttpClientResponse.json(response) as Effect.Effect + const data = yield* response.json as Effect.Effect // Store in cache yield* cache.set(url, data, ttl) @@ -152,14 +152,14 @@ const makeSWRClient = Effect.gen(function* () { get: ( url: string, options: { - staleAfter: Duration.DurationInput - expireAfter: Duration.DurationInput + staleAfter: Duration.Input + expireAfter: Duration.Input } ) => Effect.gen(function* () { const now = Date.now() - const staleMs = Duration.toMillis(Duration.decode(options.staleAfter)) - const expireMs = Duration.toMillis(Duration.decode(options.expireAfter)) + const staleMs = Duration.toMillis(Duration.fromInputUnsafe(options.staleAfter)) + const expireMs = Duration.toMillis(Duration.fromInputUnsafe(options.expireAfter)) const cached = yield* Ref.get(cache).pipe( Effect.map((map) => HashMap.get(map, url)) @@ -176,9 +176,9 @@ const makeSWRClient = Effect.gen(function* () { if (age < expireMs) { // Stale - return cached, revalidate in background - yield* Effect.fork( + yield* Effect.forkChild( httpClient.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.flatMap((data) => Ref.update(cache, (map) => HashMap.set(map, url, { @@ -189,7 +189,7 @@ const makeSWRClient = Effect.gen(function* () { }) ) ), - Effect.catchAll(() => Effect.void) // Ignore errors + Effect.catch(() => Effect.void) // Ignore errors ) ) return entry.data as T @@ -198,7 +198,7 @@ const makeSWRClient = Effect.gen(function* () { // Expired or missing - fetch fresh const response = yield* httpClient.get(url) - const data = yield* HttpClientResponse.json(response) as Effect.Effect + const data = yield* response.json as Effect.Effect yield* Ref.update(cache, (map) => HashMap.set(map, url, { @@ -244,7 +244,7 @@ const makeDeduplicatedClient = Effect.gen(function* () { // Make the request const request = httpClient.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.tap((data) => cache.set(url, data, ttl)), Effect.ensuring( Ref.update(inFlight, (map) => HashMap.remove(map, url)) diff --git a/content/published/patterns/making-http-requests/http-hello-world.mdx b/content/published/patterns/making-http-requests/http-hello-world.mdx index fdb1e000..ca482f4b 100644 --- a/content/published/patterns/making-http-requests/http-hello-world.mdx +++ b/content/published/patterns/making-http-requests/http-hello-world.mdx @@ -43,7 +43,7 @@ Effect's HttpClient is better than `fetch`: ```typescript import { Effect, Console } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" // ============================================ @@ -57,7 +57,7 @@ const simpleGet = Effect.gen(function* () { const response = yield* client.get("https://jsonplaceholder.typicode.com/posts/1") // Get response as JSON - const json = yield* HttpClientResponse.json(response) + const json = yield* response.json return json }) @@ -79,7 +79,7 @@ const getPost = (id: number) => const response = yield* client.get( `https://jsonplaceholder.typicode.com/posts/${id}` ) - const post = yield* HttpClientResponse.json(response) as Effect.Effect + const post = yield* response.json as Effect.Effect return post }) @@ -98,7 +98,7 @@ const createPost = (title: string, body: string) => ) const response = yield* client.execute(yield* request) - const created = yield* HttpClientResponse.json(response) + const created = yield* response.json return created }) @@ -109,7 +109,7 @@ const createPost = (title: string, body: string) => const safeGetPost = (id: number) => getPost(id).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Console.error(`Failed to fetch post ${id}: ${error}`) return { id, title: "Unavailable", body: "", userId: 0 } @@ -156,9 +156,9 @@ program.pipe( | Function | Purpose | |----------|---------| -| `HttpClientResponse.json` | Parse as JSON | -| `HttpClientResponse.text` | Get as text | -| `HttpClientResponse.arrayBuffer` | Get as bytes | +| `response.json` | Parse as JSON | +| `response.text` | Get as text | +| `response.arrayBuffer` | Get as bytes | | `response.status` | HTTP status code | | `response.headers` | Response headers | diff --git a/content/published/patterns/making-http-requests/http-json-responses.mdx b/content/published/patterns/making-http-requests/http-json-responses.mdx index 4f4adaa8..6bcbba14 100644 --- a/content/published/patterns/making-http-requests/http-json-responses.mdx +++ b/content/published/patterns/making-http-requests/http-json-responses.mdx @@ -43,7 +43,7 @@ Schema validation catches these issues immediately. ```typescript import { Effect, Console } from "effect" import { Schema } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" // ============================================ @@ -72,10 +72,10 @@ const getPost = (id: number) => const response = yield* client.get( `https://jsonplaceholder.typicode.com/posts/${id}` ) - const json = yield* HttpClientResponse.json(response) + const json = yield* response.json // Validate against schema - fails if data doesn't match - const post = yield* Schema.decodeUnknown(PostSchema)(json) + const post = yield* Schema.decodeUnknownEffect(PostSchema)(json) return post }) @@ -90,10 +90,10 @@ const getPosts = Effect.gen(function* () { const response = yield* client.get( "https://jsonplaceholder.typicode.com/posts" ) - const json = yield* HttpClientResponse.json(response) + const json = yield* response.json // Validate array of posts - const posts = yield* Schema.decodeUnknown(PostArraySchema)(json) + const posts = yield* Schema.decodeUnknownEffect(PostArraySchema)(json) return posts }) @@ -138,9 +138,9 @@ const getUser = (id: number) => const response = yield* client.get( `https://jsonplaceholder.typicode.com/users/${id}` ) - const json = yield* HttpClientResponse.json(response) + const json = yield* response.json - return yield* Schema.decodeUnknown(UserSchema)(json) + return yield* Schema.decodeUnknownEffect(UserSchema)(json) }) // ============================================ diff --git a/content/published/patterns/making-http-requests/http-logging.mdx b/content/published/patterns/making-http-requests/http-logging.mdx index ef324d40..545e33c9 100644 --- a/content/published/patterns/making-http-requests/http-logging.mdx +++ b/content/published/patterns/making-http-requests/http-logging.mdx @@ -39,7 +39,7 @@ HTTP logging helps with: ```typescript import { Effect, Duration } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" // ============================================ // 1. Simple request/response logging @@ -114,7 +114,7 @@ const makeLoggingClient = Effect.gen(function* () { response.headers ) - return yield* HttpClientResponse.json(response) as Effect.Effect + return yield* response.json as Effect.Effect }), post: (url: string, body: unknown, options?: { headers?: Record }) => @@ -136,7 +136,7 @@ const makeLoggingClient = Effect.gen(function* () { response.headers ) - return yield* HttpClientResponse.json(response) as Effect.Effect + return yield* response.json as Effect.Effect }), } }) @@ -150,7 +150,7 @@ const fetchWithSpan = (url: string) => const client = yield* HttpClient.HttpClient return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.withLogSpan(`HTTP GET ${url}`) ) }) @@ -183,7 +183,7 @@ const makeConditionalLoggingClient = (debug: boolean) => duration: `${Date.now() - startTime}ms`, }) - return yield* HttpClientResponse.json(response) as Effect.Effect + return yield* response.json as Effect.Effect }), } }) @@ -219,7 +219,7 @@ const makeTrackedClient = Effect.gen(function* () { }) ) - return yield* HttpClientResponse.json(response) as Effect.Effect + return yield* response.json as Effect.Effect }) } }) @@ -247,7 +247,7 @@ const fetchWithErrorLogging = (url: string) => } return Effect.succeed(response) }), - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.tapError((error) => Effect.logError("Request failed").pipe( Effect.annotateLogs({ diff --git a/content/published/patterns/making-http-requests/http-rate-limit-handling.mdx b/content/published/patterns/making-http-requests/http-rate-limit-handling.mdx index 7c824d97..ad2c9c5a 100644 --- a/content/published/patterns/making-http-requests/http-rate-limit-handling.mdx +++ b/content/published/patterns/making-http-requests/http-rate-limit-handling.mdx @@ -40,7 +40,7 @@ Respecting limits prevents bans and ensures reliable access. ```typescript import { Effect, Schedule, Duration, Data, Ref } from "effect" -import { HttpClient, HttpClientResponse } from "@effect/platform" +import { HttpClient, HttpClientResponse } from "effect/http" // ============================================ // 1. Rate limit error type @@ -117,18 +117,20 @@ const makeRateLimitAwareClient = Effect.gen(function* () { })) } - return yield* HttpClientResponse.json(response) as Effect.Effect + return yield* response.json as Effect.Effect }).pipe( Effect.retry({ - schedule: Schedule.recurWhile( - (e) => e._tag === "RateLimitedError" - ).pipe( - Schedule.intersect(Schedule.recurs(3)), - Schedule.delayed((_, error) => - Duration.seconds(error.retryAfter + 1) // Add 1s buffer + schedule: Schedule.max([ + Schedule.identity().pipe( + Schedule.while(({ input: e }) => e._tag === "RateLimitedError") + ), + Schedule.recurs(3) + ]).pipe( + Schedule.modifyDelay(({ input: error }) => + Effect.succeed(Duration.seconds(error.retryAfter + 1)) // Add 1s buffer ) ), - while: (error) => error._tag === "RateLimitedError", + while: (error) => error._tag === "RateLimitedError" }) ), } @@ -149,7 +151,7 @@ const makeClientRateLimiter = (requestsPerSecond: number) => const interval = 1000 / requestsPerSecond // Refill tokens periodically - yield* Effect.fork( + yield* Effect.forkChild( Effect.forever( Effect.gen(function* () { yield* Effect.sleep(Duration.millis(interval)) @@ -203,12 +205,9 @@ const makeRobustHttpClient = (requestsPerSecond: number) => return yield* Effect.fail(new Error("Rate limited")) } - return yield* HttpClientResponse.json(response) as Effect.Effect + return yield* response.json as Effect.Effect }).pipe( - Effect.retry( - Schedule.exponential("1 second").pipe( - Schedule.intersect(Schedule.recurs(3)) - ) + Effect.retry(Schedule.max([Schedule.exponential("1 second"), Schedule.recurs(3)]) ) ), } @@ -229,7 +228,7 @@ const batchRequests = ( for (const url of urls) { const response = yield* httpClient.get(url) - const data = yield* HttpClientResponse.json(response) as Effect.Effect + const data = yield* response.json as Effect.Effect results.push(data) // Wait between requests diff --git a/content/published/patterns/making-http-requests/http-retries.mdx b/content/published/patterns/making-http-requests/http-retries.mdx index 29728238..9c379c23 100644 --- a/content/published/patterns/making-http-requests/http-retries.mdx +++ b/content/published/patterns/making-http-requests/http-retries.mdx @@ -43,7 +43,7 @@ Proper retry logic handles these gracefully. ```typescript import { Effect, Schedule, Duration, Data } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse, HttpClientError } from "@effect/platform" +import { HttpClient, HttpClientRequest, HttpClientResponse, HttpClientError } from "effect/http" // ============================================ // 1. Basic retry with exponential backoff @@ -54,11 +54,11 @@ const fetchWithRetry = (url: string) => const client = yield* HttpClient.HttpClient return yield* client.get(url).pipe( - Effect.flatMap((response) => HttpClientResponse.json(response)), + Effect.flatMap((response) => response.json), Effect.retry( - Schedule.exponential("100 millis", 2).pipe( - Schedule.intersect(Schedule.recurs(5)), // Max 5 retries - Schedule.jittered // Add randomness + Schedule.max([Schedule.exponential("100 millis", 2), Schedule.recurs(5)]).pipe( + // Max 5 retries + Schedule.jittered // Add randomness ) ) ) @@ -106,14 +106,12 @@ const fetchWithSelectiveRetry = (url: string) => return Effect.succeed(response) }), Effect.retry({ - schedule: Schedule.exponential("200 millis").pipe( - Schedule.intersect(Schedule.recurs(3)) - ), - while: (error) => error._tag === "RetryableHttpError", + schedule: Schedule.max([Schedule.exponential("200 millis"), Schedule.recurs(3)]), + while: (error) => error._tag === "RetryableHttpError" }) ) - return yield* HttpClientResponse.json(response) + return yield* response.json }) // ============================================ @@ -125,11 +123,10 @@ const fetchWithRetryLogging = (url: string) => const client = yield* HttpClient.HttpClient return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.retry( - Schedule.exponential("100 millis").pipe( - Schedule.intersect(Schedule.recurs(3)), - Schedule.tapOutput((_, output) => + Schedule.max([Schedule.exponential("100 millis"), Schedule.recurs(3)]).pipe( + Schedule.tap(({ output }) => Effect.log(`Retry attempt, waiting ${Duration.toMillis(output)}ms`) ) ) @@ -142,12 +139,15 @@ const fetchWithRetryLogging = (url: string) => // 4. Custom retry policy // ============================================ -const customRetryPolicy = Schedule.exponential("500 millis", 2).pipe( - Schedule.intersect(Schedule.recurs(5)), - Schedule.union(Schedule.spaced("30 seconds")), // Also retry after 30s - Schedule.whileOutput((duration) => Duration.lessThanOrEqualTo(duration, "2 minutes")), +const customRetryPolicy = Schedule.min([ + Schedule.max([Schedule.exponential("500 millis", 2), Schedule.recurs(5)]), + Schedule.spaced("30 seconds") // Also retry after 30s +]).pipe( + Schedule.while(({ output: duration }) => + Duration.isLessThanOrEqualTo(duration, "2 minutes") + ), Schedule.jittered -) +); // ============================================ // 5. Retry respecting Retry-After header @@ -174,14 +174,18 @@ const fetchWithRetryAfter = (url: string) => return yield* makeRequest.pipe( Effect.retry( - Schedule.recurWhile<{ _tag: "RateLimited"; delay: number }>( - (error) => error._tag === "RateLimited" - ).pipe( - Schedule.intersect(Schedule.recurs(3)), - Schedule.delayed((_, error) => Duration.millis(error.delay)) + Schedule.max([ + Schedule.identity<{ _tag: "RateLimited"; delay: number }>().pipe( + Schedule.while(({ input: error }) => error._tag === "RateLimited") + ), + Schedule.recurs(3) + ]).pipe( + Schedule.modifyDelay(({ input: error }) => + Effect.succeed(Duration.millis(error.delay)) + ) ) ), - Effect.flatMap((r) => HttpClientResponse.json(r)) + Effect.flatMap((r) => r.json) ) }) @@ -193,7 +197,7 @@ const program = Effect.gen(function* () { yield* Effect.log("Fetching with retry...") const data = yield* fetchWithRetry("https://api.example.com/data").pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { return Effect.succeed({ error: "All retries exhausted" }) }) ) diff --git a/content/published/patterns/making-http-requests/http-timeouts.mdx b/content/published/patterns/making-http-requests/http-timeouts.mdx index 4eff3fba..9a9994ea 100644 --- a/content/published/patterns/making-http-requests/http-timeouts.mdx +++ b/content/published/patterns/making-http-requests/http-timeouts.mdx @@ -42,18 +42,18 @@ Timeouts prevent these from blocking your application. ```typescript import { Effect, Duration, Data } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" // ============================================ // 1. Basic request timeout // ============================================ -const fetchWithTimeout = (url: string, timeout: Duration.DurationInput) => +const fetchWithTimeout = (url: string, timeout: Duration.Input) => Effect.gen(function* () { const client = yield* HttpClient.HttpClient return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.timeout(timeout) ) // Returns Option - None if timed out @@ -68,17 +68,17 @@ class RequestTimeoutError extends Data.TaggedError("RequestTimeoutError")<{ readonly timeout: Duration.Duration }> {} -const fetchWithTimeoutError = (url: string, timeout: Duration.DurationInput) => +const fetchWithTimeoutError = (url: string, timeout: Duration.Input) => Effect.gen(function* () { const client = yield* HttpClient.HttpClient return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.timeoutFail({ duration: timeout, onTimeout: () => new RequestTimeoutError({ url, - timeout: Duration.decode(timeout), + timeout: Duration.fromInputUnsafe(timeout), }), }) ) @@ -100,7 +100,7 @@ const fetchWithPhasedTimeouts = (url: string) => ) // Read timeout (body) - const body = yield* HttpClientResponse.text(response).pipe( + const body = yield* response.text.pipe( Effect.timeout("30 seconds"), Effect.flatten, Effect.mapError(() => new Error("Read timeout")) @@ -123,7 +123,7 @@ const fetchWithFallback = (url: string): Effect.Effect => const client = yield* HttpClient.HttpClient return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.map((data) => ({ data, cached: false })), Effect.timeout("5 seconds"), Effect.flatMap((result) => @@ -143,7 +143,7 @@ const fetchWithInterrupt = (url: string) => const client = yield* HttpClient.HttpClient return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), + Effect.flatMap((r) => r.json), Effect.interruptible, Effect.timeout("10 seconds") ) @@ -155,9 +155,9 @@ const fetchWithInterrupt = (url: string) => // ============================================ interface TimeoutConfig { - readonly connect: Duration.DurationInput - readonly read: Duration.DurationInput - readonly total: Duration.DurationInput + readonly connect: Duration.Input + readonly read: Duration.Input + readonly total: Duration.Input } const defaultTimeouts: TimeoutConfig = { @@ -176,7 +176,7 @@ const createHttpClient = (config: TimeoutConfig = defaultTimeouts) => Effect.timeout(config.connect), Effect.flatten, Effect.flatMap((r) => - HttpClientResponse.json(r).pipe( + r.json.pipe( Effect.timeout(config.read), Effect.flatten ) diff --git a/content/published/patterns/making-http-requests/model-dependencies-as-services.mdx b/content/published/patterns/making-http-requests/model-dependencies-as-services.mdx index 988d1c82..53617f8a 100644 --- a/content/published/patterns/making-http-requests/model-dependencies-as-services.mdx +++ b/content/published/patterns/making-http-requests/model-dependencies-as-services.mdx @@ -35,15 +35,17 @@ This pattern is the key to testability. It allows you to provide a `Live` implem ## Good Example ```typescript -import { Effect } from "effect"; +import { Effect, Context, Layer } from "effect"; // Define Random service with production implementation as default -export class Random extends Effect.Service()("Random", { +export class Random extends Context.Service()("Random", { // Default production implementation - sync: () => ({ + make: Effect.sync(() => ({ next: Effect.sync(() => Math.random()), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Example usage const program = Effect.gen(function* () { @@ -54,7 +56,7 @@ const program = Effect.gen(function* () { // Run with default implementation const programWithLogging = Effect.gen(function* () { - const value = yield* Effect.provide(program, Random.Default); + const value = yield* Effect.provide(program, Random.layer); yield* Effect.log(`Random value: ${value}`); return value; }); diff --git a/content/published/patterns/observability/observability-alerting.mdx b/content/published/patterns/observability/observability-alerting.mdx index 769e2d14..b5a4fa8e 100644 --- a/content/published/patterns/observability/observability-alerting.mdx +++ b/content/published/patterns/observability/observability-alerting.mdx @@ -58,7 +58,7 @@ interface AlertRule { readonly severity: "critical" | "warning" | "info" readonly message: string readonly labels: Record - readonly forDuration: Duration.DurationInput + readonly forDuration: Duration.Input } // ============================================ @@ -222,7 +222,7 @@ const pagerDutyChannel: NotificationChannel = { const runAlertEvaluation = ( rules: AlertRule[], channels: NotificationChannel[], - interval: Duration.DurationInput + interval: Duration.Input ) => Effect.gen(function* () { const alertManager = yield* makeAlertManager diff --git a/content/published/patterns/observability/observability-debugging.mdx b/content/published/patterns/observability/observability-debugging.mdx index 49d1d1c9..0e3e276b 100644 --- a/content/published/patterns/observability/observability-debugging.mdx +++ b/content/published/patterns/observability/observability-debugging.mdx @@ -101,7 +101,7 @@ const debugErrors = riskyOperation(true).pipe( Effect.tapError((error) => Effect.log(`Operation failed: ${error.message}`)), // Provide a fallback - Effect.catchAll((error) => { + Effect.catch((error) => { return Effect.succeed(`Recovered from: ${error.message}`) }) ) diff --git a/content/published/patterns/observability/observability-distributed-tracing.mdx b/content/published/patterns/observability/observability-distributed-tracing.mdx index ff7a7d3c..9c2279e6 100644 --- a/content/published/patterns/observability/observability-distributed-tracing.mdx +++ b/content/published/patterns/observability/observability-distributed-tracing.mdx @@ -41,7 +41,7 @@ Distributed tracing shows the complete request journey: ```typescript import { Effect, Context, Layer } from "effect" -import { HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "@effect/platform" +import { HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "effect/http" // ============================================ // 1. Define trace context @@ -54,10 +54,9 @@ interface TraceContext { readonly sampled: boolean } -class CurrentTrace extends Context.Tag("CurrentTrace")< - CurrentTrace, - TraceContext ->() {} +class CurrentTrace extends Context.Service< + CurrentTrace, TraceContext +>()("CurrentTrace") {} // W3C Trace Context header names const TRACEPARENT_HEADER = "traceparent" diff --git a/content/published/patterns/observability/observability-prometheus.mdx b/content/published/patterns/observability/observability-prometheus.mdx index e77bad7c..fd06d974 100644 --- a/content/published/patterns/observability/observability-prometheus.mdx +++ b/content/published/patterns/observability/observability-prometheus.mdx @@ -39,7 +39,7 @@ Prometheus metrics enable: ```typescript import { Effect, Metric, MetricLabel, Duration } from "effect" -import { HttpServerResponse } from "@effect/platform" +import { HttpServerResponse } from "effect/http" // ============================================ // 1. Define application metrics diff --git a/content/published/patterns/platform/getting-started/platform-hello-world.mdx b/content/published/patterns/platform/getting-started/platform-hello-world.mdx index ffbb43fb..107336f3 100644 --- a/content/published/patterns/platform/getting-started/platform-hello-world.mdx +++ b/content/published/patterns/platform/getting-started/platform-hello-world.mdx @@ -43,8 +43,8 @@ Platform wraps system operations in Effect, giving you: ```typescript import { Effect } from "effect" -import { FileSystem } from "@effect/platform" -import { NodeContext, NodeRuntime } from "@effect/platform-node" +import { FileSystem } from "effect" +import { NodeServices, NodeRuntime } from "@effect/platform-node" // Read a file - returns Effect const readConfig = Effect.gen(function* () { @@ -77,7 +77,7 @@ const program = Effect.gen(function* () { // Run with Node.js platform program.pipe( - Effect.provide(NodeContext.layer), + Effect.provide(NodeServices.layer), NodeRuntime.runMain ) ``` diff --git a/content/published/patterns/platform/platform-filesystem-operations.mdx b/content/published/patterns/platform/platform-filesystem-operations.mdx index baa82ec6..b366dd3f 100644 --- a/content/published/patterns/platform/platform-filesystem-operations.mdx +++ b/content/published/patterns/platform/platform-filesystem-operations.mdx @@ -66,7 +66,7 @@ Real-world example: Process log files This example demonstrates reading, writing, and manipulating files. ```typescript -import { FileSystem, Effect, Stream } from "@effect/platform"; +import { FileSystem, Effect, Stream } from "effect"; import * as fs from "fs/promises"; const program = Effect.gen(function* () { @@ -239,10 +239,10 @@ const atomicWrite = (filePath: string, content: string) => // Backup original if exists const exists = yield* FileSystem.exists(filePath).pipe( - Effect.either + Effect.result ); - if (exists._tag === "Right" && exists.right) { + if (exists._tag === "Success" && exists.success) { yield* FileSystem.copy(filePath, backupPath); } @@ -271,11 +271,11 @@ const watchFile = (filePath: string) => yield* Effect.sleep("1 second"); const stats = yield* FileSystem.stat(filePath).pipe( - Effect.either + Effect.result ); - if (stats._tag === "Right") { - const currentModified = stats.right.mtimeMs; + if (stats._tag === "Success") { + const currentModified = stats.success.mtimeMs; if (currentModified > lastModified) { lastModified = currentModified; diff --git a/content/published/patterns/platform/platform-keyvaluestore-persistence.mdx b/content/published/patterns/platform/platform-keyvaluestore-persistence.mdx index 188e08c8..0f0ece72 100644 --- a/content/published/patterns/platform/platform-keyvaluestore-persistence.mdx +++ b/content/published/patterns/platform/platform-keyvaluestore-persistence.mdx @@ -66,7 +66,8 @@ Real-world example: Caching API responses This example demonstrates storing and retrieving persistent data. ```typescript -import { KeyValueStore, Effect } from "@effect/platform"; +import { Effect } from "effect"; +import { KeyValueStore } from "effect/persistence"; interface UserSession { readonly userId: string; diff --git a/content/published/patterns/platform/platform-pattern-advanced-filesystem.mdx b/content/published/patterns/platform/platform-pattern-advanced-filesystem.mdx index 05a64ce0..d43707d0 100644 --- a/content/published/patterns/platform/platform-pattern-advanced-filesystem.mdx +++ b/content/published/patterns/platform/platform-pattern-advanced-filesystem.mdx @@ -106,7 +106,7 @@ Solutions: This example demonstrates advanced file system patterns. ```typescript -import { Effect, Stream, Ref, FileSystem } from "@effect/platform"; +import { Effect, Stream, Ref, FileSystem } from "effect"; import * as Path from "node:path"; import * as FS from "node:fs"; import * as PromiseFS from "node:fs/promises"; @@ -427,7 +427,7 @@ const processBulkFiles = ( { ...p, completed: p.completed + 1 }, ]) ), - Effect.catchAll((error) => + Effect.catch((error) => Ref.modify(progress, (p) => [ undefined, { ...p, failed: p.failed + 1 }, diff --git a/content/published/patterns/platform/platform-pattern-command-execution.mdx b/content/published/patterns/platform/platform-pattern-command-execution.mdx index 06098b74..ebd6d27b 100644 --- a/content/published/patterns/platform/platform-pattern-command-execution.mdx +++ b/content/published/patterns/platform/platform-pattern-command-execution.mdx @@ -4,7 +4,7 @@ id: platform-pattern-command-execution skillLevel: intermediate applicationPatternId: platform summary: >- - Use Command module to execute shell commands, capture output, and handle exit + Use the ChildProcess module to execute shell commands, capture output, and handle exit codes, enabling integration with system tools and external programs. tags: - platform @@ -15,7 +15,7 @@ tags: - external-process rule: description: >- - Use Command to spawn and manage external processes, capturing output and + Use ChildProcess to spawn and manage external processes, capturing output and handling exit codes reliably with proper error handling. related: - platform-filesystem-operations @@ -27,14 +27,14 @@ lessonOrder: 1 ## Guideline -Execute shell commands with Command: +Execute shell commands with ChildProcess: - **Spawn**: Start external process - **Capture**: Get stdout/stderr/exit code - **Wait**: Block until completion - **Handle errors**: Exit codes indicate failure -Pattern: `Command.exec("command args").pipe(...)` +Pattern: `ChildProcess.make("command", ["args"]).pipe(...)` --- @@ -48,7 +48,7 @@ Shell integration without proper handling causes issues: - **Output loss**: stderr ignored - **Race conditions**: Unsafe concurrent execution -Command enables: +ChildProcess enables: - **Type-safe execution**: Success/failure handled in Effect - **Output capture**: Both stdout and stderr available @@ -57,7 +57,7 @@ Command enables: Real-world example: Build pipeline - **Direct**: Process spawned, output mixed with app logs, exit code ignored -- **With Command**: Output captured, exit code checked, errors propagated +- **With ChildProcess**: Output captured, exit code checked, errors propagated --- @@ -66,17 +66,22 @@ Real-world example: Build pipeline This example demonstrates executing commands and handling their output. ```typescript -import { Command, Effect, Chunk } from "@effect/platform"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import { NodeServices } from "@effect/platform-node"; +import { Array, Effect, Stream } from "effect"; // Simple command execution const program = Effect.gen(function* () { + // Access the spawner service that runs child processes + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + console.log(`\n[COMMAND] Executing shell commands\n`); // Example 1: List files console.log(`[1] List files in current directory:\n`); - const lsResult = yield* Command.make("ls", ["-la"]).pipe( - Command.string + const lsResult = yield* spawner.string( + ChildProcess.make("ls", ["-la"]) ); console.log(lsResult); @@ -84,8 +89,8 @@ const program = Effect.gen(function* () { // Example 2: Get current date console.log(`\n[2] Get current date:\n`); - const dateResult = yield* Command.make("date", ["+%Y-%m-%d %H:%M:%S"]).pipe( - Command.string + const dateResult = yield* spawner.string( + ChildProcess.make("date", ["+%Y-%m-%d %H:%M:%S"]) ); console.log(`Current date: ${dateResult.trim()}`); @@ -93,49 +98,42 @@ const program = Effect.gen(function* () { // Example 3: Capture exit code console.log(`\n[3] Check if file exists:\n`); - const fileCheckCmd = yield* Command.make("test", [ - "-f", - "/etc/passwd", - ]).pipe( - Command.exitCode, - Effect.either + const fileCheckCmd = yield* spawner.exitCode( + ChildProcess.make("test", ["-f", "/etc/passwd"]) + ).pipe( + Effect.result ); - if (fileCheckCmd._tag === "Right") { + if (fileCheckCmd._tag === "Success") { console.log(`βœ“ File exists (exit code: 0)`); } else { - console.log(`βœ— File not found (exit code: ${fileCheckCmd.left})`); + console.log(`βœ— File not found (exit code: ${fileCheckCmd.failure})`); } // Example 4: Execute with custom working directory console.log(`\n[4] List TypeScript files:\n`); - const findResult = yield* Command.make("find", [ - ".", - "-name", - "*.ts", - "-type", - "f", - ]).pipe( - Command.lines + const findResult = yield* spawner.lines( + ChildProcess.make("find", [".", "-name", "*.ts", "-type", "f"]) ); - const tsFiles = Chunk.take(findResult, 5); // First 5 + const tsFiles = Array.take(findResult, 5); // First 5 - Chunk.forEach(tsFiles, (file) => { + Array.forEach(tsFiles, (file) => { console.log(` - ${file}`); }); - if (Chunk.size(findResult) > 5) { - console.log(` ... and ${Chunk.size(findResult) - 5} more`); + if (findResult.length > 5) { + console.log(` ... and ${findResult.length - 5} more`); } // Example 5: Handle command failure console.log(`\n[5] Handle command failure gracefully:\n`); - const failResult = yield* Command.make("false").pipe( - Command.exitCode, - Effect.catchAll((error) => + const failResult = yield* spawner.exitCode( + ChildProcess.make("false") + ).pipe( + Effect.catch((error) => Effect.succeed(-1) // Return -1 for any error ) ); @@ -143,7 +141,8 @@ const program = Effect.gen(function* () { console.log(`Exit code: ${failResult}`); }); -Effect.runPromise(program); +// Provide the platform spawner implementation to run child processes +Effect.runPromise(program.pipe(Effect.provide(NodeServices.layer))); ``` --- @@ -153,14 +152,17 @@ Effect.runPromise(program); Chain command executions: ```typescript +import { ChildProcess, ChildProcessSpawner } from "effect/process"; + const buildPipeline = Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + console.log(`[BUILD] Starting build pipeline\n`); // Step 1: Clean console.log(`[STEP 1] Cleaning...\n`); - yield* Command.make("rm", ["-rf", "dist"]).pipe( - Command.exitCode, + yield* spawner.exitCode(ChildProcess.make("rm", ["-rf", "dist"])).pipe( Effect.tap((code) => Effect.log(`Clean: exit code ${code}`) ) @@ -169,9 +171,10 @@ const buildPipeline = Effect.gen(function* () { // Step 2: Compile console.log(`[STEP 2] Compiling...\n`); - const compileOutput = yield* Command.make("tsc", []).pipe( - Command.string, - Effect.catchAll((error) => { + const compileOutput = yield* spawner.string( + ChildProcess.make("tsc", []) + ).pipe( + Effect.catch((error) => { console.error(`Compile failed: ${error}`); return Effect.fail(error); }) @@ -182,8 +185,8 @@ const buildPipeline = Effect.gen(function* () { // Step 3: Test console.log(`[STEP 3] Running tests...\n`); - const testOutput = yield* Command.make("npm", ["test"]).pipe( - Command.string + const testOutput = yield* spawner.string( + ChildProcess.make("npm", ["test"]) ); yield* Effect.log(`Tests: ${testOutput.includes("pass") ? "βœ“" : "βœ—"}`); @@ -202,27 +205,36 @@ const buildPipeline = Effect.gen(function* () { Process output line-by-line: ```typescript -const streamCommandOutput = ( +import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import { Effect, Stream } from "effect"; + +// Build a stream of command output lines. The spawner service runs the +// process; `streamLines` handles backpressure while the process runs. +const streamCommandOutput = Effect.fn("streamCommandOutput")(function*( command: string, args: string[] -): Stream.Stream => - Command.make(command, args).pipe( - Command.lines, - Stream.fromChunk - ); +) { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + return spawner.streamLines(ChildProcess.make(command, args)); +}); // Usage: Process log file line-by-line -const logProcessing = streamCommandOutput("tail", ["-f", "/var/log/system.log"]).pipe( +const logProcessing = Effect.gen(function* () { + const lines = yield* streamCommandOutput("tail", ["-f", "/var/log/system.log"]); + yield* lines.pipe( Stream.filter((line) => line.includes("ERROR")), Stream.tap((line) => Effect.log(`[ERROR LOG] ${line}`) ), Stream.take(10), Stream.runDrain -); + ); +}); // Usage: Process command output with transformation -const fileStats = streamCommandOutput("ls", ["-lh"]).pipe( +const fileStats = Effect.gen(function* () { + const lines = yield* streamCommandOutput("ls", ["-lh"]); + yield* lines.pipe( Stream.drop(1), // Skip header Stream.map((line) => { const parts = line.split(/\s+/); @@ -232,7 +244,8 @@ const fileStats = streamCommandOutput("ls", ["-lh"]).pipe( Effect.log(`File: ${stat.name} (${stat.size})`) ), Stream.runDrain -); + ); +}); ``` --- @@ -242,15 +255,19 @@ const fileStats = streamCommandOutput("ls", ["-lh"]).pipe( Set environment for command execution: ```typescript +import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import { Effect } from "effect"; + const commandWithEnv = Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + // Execute command with custom environment - const result = yield* Command.make("printenv", ["MY_VAR"]).pipe( - Command.env((env) => ({ - ...env, - MY_VAR: "custom-value", - NODE_ENV: "production", - })), - Command.string, + const result = yield* spawner.string( + ChildProcess.make("printenv", ["MY_VAR"], { + env: { MY_VAR: "custom-value", NODE_ENV: "production" }, + extendEnv: true + }) + ).pipe( Effect.map((output) => output.trim()) ); @@ -259,13 +276,13 @@ const commandWithEnv = Effect.gen(function* () { // Usage: Build with environment variables const buildWithEnv = Effect.gen(function* () { - const result = yield* Command.make("npm", ["run", "build"]).pipe( - Command.env((env) => ({ - ...env, - NODE_ENV: "production", - SKIP_TESTS: "true", - })), - Command.exitCode + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + + const result = yield* spawner.exitCode( + ChildProcess.make("npm", ["run", "build"], { + env: { NODE_ENV: "production", SKIP_TESTS: "true" }, + extendEnv: true + }) ); yield* Effect.log(`Build exit code: ${result}`); @@ -279,12 +296,17 @@ const buildWithEnv = Effect.gen(function* () { Run multiple commands concurrently: ```typescript +import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import { Effect } from "effect"; + const parallelCommands = Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + console.log(`[PARALLEL] Running 3 commands concurrently\n`); - const cmd1 = Command.make("npm", ["list"]).pipe(Command.string); - const cmd2 = Command.make("git", ["status"]).pipe(Command.string); - const cmd3 = Command.make("node", ["-v"]).pipe(Command.string); + const cmd1 = spawner.string(ChildProcess.make("npm", ["list"])); + const cmd2 = spawner.string(ChildProcess.make("git", ["status"])); + const cmd3 = spawner.string(ChildProcess.make("node", ["-v"])); // Execute in parallel const [result1, result2, result3] = yield* Effect.all( @@ -305,15 +327,20 @@ const parallelCommands = Effect.gen(function* () { Set execution timeouts: ```typescript +import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import { Duration, Effect } from "effect"; + const commandWithTimeout = ( command: string, args: string[], timeoutMs: number ) => - Command.make(command, args).pipe( - Command.string, - Effect.timeout(`${timeoutMs} millis`), - Effect.catchAll((error) => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + return yield* spawner.string(ChildProcess.make(command, args)); + }).pipe( + Effect.timeout(Duration.millis(timeoutMs)), + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log( `Command timed out after ${timeoutMs}ms` @@ -328,7 +355,7 @@ const buildWithTimeout = commandWithTimeout("npm", ["run", "build"], 30000).pipe Effect.tap((output) => Effect.log(`Build completed:\n${output}`) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.log(`Build failed: ${error.message}`) ) ); @@ -341,23 +368,30 @@ const buildWithTimeout = commandWithTimeout("npm", ["run", "build"], 30000).pipe Retry failed commands: ```typescript +import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import { Effect, Schedule } from "effect"; + const commandWithRetry = ( command: string, args: string[], maxRetries: number ) => - Command.make(command, args).pipe( - Command.exitCode, + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + return yield* spawner.exitCode(ChildProcess.make(command, args)); + }).pipe( Effect.retry( - Schedule.exponential("100 millis").pipe( - Schedule.upTo(`5 seconds`), - Schedule.compose(Schedule.recurs(maxRetries)) - ) + Schedule.max([ + Schedule.exponential("100 millis").pipe( + Schedule.upTo({ duration: `5 seconds` }) + ), + Schedule.recurs(maxRetries) + ]) ), Effect.tap((code) => Effect.log(`Command succeeded with exit code ${code}`) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.log(`Command failed after ${maxRetries} retries`) ) ); @@ -374,7 +408,7 @@ const flakyCurl = commandWithRetry( ## When to Use This Pattern -βœ… **Use Command when:** +βœ… **Use ChildProcess when:** - Executing external programs - Running shell scripts @@ -398,16 +432,16 @@ const flakyCurl = commandWithRetry( **Avoid shell injection:** ```typescript // ❌ UNSAFE - Input not escaped -Command.make("echo", [`Hello ${userInput}`]) +ChildProcess.make("echo", [`Hello ${userInput}`]) // βœ… SAFE - Input as separate argument -Command.make("echo", [userInput]) +ChildProcess.make("echo", [userInput]) ``` **Validate/sanitize inputs:** ```typescript const safePath = path.replace(/[^\w.-]/g, ''); // Remove special chars -Command.make("ls", [safePath]) +ChildProcess.make("ls", [safePath]) ``` --- diff --git a/content/published/patterns/platform/platform-pattern-path-manipulation.mdx b/content/published/patterns/platform/platform-pattern-path-manipulation.mdx index 397266a0..8a847b28 100644 --- a/content/published/patterns/platform/platform-pattern-path-manipulation.mdx +++ b/content/published/patterns/platform/platform-pattern-path-manipulation.mdx @@ -88,7 +88,7 @@ Solutions: This example demonstrates cross-platform path manipulation. ```typescript -import { Effect, FileSystem } from "@effect/platform"; +import { Effect, FileSystem } from "effect"; import * as Path from "node:path"; import * as OS from "node:os"; diff --git a/content/published/patterns/platform/platform-terminal-interactive.mdx b/content/published/patterns/platform/platform-terminal-interactive.mdx index 8db64683..40b673a0 100644 --- a/content/published/patterns/platform/platform-terminal-interactive.mdx +++ b/content/published/patterns/platform/platform-terminal-interactive.mdx @@ -66,7 +66,7 @@ Real-world example: CLI setup wizard This example demonstrates building an interactive CLI application. ```typescript -import { Terminal, Effect } from "@effect/platform"; +import { Terminal, Effect } from "effect"; interface UserInput { readonly name: string; diff --git a/content/published/patterns/resource-management/compose-scoped-layers.mdx b/content/published/patterns/resource-management/compose-scoped-layers.mdx index f722d1ae..0e3630d2 100644 --- a/content/published/patterns/resource-management/compose-scoped-layers.mdx +++ b/content/published/patterns/resource-management/compose-scoped-layers.mdx @@ -44,35 +44,39 @@ This automates one of the most complex and error-prone parts of application arch ## Good Example ```typescript -import { Effect, Layer, Console } from "effect"; +import { Effect, Layer, Console, Context } from "effect"; // --- Service 1: Database --- interface DatabaseOps { query: (sql: string) => Effect.Effect; } -class Database extends Effect.Service()("Database", { - sync: () => ({ +class Database extends Context.Service()("Database", { + make: Effect.sync(() => ({ query: (sql: string): Effect.Effect => Effect.sync(() => `db says: ${sql}`), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // --- Service 2: API Client --- interface ApiClientOps { fetch: (path: string) => Effect.Effect; } -class ApiClient extends Effect.Service()("ApiClient", { - sync: () => ({ +class ApiClient extends Context.Service()("ApiClient", { + make: Effect.sync(() => ({ fetch: (path: string): Effect.Effect => Effect.sync(() => `api says: ${path}`), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // --- Application Layer --- // We merge the two independent layers into one. -const AppLayer = Layer.merge(Database.Default, ApiClient.Default); +const AppLayer = Layer.merge(Database.layer, ApiClient.layer); // This program uses both services, unaware of their implementation details. const program = Effect.gen(function* () { diff --git a/content/published/patterns/resource-management/create-managed-runtime-for-scoped-resources.mdx b/content/published/patterns/resource-management/create-managed-runtime-for-scoped-resources.mdx index 2ab24118..57bc69a8 100644 --- a/content/published/patterns/resource-management/create-managed-runtime-for-scoped-resources.mdx +++ b/content/published/patterns/resource-management/create-managed-runtime-for-scoped-resources.mdx @@ -27,7 +27,7 @@ lessonOrder: 1 ## Guideline For services that manage resources needing explicit cleanup (e.g., a database -connection), define them in a `Layer` using `Layer.scoped`. Then, use +connection), define them in a `Layer` using `Layer.effect`. Then, use `Layer.launch` to provide this layer to your application. ## Rationale @@ -39,16 +39,18 @@ finalizers are executed upon completion or interruption. ## Good Example ```typescript -import { Effect, Layer } from "effect"; +import { Effect, Layer, Context } from "effect"; -class DatabasePool extends Effect.Service()("DbPool", { - effect: Effect.gen(function* () { +class DatabasePool extends Context.Service()("DbPool", { + make: Effect.gen(function* () { yield* Effect.log("Acquiring pool"); return { query: () => Effect.succeed("result"), }; }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create a program that uses the DatabasePool service const program = Effect.gen(function* () { @@ -59,7 +61,7 @@ const program = Effect.gen(function* () { // Run the program with the service implementation Effect.runPromise( - program.pipe(Effect.provide(DatabasePool.Default), Effect.scoped) + program.pipe(Effect.provide(DatabasePool.layer), Effect.scoped) ); ``` diff --git a/content/published/patterns/resource-management/manual-scope-management.mdx b/content/published/patterns/resource-management/manual-scope-management.mdx index bd381678..13898017 100644 --- a/content/published/patterns/resource-management/manual-scope-management.mdx +++ b/content/published/patterns/resource-management/manual-scope-management.mdx @@ -32,7 +32,7 @@ For complex scenarios where a resource's lifecycle doesn't fit a simple `acquire ## Rationale -While `Effect.acquireRelease` and `Layer.scoped` are sufficient for most use cases, sometimes you need more control. This pattern is essential when: +While `Effect.acquireRelease` and `Layer.effect` are sufficient for most use cases, sometimes you need more control. This pattern is essential when: 1. A single logical operation acquires multiple resources that need independent cleanup. 2. You are building a custom, complex `Layer` that orchestrates several dependent resources. @@ -43,7 +43,7 @@ By interacting with `Scope` directly, you gain precise, imperative-style control ## Good Example ```typescript -import { Effect, Console } from "effect"; +import { Effect, Console, Layer } from "effect"; // Mocking a complex file operation const openFile = (path: string) => diff --git a/content/published/patterns/resource-management/resource-management-runtime-vs-provide.mdx b/content/published/patterns/resource-management/resource-management-runtime-vs-provide.mdx index 2aad3fec..6d3fa8ba 100644 --- a/content/published/patterns/resource-management/resource-management-runtime-vs-provide.mdx +++ b/content/published/patterns/resource-management/resource-management-runtime-vs-provide.mdx @@ -33,21 +33,23 @@ A runtime bakes your layers into a reusable execution environment. You call `run ## Good Example ```typescript -import { Effect, Layer, ManagedRuntime } from "effect" +import { Effect, Layer, ManagedRuntime, Context } from "effect" -class Db extends Effect.Service()("Db", { - sync: () => ({ query: () => Effect.succeed("data") }), -}) {} +class Db extends Context.Service()("Db", { + make: Effect.sync(() => ({ query: () => Effect.succeed("data") })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // One-off: provide at the edge const oneOff = Effect.gen(function* () { const db = yield* Db return yield* db.query() }) -Effect.runPromise(Effect.provide(oneOff, Db.Default)) +Effect.runPromise(Effect.provide(oneOff, Db.layer)) // Many runs: create runtime once -const runtime = ManagedRuntime.make(Db.Default) +const runtime = ManagedRuntime.make(Db.layer) const effect = Effect.gen(function* () { const db = yield* Db return yield* db.query() diff --git a/content/published/patterns/resource-management/resource-timeouts.mdx b/content/published/patterns/resource-management/resource-timeouts.mdx index ffb6f28e..fc5a9df3 100644 --- a/content/published/patterns/resource-management/resource-timeouts.mdx +++ b/content/published/patterns/resource-management/resource-timeouts.mdx @@ -132,7 +132,7 @@ const program = Effect.gen(function* () { yield* Effect.log("=== Testing timeouts ===") const result = yield* entireOperationWithTimeout.pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.logError(`Failed: ${error.message}`) return [] diff --git a/content/published/patterns/resource-management/scoped-service-layer.mdx b/content/published/patterns/resource-management/scoped-service-layer.mdx index 5a38bbb8..ec6df13e 100644 --- a/content/published/patterns/resource-management/scoped-service-layer.mdx +++ b/content/published/patterns/resource-management/scoped-service-layer.mdx @@ -4,7 +4,7 @@ id: scoped-service-layer skillLevel: intermediate applicationPatternId: resource-management summary: >- - Use `Layer.scoped` with `Effect.Service` to transform a managed resource into + Use `Layer.effect` with `Context.Service` to transform a managed resource into a shareable, application-wide service. tags: - resource @@ -15,7 +15,7 @@ tags: - context - acquire-release rule: - description: Provide a managed resource to the application context using `Layer.scoped`. + description: Provide a managed resource to the application context using `Layer.effect`. related: - acquire-release-bracket author: PaulJPhilp @@ -26,16 +26,16 @@ lessonOrder: 2 ## Guideline -Define a service using `class MyService extends Effect.Service(...)`. Implement the service using the `scoped` property of the service class. This property should be a scoped `Effect` (typically from `Effect.acquireRelease`) that builds and releases the underlying resource. +Define a service using `class MyService extends Context.Service(...)`. Implement the service using the `make` option of the service class, then expose it with `static readonly layer = Layer.effect(this, this.make)`. The `make` effect should be scoped (typically from `Effect.acquireRelease`) so it builds and releases the underlying resource. ## Rationale -This pattern is the key to building robust, testable, and leak-proof applications in Effect. It elevates a managed resource into a first-class service that can be used anywhere in your application. The `Effect.Service` helper simplifies defining the service's interface and context key. This approach decouples your business logic from the concrete implementation, as the logic only depends on the abstract service. The `Layer` declaratively handles the resource's entire lifecycle, ensuring it is acquired lazily, shared safely, and released automatically. +This pattern is the key to building robust, testable, and leak-proof applications in Effect. It elevates a managed resource into a first-class service that can be used anywhere in your application. The `Context.Service` helper simplifies defining the service's interface and context key. This approach decouples your business logic from the concrete implementation, as the logic only depends on the abstract service. The `Layer` declaratively handles the resource's entire lifecycle, ensuring it is acquired lazily, shared safely, and released automatically. ## Good Example ```typescript -import { Effect, Console } from "effect"; +import { Effect, Console, Layer, Context } from "effect"; // 1. Define the service interface interface DatabaseService { @@ -43,9 +43,9 @@ interface DatabaseService { } // 2. Define the service implementation with scoped resource management -class Database extends Effect.Service()("Database", { - // The scoped property manages the resource lifecycle - scoped: Effect.gen(function* () { +class Database extends Context.Service()("Database", { + // The make effect manages the resource lifecycle + make: Effect.gen(function* () { const id = Math.floor(Math.random() * 1000); // Acquire the connection @@ -60,7 +60,9 @@ class Database extends Effect.Service()("Database", { Effect.sync(() => [`Result for '${sql}' from pool ${id}`]), }; }), -}) {} +}) { + static readonly layer = Layer.effect(this, this.make) +} // 3. Use the service in your program const program = Effect.gen(function* () { @@ -71,7 +73,7 @@ const program = Effect.gen(function* () { // 4. Run the program with scoped resource management Effect.runPromise( - Effect.scoped(program).pipe(Effect.provide(Database.Default)) + Effect.scoped(program).pipe(Effect.provide(Database.layer)) ); /* @@ -83,7 +85,7 @@ Query successful: Result for 'SELECT * FROM users' from pool 458 ``` **Explanation:** -The `Effect.Service` helper creates the `Database` class, which acts as both the service definition and its context key (Tag). The `Database.Live` layer connects this service to a concrete, lifecycle-managed implementation. When `program` asks for the `Database` service, the Effect runtime uses the `Live` layer to run the `acquire` effect once, caches the resulting `DbPool`, and injects it. The `release` effect is automatically run when the program completes. +The `Context.Service` helper creates the `Database` class, which acts as both the service definition and its context key. The `Database.layer` layer (built with `Layer.effect`) connects this service to a concrete, lifecycle-managed implementation. When `program` asks for the `Database` service, the Effect runtime uses the `layer` to run the `acquire` effect once, caches the result, and injects it. The `release` effect is automatically run when the program completes. ## Anti-Pattern diff --git a/content/published/patterns/scheduling/scheduling-hello-world.mdx b/content/published/patterns/scheduling/scheduling-hello-world.mdx index cb7fdfd3..65e740cc 100644 --- a/content/published/patterns/scheduling/scheduling-hello-world.mdx +++ b/content/published/patterns/scheduling/scheduling-hello-world.mdx @@ -82,10 +82,7 @@ const repeated = logTime.pipe( // Repeat every second, 5 times const polling = logTime.pipe( - Effect.repeat( - Schedule.spaced("1 second").pipe( - Schedule.intersect(Schedule.recurs(5)) - ) + Effect.repeat(Schedule.max([Schedule.spaced("1 second"), Schedule.recurs(5)]) ) ) @@ -103,9 +100,7 @@ const exponentialBackoff = Schedule.exponential("1 second") const limitedAttempts = Schedule.recurs(3) // Combine: exponential backoff, max 5 attempts -const retryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.intersect(Schedule.recurs(5)) -) +const retryPolicy = Schedule.max([Schedule.exponential("100 millis"), Schedule.recurs(5)]) // ============================================ // 5. Run examples @@ -131,15 +126,15 @@ Effect.runPromise(program) | `Schedule.spaced(duration)` | Wait duration between runs | | `Schedule.exponential(base)` | Double the delay each time | | `Schedule.forever` | Run indefinitely | -| `Schedule.once` | Run exactly once | +| `Schedule.duration(Duration.zero)` | Run exactly once | ## Combining Schedules | Combinator | Meaning | |------------|---------| -| `intersect` | Both conditions must be true | -| `union` | Either condition can be true | -| `andThen` | First schedule, then second | +| `Schedule.max([...])` | All schedules must continue (slowest delay wins) | +| `Schedule.min([...])` | Any schedule may continue (fastest delay wins) | +| `Schedule.concat` | First schedule, then second | ## When to Use diff --git a/content/published/patterns/scheduling/scheduling-pattern-advanced-retry-chains.mdx b/content/published/patterns/scheduling/scheduling-pattern-advanced-retry-chains.mdx index 8661fed8..cb1ecdb4 100644 --- a/content/published/patterns/scheduling/scheduling-pattern-advanced-retry-chains.mdx +++ b/content/published/patterns/scheduling/scheduling-pattern-advanced-retry-chains.mdx @@ -87,7 +87,7 @@ Solutions: This example demonstrates circuit breaker and fallback chain patterns. ```typescript -import { Effect, Schedule, Ref, Data } from "effect"; +import { Duration, Effect, Schedule, Ref, Data } from "effect"; // Error classification class RetryableError extends Data.TaggedError("RetryableError")<{ @@ -252,7 +252,7 @@ const program = Effect.gen(function* () { for (const shouldFail of failSequence) { yield* callWithCircuitBreaker(shouldFail).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (error._tag === "CircuitBreakerOpenError") { yield* Effect.log( @@ -399,27 +399,23 @@ const createComplexRetryStrategy = (config: { maxDelayMs: number; timeoutMs: number; }) => - Schedule.recurse((attempt) => { - if (attempt >= config.maxAttempts) { - return Schedule.stop; - } - - // Exponential backoff with jitter - const delay = Math.min( - config.maxDelayMs, - config.baseDelayMs * Math.pow(2, attempt) + - Math.random() * config.baseDelayMs - ); - - // Add jitter to prevent thundering herd - const jitter = Math.random() * 0.1 * delay; - - return Schedule.delay( - Duration.millis(Math.ceil(delay + jitter)) - ); - }).pipe( - Schedule.upTo(Duration.millis(config.timeoutMs)), + Schedule.max([ + Schedule.exponential(config.baseDelayMs).pipe( + // Exponential backoff with cap and jitter + Schedule.modifyDelay((meta) => + Effect.succeed( + Duration.millis( + Math.min( + config.maxDelayMs, + Duration.toMillis(meta.duration) + Math.random() * config.baseDelayMs + ) + ) + ) + ) + ), Schedule.recurs(config.maxAttempts) + ]).pipe( + Schedule.upTo({ duration: Duration.millis(config.timeoutMs) }) ); // Usage with error filtering @@ -431,7 +427,7 @@ const robustCall = operation.pipe( maxDelayMs: 5000, timeoutMs: 30000, }).pipe( - Schedule.filter((error) => { + Schedule.while(({ input: error }) => { // Don't retry non-retryable errors if (error instanceof NonRetryableError) { return false; @@ -472,7 +468,7 @@ const createHealthAwareRetry = (config: { return healthy; }).pipe( Effect.timeout(Duration.millis(config.healthCheckTimeoutMs)), - Effect.catchAll(() => + Effect.catch(() => Effect.gen(function* () { yield* Effect.log("[HEALTH] Check timed out, assuming unhealthy"); return false; @@ -511,25 +507,22 @@ const trackRetryMetrics = (operation: Effect.Effect) => const result = yield* operation.pipe( Effect.retry( - Schedule.exponential("100 millis").pipe( - Schedule.compose( - Schedule.recurs(3), - Schedule.tapOutput((delay) => - Effect.gen(function* () { - yield* Ref.modify(metrics, (m) => [ - undefined, - { - ...m, - attempts: m.attempts + 1, - totalDelayMs: m.totalDelayMs + delay.millis, - }, - ]); - }) - ) + Schedule.max([Schedule.exponential("100 millis"), Schedule.recurs(3)]).pipe( + Schedule.tap(({ output: delay }) => + Effect.gen(function* () { + yield* Ref.modify(metrics, (m) => [ + undefined, + { + ...m, + attempts: m.attempts + 1, + totalDelayMs: m.totalDelayMs + Duration.toMillis(delay) + } + ]); + }) ) ) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Ref.modify(metrics, (m) => [ undefined, diff --git a/content/published/patterns/scheduling/scheduling-pattern-cron-expressions.mdx b/content/published/patterns/scheduling/scheduling-pattern-cron-expressions.mdx index 8aec9930..0c601445 100644 --- a/content/published/patterns/scheduling/scheduling-pattern-cron-expressions.mdx +++ b/content/published/patterns/scheduling/scheduling-pattern-cron-expressions.mdx @@ -160,8 +160,7 @@ const scheduleWithCron = (config: ReportConfig) => // Create schedule that checks every minute const schedule = Schedule.fixed("1 minute").pipe( - Schedule.untilInputEffect((report: ScheduledReport) => - Effect.gen(function* () { + Schedule.while(({ input: report }: { input: ScheduledReport }) => Effect.map(Effect.gen(function* () { const isPastTime = shouldRunNow(parsed); if (isPastTime) { @@ -172,8 +171,7 @@ const scheduleWithCron = (config: ReportConfig) => } return false; // Continue scheduling - }) - ) + }), (completed) => !completed)) ); // Generate report with cron schedule @@ -266,18 +264,20 @@ const scheduleWithTimezone = (config: TimezoneConfig) => // Schedule task const schedule = Schedule.fixed("1 minute").pipe( - Schedule.untilInput((prev: number) => { - const shouldRun = shouldRunInTimezone(parsed); + Schedule.while(({ input: prev }: { input: number }) => + Effect.sync(() => { + const shouldRun = shouldRunInTimezone(parsed); - if (shouldRun) { - yield* Console.log( - `[TIMEZONE] Running job in ${config.timezone} at ${getTimeInTimezone().toISOString()}` - ); - return true; - } + if (shouldRun) { + console.log( + `[TIMEZONE] Running job in ${config.timezone} at ${getTimeInTimezone().toISOString()}` + ); + return false; + } - return false; - }) + return true; + }) + ) ); yield* generateReport(config.jobName).pipe( @@ -312,8 +312,7 @@ const scheduleWithLocking = (config: CronJobWithLocking) => // Schedule with lock const schedule = Schedule.fixed("1 minute").pipe( - Schedule.untilInputEffect((report: ScheduledReport) => - Effect.gen(function* () { + Schedule.while(({ input: report }: { input: ScheduledReport }) => Effect.map(Effect.gen(function* () { const now = Date.now(); const shouldRun = shouldRunNow(parsed); const isStale = now - lastCompletionTime > config.maxDurationMs * 2; @@ -359,8 +358,7 @@ const scheduleWithLocking = (config: CronJobWithLocking) => } return false; // Continue scheduling - }) - ) + }), (completed) => !completed)) ); yield* Schedule.repeat_forever(schedule); @@ -401,8 +399,7 @@ const scheduleDynamicCron = (config: DynamicCronConfig) => // Schedule with dynamic cron const schedule = Schedule.fixed("1 minute").pipe( - Schedule.untilInputEffect((report: ScheduledReport) => - Effect.gen(function* () { + Schedule.while(({ input: report }: { input: ScheduledReport }) => Effect.map(Effect.gen(function* () { const currentCron = yield* Ref.get(cronExpression); const parsed = parseCronExpression(currentCron); @@ -414,8 +411,7 @@ const scheduleDynamicCron = (config: DynamicCronConfig) => } return false; - }) - ) + }), (completed) => !completed)) ); // Run job @@ -462,8 +458,7 @@ const scheduleCronWithBackoff = (config: CronWithRetryConfig) => ); const schedule = Schedule.fixed("1 minute").pipe( - Schedule.untilInputEffect((report: ScheduledReport) => - Effect.gen(function* () { + Schedule.while(({ input: report }: { input: ScheduledReport }) => Effect.map(Effect.gen(function* () { if (!shouldRunNow(parsed)) { return false; } @@ -501,8 +496,7 @@ const scheduleCronWithBackoff = (config: CronWithRetryConfig) => } return false; // Continue scheduling for next cron time - }) - ) + }), (completed) => !completed)) ); yield* Schedule.repeat_forever(schedule); diff --git a/content/published/patterns/scheduling/scheduling-pattern-debounce-throttle.mdx b/content/published/patterns/scheduling/scheduling-pattern-debounce-throttle.mdx index c0849405..1f08327b 100644 --- a/content/published/patterns/scheduling/scheduling-pattern-debounce-throttle.mdx +++ b/content/published/patterns/scheduling/scheduling-pattern-debounce-throttle.mdx @@ -184,7 +184,7 @@ const program = Effect.gen(function* () { }).pipe( Effect.retry( Schedule.exponential("100 millis").pipe( - Schedule.upTo("1 second"), + Schedule.upTo({ duration: "1 second" }), Schedule.recurs(5) ) ) diff --git a/content/published/patterns/scheduling/scheduling-pattern-repeat-effect-on-fixed-interval.mdx b/content/published/patterns/scheduling/scheduling-pattern-repeat-effect-on-fixed-interval.mdx index b5863180..1e41ebac 100644 --- a/content/published/patterns/scheduling/scheduling-pattern-repeat-effect-on-fixed-interval.mdx +++ b/content/published/patterns/scheduling/scheduling-pattern-repeat-effect-on-fixed-interval.mdx @@ -119,13 +119,13 @@ const checkAllServices = ( Effect.gen(function* () { for (const service of config.services) { const status = yield* checkServiceHealth(service.url, service.name).pipe( - Effect.either + Effect.result ); - if (status._tag === "Right") { - serviceStatuses.set(service.name, status.right); + if (status._tag === "Success") { + serviceStatuses.set(service.name, status.success); console.log( - `βœ“ ${service.name}: OK (${status.right.responseTime}ms)` + `βœ“ ${service.name}: OK (${status.success.responseTime}ms)` ); } else { console.log(`βœ— ${service.name}: FAILED`); @@ -175,7 +175,7 @@ const program = Effect.gen(function* () { // Fork the health checker to run in background const checker = yield* createHealthCheckScheduler(config).pipe( - Effect.fork + Effect.forkChild ); // Check and report status every 15 seconds for 60 seconds @@ -223,7 +223,7 @@ const createAdaptiveHealthCheckScheduler = ( // Run health check const allHealthy = yield* checkAllServices(config).pipe( Effect.map(() => true), - Effect.catchAll(() => Effect.succeed(false)) + Effect.catch(() => Effect.succeed(false)) ); if (allHealthy) { @@ -300,7 +300,7 @@ const createRateLimitAwareHealthCheck = ( Effect.tap(() => { checksInWindow++; }), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (error.message.includes("429") || error.message.includes("rate limit")) { if (config.backoffOnRateLimit) { @@ -339,10 +339,10 @@ const createSmartHealthCheckScheduler = ( while (true) { const checkResult = yield* checkAllServices(config).pipe( - Effect.either + Effect.result ); - if (checkResult._tag === "Right") { + if (checkResult._tag === "Success") { if (state === "unhealthy") { state = "recovering"; unhealthyCount = 0; diff --git a/content/published/patterns/scheduling/scheduling-retry-basics.mdx b/content/published/patterns/scheduling/scheduling-retry-basics.mdx index b789e8f1..44e3d800 100644 --- a/content/published/patterns/scheduling/scheduling-retry-basics.mdx +++ b/content/published/patterns/scheduling/scheduling-retry-basics.mdx @@ -88,10 +88,7 @@ const withBasicRetry = fetchData.pipe( // ============================================ const withDelayedRetry = fetchData.pipe( - Effect.retry( - Schedule.spaced("500 millis").pipe( - Schedule.intersect(Schedule.recurs(5)) - ) + Effect.retry(Schedule.max([Schedule.spaced("500 millis"), Schedule.recurs(5)]) ) ) @@ -130,9 +127,10 @@ const retryTransientOnly = fetchWithErrors(true).pipe( const withExponentialBackoff = fetchData.pipe( Effect.retry( - Schedule.exponential("100 millis", 2).pipe( // 100ms, 200ms, 400ms... - Schedule.intersect(Schedule.recurs(5)) // Max 5 retries - ) + Schedule.max([ + Schedule.exponential("100 millis", 2), // 100ms, 200ms, 400ms... + Schedule.recurs(5) // Max 5 retries + ]) ) ) diff --git a/content/published/patterns/schema/ai-schemas/output-basics.mdx b/content/published/patterns/schema/ai-schemas/output-basics.mdx index db1ba554..72098e0e 100644 --- a/content/published/patterns/schema/ai-schemas/output-basics.mdx +++ b/content/published/patterns/schema/ai-schemas/output-basics.mdx @@ -29,9 +29,9 @@ import { Anthropic } from "@anthropic-ai/sdk" // 1. Define the output shape const SentimentAnalysis = Schema.Struct({ - sentiment: Schema.Literal("positive", "negative", "neutral"), + sentiment: Schema.Literals(["positive", "negative", "neutral"]), confidence: Schema.Number.pipe( - Schema.between(0, 1) + Schema.check(Schema.isBetween(0, 1)) ), keywords: Schema.Array(Schema.String), }) @@ -80,7 +80,7 @@ const analyzeSentiment = (text: string) => ) } - const parseResponse = Schema.decodeUnknown(SentimentAnalysis) + const parseResponse = Schema.decodeUnknownEffect(SentimentAnalysis) const result = yield* Effect.tryPromise({ try: () => parseResponse(toolUse.input), catch: (error) => new Error(`Validation failed: ${error}`), @@ -101,7 +101,7 @@ Effect.runPromise(analyzeSentiment("I absolutely love this product!")) | `Schema.Literal` | Constrains to exact string valuesβ€”LLM can only output these values | | `Schema.between` | Numeric validationβ€”confidence must be 0-1 | | `JSONSchema.make` | Converts Effect.Schema to JSON Schema for LLM APIs | -| `Schema.decodeUnknown` | Validates LLM response matches schema at runtime, catching hallucinations | +| `Schema.decodeUnknownEffect` | Validates LLM response matches schema at runtime, catching hallucinations | | Single source of truth | Schema defines types, JSON Schema, and validation all in one place | # When to Use diff --git a/content/published/patterns/schema/ai-schemas/output-descriptions.mdx b/content/published/patterns/schema/ai-schemas/output-descriptions.mdx index 7b79d308..a01b5af8 100644 --- a/content/published/patterns/schema/ai-schemas/output-descriptions.mdx +++ b/content/published/patterns/schema/ai-schemas/output-descriptions.mdx @@ -33,12 +33,12 @@ const MovieReview = Schema.Struct({ Schema.description("Movie title from the review text") ), rating: Schema.Number.pipe( - Schema.between(1, 10), + Schema.check(Schema.isBetween(1, 10)), Schema.description( "Rating on scale of 1-10. Be precise: 7.5 is better than 8" ) ), - reviewSentiment: Schema.Literal("positive", "mixed", "negative").pipe( + reviewSentiment: Schema.Literals(["positive", "mixed", "negative"]).pipe( Schema.description( "Overall sentiment: positive (recommends watching), " + "mixed (has pros and cons), negative (don't watch)" diff --git a/content/published/patterns/schema/ai-schemas/output-enums.mdx b/content/published/patterns/schema/ai-schemas/output-enums.mdx index d0c99262..2ec68ef1 100644 --- a/content/published/patterns/schema/ai-schemas/output-enums.mdx +++ b/content/published/patterns/schema/ai-schemas/output-enums.mdx @@ -28,7 +28,7 @@ import { Schema, JSONSchema, Effect } from "effect" import { Anthropic } from "@anthropic-ai/sdk" // 1. Define literal and enum types -const Priority = Schema.Literal("critical", "high", "medium", "low").pipe( +const Priority = Schema.Literals(["critical", "high", "medium", "low"]).pipe( Schema.description( "Task priority: critical (fix immediately), high (this sprint), " + "medium (next sprint), low (backlog)" @@ -43,13 +43,13 @@ const Status = Schema.Enum({ CANCELLED: "cancelled", }) -const Category = Schema.Union( +const Category = Schema.Union([ Schema.Literal("bug"), Schema.Literal("feature"), Schema.Literal("documentation"), Schema.Literal("refactor"), Schema.Literal("test") -).pipe( +]).pipe( Schema.description("Categorize the work type") ) @@ -61,8 +61,8 @@ const Task = Schema.Struct({ status: Status, category: Category, estimatedHours: Schema.Number.pipe( - Schema.int(), - Schema.between(1, 40), + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(1, 40)), Schema.description("Estimate in hours: 1-40 range") ), labels: Schema.Array(Schema.String).pipe( diff --git a/content/published/patterns/schema/ai-schemas/output-nested.mdx b/content/published/patterns/schema/ai-schemas/output-nested.mdx index 4ae461db..d3506c27 100644 --- a/content/published/patterns/schema/ai-schemas/output-nested.mdx +++ b/content/published/patterns/schema/ai-schemas/output-nested.mdx @@ -69,7 +69,7 @@ const Article = Schema.Struct({ Schema.description("Main title of article") ), summary: Schema.String.pipe( - Schema.maxLength(300), + Schema.check(Schema.isMaxLength(300)), Schema.description("Executive summary, under 300 chars") ), metadata: Metadata, diff --git a/content/published/patterns/schema/ai-schemas/output-unions.mdx b/content/published/patterns/schema/ai-schemas/output-unions.mdx index fc0f590a..a05dad3e 100644 --- a/content/published/patterns/schema/ai-schemas/output-unions.mdx +++ b/content/published/patterns/schema/ai-schemas/output-unions.mdx @@ -34,9 +34,9 @@ const SuccessResponse = Schema.Struct({ id: Schema.String, name: Schema.String, email: Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ), - score: Schema.Number.pipe(Schema.between(0, 100)), + score: Schema.Number.pipe(Schema.check(Schema.isBetween(0, 100))), }), timestamp: Schema.String.pipe( Schema.description("ISO 8601 timestamp") @@ -47,7 +47,7 @@ const SuccessResponse = Schema.Struct({ const ErrorResponse = Schema.Struct({ type: Schema.Literal("error"), error: Schema.Struct({ - code: Schema.Literal("not_found", "invalid_input", "server_error"), + code: Schema.Literals(["not_found", "invalid_input", "server_error"]), message: Schema.String, details: Schema.String.pipe(Schema.optional), }), @@ -65,11 +65,11 @@ const PendingResponse = Schema.Struct({ }) // 4. Create discriminated union -const QueryResponse = Schema.Union( +const QueryResponse = Schema.Union([ SuccessResponse, ErrorResponse, PendingResponse -).pipe( +]).pipe( Schema.description("Response type determined by 'type' field") ) diff --git a/content/published/patterns/schema/ai-schemas/parsing-basics.mdx b/content/published/patterns/schema/ai-schemas/parsing-basics.mdx index 61ef22da..5df2fd6f 100644 --- a/content/published/patterns/schema/ai-schemas/parsing-basics.mdx +++ b/content/published/patterns/schema/ai-schemas/parsing-basics.mdx @@ -31,14 +31,14 @@ import { Anthropic } from "@anthropic-ai/sdk" // 1. Define expected schema const Analysis = Schema.Struct({ summary: Schema.String, - score: Schema.Number.pipe(Schema.between(0, 100)), + score: Schema.Number.pipe(Schema.check(Schema.isBetween(0, 100))), tags: Schema.Array(Schema.String), }) type Analysis = typeof Analysis.Type // 2. Create parser -const parseAnalysis = Schema.decodeUnknown(Analysis) +const parseAnalysis = Schema.decodeUnknownEffect(Analysis) // 3. Parse LLM response in pipeline const analyzeSafely = (llmResponse: unknown) => @@ -112,7 +112,7 @@ Effect.runPromise( | Concept | Explanation | |---------|-------------| -| `Schema.decodeUnknown` | Returns `Effect` β€” no thrown exceptions | +| `Schema.decodeUnknownEffect` | Returns `Effect` β€” no thrown exceptions | | `Effect.tryPromise` | Catches JSON parsing errors without throwing | | `Schema.between` | Runtime validation beyond type checkingβ€”catches out-of-range values | | Typed error channel | Caller must explicitly handle parse failures | diff --git a/content/published/patterns/schema/ai-schemas/parsing-partial.mdx b/content/published/patterns/schema/ai-schemas/parsing-partial.mdx index c2291518..b3f5c9e9 100644 --- a/content/published/patterns/schema/ai-schemas/parsing-partial.mdx +++ b/content/published/patterns/schema/ai-schemas/parsing-partial.mdx @@ -112,7 +112,7 @@ const parseStreamed = (json: string) => } // Validate against schema - const parsed = yield* Schema.decodeUnknown( + const parsed = yield* Schema.decodeUnknownEffect( StreamedAnalysis )(data).pipe( Effect.mapError((error) => ({ diff --git a/content/published/patterns/schema/ai-schemas/parsing-recovery.mdx b/content/published/patterns/schema/ai-schemas/parsing-recovery.mdx index a1c6a196..ebc42162 100644 --- a/content/published/patterns/schema/ai-schemas/parsing-recovery.mdx +++ b/content/published/patterns/schema/ai-schemas/parsing-recovery.mdx @@ -41,7 +41,7 @@ type Article = typeof Article.Type // 2. Recovery strategy 1: Provide defaults const parseWithDefaults = (data: unknown) => Effect.gen(function* () { - const parseArticle = Schema.decodeUnknown(Article) + const parseArticle = Schema.decodeUnknownEffect(Article) const result = yield* parseArticle(data).pipe( Effect.catchTag("ParseError", () => @@ -88,7 +88,7 @@ const parseWithRetry = ( maxRetries: number = 2 ) => Effect.gen(function* () { - const parseArticle = Schema.decodeUnknown(Article) + const parseArticle = Schema.decodeUnknownEffect(Article) let lastError: Error | null = null let data = initialData @@ -123,7 +123,7 @@ const parseWithRetry = ( // 5. Recovery strategy 4: Log and continue const parseWithLogging = (data: unknown, id: string) => Effect.gen(function* () { - const parseArticle = Schema.decodeUnknown(Article) + const parseArticle = Schema.decodeUnknownEffect(Article) const result = yield* parseArticle(data).pipe( Effect.catchTag("ParseError", (error) => { @@ -150,7 +150,7 @@ const parseWithLogging = (data: unknown, id: string) => const handleResponse = (response: unknown) => Effect.gen(function* () { // Try exact parse first - const exact = yield* Schema.decodeUnknown(Article)( + const exact = yield* Schema.decodeUnknownEffect(Article)( response ).pipe( Effect.catchTag("ParseError", () => diff --git a/content/published/patterns/schema/ai-schemas/parsing-retry.mdx b/content/published/patterns/schema/ai-schemas/parsing-retry.mdx index 1391ec86..9672103f 100644 --- a/content/published/patterns/schema/ai-schemas/parsing-retry.mdx +++ b/content/published/patterns/schema/ai-schemas/parsing-retry.mdx @@ -51,7 +51,7 @@ const isRetryable = (error: ParseError): boolean => { const Response = Schema.Struct({ result: Schema.String, confidence: Schema.Number.pipe( - Schema.between(0, 1) + Schema.check(Schema.isBetween(0, 1)) ), }) @@ -61,7 +61,7 @@ type Response = typeof Response.Type const parseResponse = (data: unknown) => Effect.gen(function* () { try { - const parsed = yield* Schema.decodeUnknown( + const parsed = yield* Schema.decodeUnknownEffect( Response )(data) return { _tag: "Success" as const, data: parsed } @@ -107,26 +107,19 @@ const callLLMWithRetry = (prompt: string) => Effect.gen(function* () { const client = new Anthropic() - // Exponential backoff: 100ms β†’ 200ms β†’ 400ms β†’ 800ms (max 5s) - const schedule = Schedule.exponential( - "100 millis" - ).pipe( - Schedule.map(() => - Effect.gen(function* () { - // Add jitter to prevent thundering herd - const jitter = yield* Random.nextIntBetween( - 0, - 100 - ) - return jitter - }).pipe(Effect.runSync()) - ), - Schedule.compose( - Schedule.recurUntil( - (attempt) => attempt >= 3 + // Exponential backoff: 100ms β†’ 200ms β†’ 400ms (max 3 attempts) + const schedule = Schedule.max([ + Schedule.exponential("100 millis").pipe( + Schedule.map(() => + Effect.gen(function* () { + // Add jitter to prevent thundering herd + const jitter = yield* Random.nextIntBetween(0, 100) + return jitter + }).pipe(Effect.runSync()) ) - ) - ) + ), + Schedule.recurs(3) + ]) const callLLM = () => Effect.gen(function* () { @@ -172,9 +165,7 @@ const callLLMWithRetry = (prompt: string) => const result = yield* callLLM().pipe( Effect.retry( schedule.pipe( - Schedule.whileInput((error) => - isRetryable(error) - ) + Schedule.while(({ input: error }) => isRetryable(error)) ) ), Effect.catchTag("Timeout", (error) => diff --git a/content/published/patterns/schema/ai-schemas/parsing-streaming.mdx b/content/published/patterns/schema/ai-schemas/parsing-streaming.mdx index 6ccc4ad9..978d6c8d 100644 --- a/content/published/patterns/schema/ai-schemas/parsing-streaming.mdx +++ b/content/published/patterns/schema/ai-schemas/parsing-streaming.mdx @@ -120,7 +120,7 @@ const jsonStreamParser = ( // 3. Incremental validator const validateStreamed = (object: unknown) => Effect.gen(function* () { - const decoder = Schema.decodeUnknown(StreamedThought) + const decoder = Schema.decodeUnknownEffect(StreamedThought) const result = yield* decoder(object).pipe( Effect.mapError((error) => ({ @@ -158,7 +158,7 @@ const handleStreamedThoughts = ( ) ) ), - Stream.catchAll((error) => + Stream.catch((error) => Effect.sync(() => onError(error)) ), Stream.runDrain diff --git a/content/published/patterns/schema/ai-schemas/vercel-ai-sdk.mdx b/content/published/patterns/schema/ai-schemas/vercel-ai-sdk.mdx index 015ce40f..b42fd995 100644 --- a/content/published/patterns/schema/ai-schemas/vercel-ai-sdk.mdx +++ b/content/published/patterns/schema/ai-schemas/vercel-ai-sdk.mdx @@ -24,7 +24,7 @@ You're using Vercel AI SDK's `generateObject` function to get structured output # Solution ```typescript -import { Schema, JSONSchema, Effect } from "effect" +import { Schema, JSONSchema, Effect, Result } from "effect" import { generateObject } from "ai" import { openai } from "@ai-sdk/openai" @@ -34,10 +34,10 @@ const ProductReview = Schema.Struct({ Schema.description("Internal product ID") ), rating: Schema.Number.pipe( - Schema.between(1, 5), + Schema.check(Schema.isBetween(1, 5)), Schema.description("Rating: 1-5 stars") ), - sentiment: Schema.Literal("positive", "neutral", "negative").pipe( + sentiment: Schema.Literals(["positive", "neutral", "negative"]).pipe( Schema.description("Overall sentiment of review") ), pros: Schema.Array(Schema.String).pipe( diff --git a/content/published/patterns/schema/arrays/tuples.mdx b/content/published/patterns/schema/arrays/tuples.mdx index 9abaa317..b203b798 100644 --- a/content/published/patterns/schema/arrays/tuples.mdx +++ b/content/published/patterns/schema/arrays/tuples.mdx @@ -30,19 +30,19 @@ import { Schema } from "effect" // ============================================ // 2D coordinate [x, y] -const Point2D = Schema.Tuple(Schema.Number, Schema.Number) +const Point2D = Schema.Tuple([Schema.Number, Schema.Number]) type Point2D = typeof Point2D.Type // readonly [number, number] // 3D coordinate [x, y, z] -const Point3D = Schema.Tuple(Schema.Number, Schema.Number, Schema.Number) +const Point3D = Schema.Tuple([Schema.Number, Schema.Number, Schema.Number]) type Point3D = typeof Point3D.Type // readonly [number, number, number] // RGB color [r, g, b] -const RGB = Schema.Tuple(Schema.Number, Schema.Number, Schema.Number) +const RGB = Schema.Tuple([Schema.Number, Schema.Number, Schema.Number]) type RGB = typeof RGB.Type // Date range [start, end] -const DateRange = Schema.Tuple(Schema.Date, Schema.Date) +const DateRange = Schema.Tuple([Schema.DateFromString, Schema.DateFromString]) type DateRange = typeof DateRange.Type // readonly [Date, Date] // ============================================ @@ -50,15 +50,15 @@ type DateRange = typeof DateRange.Type // readonly [Date, Date] // ============================================ // Key-value pair [key, value] -const KeyValue = Schema.Tuple(Schema.String, Schema.Unknown) +const KeyValue = Schema.Tuple([Schema.String, Schema.Unknown]) type KeyValue = typeof KeyValue.Type // readonly [string, unknown] // Named point ["label", x, y] -const NamedPoint = Schema.Tuple(Schema.String, Schema.Number, Schema.Number) +const NamedPoint = Schema.Tuple([Schema.String, Schema.Number, Schema.Number]) type NamedPoint = typeof NamedPoint.Type // Result tuple [success, data | error] -const StringResult = Schema.Tuple(Schema.Boolean, Schema.String) +const StringResult = Schema.Tuple([Schema.Boolean, Schema.String]) type StringResult = typeof StringResult.Type // readonly [boolean, string] // ============================================ @@ -66,10 +66,11 @@ type StringResult = typeof StringResult.Type // readonly [boolean, string] // ============================================ // Optional third element [x, y, z?] -const Point2Dor3D = Schema.Tuple( +const Point2Dor3D = Schema.Tuple([ Schema.Number, Schema.Number, -).pipe(Schema.optionalElement(Schema.Number)) + Schema.optional(Schema.Number) +]) // readonly [number, number, number?] // ============================================ @@ -77,9 +78,9 @@ const Point2Dor3D = Schema.Tuple( // ============================================ // Fixed start, variable rest: [header, ...items] -const ListWithHeader = Schema.Tuple( - [Schema.String], // First element: header - Schema.rest(Schema.Number) // Rest: numbers +const ListWithHeader = Schema.TupleWithRest( + Schema.Tuple([Schema.String]), // First element: header + [Schema.Number] // Rest: numbers ) type ListWithHeader = typeof ListWithHeader.Type // readonly [string, ...number[]] @@ -164,8 +165,8 @@ console.log(`\nβœ… Box: ${width}x${height}`) | Schema | Purpose | |--------|---------| -| **Schema.Tuple(S1, S2, ...)** | Fixed-length typed array | -| **optionalElement** | Optional trailing elements | +| **Schema.Tuple([S1, S2, ...])** | Fixed-length typed array | +| **Schema.optional (element)** | Optional trailing elements | | **rest** | Variable-length tail | | **Position types** | Each position has specific type | | **Length validation** | Exact length enforced | diff --git a/content/published/patterns/schema/async-validation/basic-async.mdx b/content/published/patterns/schema/async-validation/basic-async.mdx index 3cea0842..7664bd74 100644 --- a/content/published/patterns/schema/async-validation/basic-async.mdx +++ b/content/published/patterns/schema/async-validation/basic-async.mdx @@ -31,9 +31,9 @@ import { Schema, Effect } from "effect" // ============================================ const ValidUsername = Schema.String.pipe( - Schema.minLength(3), - Schema.maxLength(20), - Schema.pattern(/^[a-zA-Z0-9_-]+$/), + Schema.check(Schema.isMinLength(3)), + Schema.check(Schema.isMaxLength(20)), + Schema.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)), Schema.filterEffect((username) => Effect.gen(function* () { // Simulate async check @@ -58,7 +58,7 @@ const ValidUsername = Schema.String.pipe( // ============================================ const VerifiedEmail = Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), Schema.filterEffect((email) => Effect.gen(function* () { // Simulate async email verification @@ -114,7 +114,7 @@ const ValidDiscountCode = Schema.String.pipe( const SignUpForm = Schema.Struct({ username: ValidUsername, email: VerifiedEmail, - password: Schema.String.pipe(Schema.minLength(8)), + password: Schema.String.pipe(Schema.check(Schema.isMinLength(8))), discountCode: Schema.optional(ValidDiscountCode), }) @@ -131,7 +131,7 @@ const validateSignup = ( console.log("Validating signup form...") const result = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(SignUpForm)(data), + try: () => Schema.decodeUnknownEffect(SignUpForm)(data), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Validation failed: ${msg}`) @@ -171,11 +171,11 @@ const appLogic = Effect.gen(function* () { } const result2 = yield* validateSignup(invalidUsername).pipe( - Effect.either + Effect.result ) - if (result2._tag === "Left") { - console.log(`βœ— Error: ${result2.left.message}`) + if (result2._tag === "Failure") { + console.log(`βœ— Error: ${result2.failure.message}`) } console.log("\n3. Invalid email domain:\n") @@ -187,11 +187,11 @@ const appLogic = Effect.gen(function* () { } const result3 = yield* validateSignup(invalidEmail).pipe( - Effect.either + Effect.result ) - if (result3._tag === "Left") { - console.log(`βœ— Error: ${result3.left.message}`) + if (result3._tag === "Failure") { + console.log(`βœ— Error: ${result3.failure.message}`) } console.log("\n4. Invalid discount code:\n") @@ -204,11 +204,11 @@ const appLogic = Effect.gen(function* () { } const result4 = yield* validateSignup(invalidCode).pipe( - Effect.either + Effect.result ) - if (result4._tag === "Left") { - console.log(`βœ— Error: ${result4.left.message}`) + if (result4._tag === "Failure") { + console.log(`βœ— Error: ${result4.failure.message}`) } return result1 diff --git a/content/published/patterns/schema/async-validation/batched-async.mdx b/content/published/patterns/schema/async-validation/batched-async.mdx index 7d4bedbd..5988935a 100644 --- a/content/published/patterns/schema/async-validation/batched-async.mdx +++ b/content/published/patterns/schema/async-validation/batched-async.mdx @@ -111,7 +111,7 @@ const cache = new ValidationCache() const service = new BatchValidationService() const BatchValidatedUsername = Schema.String.pipe( - Schema.minLength(3), + Schema.check(Schema.isMinLength(3)), Schema.filterEffect((username) => Effect.gen(function* () { const isValid = yield* Effect.tryPromise({ @@ -131,7 +131,7 @@ const BatchValidatedUsername = Schema.String.pipe( ) const BatchValidatedEmail = Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), Schema.filterEffect((email) => Effect.gen(function* () { const isValid = yield* Effect.tryPromise({ @@ -186,16 +186,16 @@ const appLogic = Effect.gen(function* () { console.log("\nValidating...") const result = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(BulkUserData)(users), + try: () => Schema.decodeUnknownEffect(BulkUserData)(users), catch: (e) => new Error(String(e)), - }).pipe(Effect.either) + }).pipe(Effect.result) - if (result._tag === "Left") { - console.log(`\nβœ— Validation failed: ${result.left.message}`) + if (result._tag === "Failure") { + console.log(`\nβœ— Validation failed: ${result.failure.message}`) console.log("Note: Thanks to batching + cache, duplicates only validated once") } else { console.log("\nβœ“ All users validated") - for (const user of result.right) { + for (const user of result.success) { console.log(` - ${user.username}: OK`) } } diff --git a/content/published/patterns/schema/async-validation/database-checks.mdx b/content/published/patterns/schema/async-validation/database-checks.mdx index 56a34359..b22b4b88 100644 --- a/content/published/patterns/schema/async-validation/database-checks.mdx +++ b/content/published/patterns/schema/async-validation/database-checks.mdx @@ -83,7 +83,7 @@ const db = new Database() // ============================================ const UniqueUsername = Schema.String.pipe( - Schema.minLength(3), + Schema.check(Schema.isMinLength(3)), Schema.filterEffect((username) => Effect.gen(function* () { const isUnique = yield* Effect.tryPromise({ @@ -103,7 +103,7 @@ const UniqueUsername = Schema.String.pipe( ) const UniqueEmail = Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), Schema.filterEffect((email) => Effect.gen(function* () { const isUnique = yield* Effect.tryPromise({ @@ -167,13 +167,13 @@ const ValidUserReference = Schema.String.pipe( const CreateOrderForm = Schema.Struct({ userId: ValidUserReference, productId: ExistingProduct, - quantity: Schema.Number.pipe(Schema.int(), Schema.between(1, 100)), + quantity: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(1, 100))), }) const CreateUserForm = Schema.Struct({ username: UniqueUsername, email: UniqueEmail, - password: Schema.String.pipe(Schema.minLength(8)), + password: Schema.String.pipe(Schema.check(Schema.isMinLength(8))), }) // ============================================ @@ -192,7 +192,7 @@ const appLogic = Effect.gen(function* () { } const user1 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(CreateUserForm)(validUser), + try: () => Schema.decodeUnknownEffect(CreateUserForm)(validUser), catch: (e) => new Error(String(e)), }) @@ -207,12 +207,12 @@ const appLogic = Effect.gen(function* () { } const user2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(CreateUserForm)(duplicateUser), + try: () => Schema.decodeUnknownEffect(CreateUserForm)(duplicateUser), catch: (e) => new Error(String(e)), - }).pipe(Effect.either) + }).pipe(Effect.result) - if (user2._tag === "Left") { - console.log(`βœ— Error: ${user2.left.message}`) + if (user2._tag === "Failure") { + console.log(`βœ— Error: ${user2.failure.message}`) } console.log("\n3. Valid order with foreign keys:\n") @@ -224,7 +224,7 @@ const appLogic = Effect.gen(function* () { } const order = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(CreateOrderForm)(validOrder), + try: () => Schema.decodeUnknownEffect(CreateOrderForm)(validOrder), catch: (e) => new Error(String(e)), }) @@ -240,12 +240,12 @@ const appLogic = Effect.gen(function* () { } const order2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(CreateOrderForm)(invalidOrder), + try: () => Schema.decodeUnknownEffect(CreateOrderForm)(invalidOrder), catch: (e) => new Error(String(e)), - }).pipe(Effect.either) + }).pipe(Effect.result) - if (order2._tag === "Left") { - console.log(`βœ— Error: ${order2.left.message}`) + if (order2._tag === "Failure") { + console.log(`βœ— Error: ${order2.failure.message}`) } return { user1, order } diff --git a/content/published/patterns/schema/async-validation/external-api-validation.mdx b/content/published/patterns/schema/async-validation/external-api-validation.mdx index 7c2d99e2..6460b5ce 100644 --- a/content/published/patterns/schema/async-validation/external-api-validation.mdx +++ b/content/published/patterns/schema/async-validation/external-api-validation.mdx @@ -95,7 +95,7 @@ const ValidCreditCard = Schema.String.pipe( ) const ValidShippingAddress = Schema.String.pipe( - Schema.minLength(10), + Schema.check(Schema.isMinLength(10)), Schema.filterEffect((address) => Effect.gen(function* () { const isValid = yield* Effect.tryPromise({ @@ -142,7 +142,7 @@ const SafeIPAddress = Schema.String.pipe( const PaymentForm = Schema.Struct({ cardNumber: ValidCreditCard, - amount: Schema.Number.pipe(Schema.greaterThan(0)), + amount: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0))), shippingAddress: ValidShippingAddress, }) @@ -168,7 +168,7 @@ const appLogic = Effect.gen(function* () { } const payment = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(PaymentForm)(validPayment), + try: () => Schema.decodeUnknownEffect(PaymentForm)(validPayment), catch: (e) => new Error(String(e)), }) @@ -186,12 +186,12 @@ const appLogic = Effect.gen(function* () { } const payment2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(PaymentForm)(invalidCard), + try: () => Schema.decodeUnknownEffect(PaymentForm)(invalidCard), catch: (e) => new Error(String(e)), - }).pipe(Effect.either) + }).pipe(Effect.result) - if (payment2._tag === "Left") { - console.log(`βœ— Error: ${payment2.left.message}`) + if (payment2._tag === "Failure") { + console.log(`βœ— Error: ${payment2.failure.message}`) } console.log("\n3. Valid login (safe IP):\n") @@ -203,7 +203,7 @@ const appLogic = Effect.gen(function* () { } const login = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(LoginForm)(validLogin), + try: () => Schema.decodeUnknownEffect(LoginForm)(validLogin), catch: (e) => new Error(String(e)), }) @@ -220,12 +220,12 @@ const appLogic = Effect.gen(function* () { } const login2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(LoginForm)(suspiciousLogin), + try: () => Schema.decodeUnknownEffect(LoginForm)(suspiciousLogin), catch: (e) => new Error(String(e)), - }).pipe(Effect.either) + }).pipe(Effect.result) - if (login2._tag === "Left") { - console.log(`βœ— Error: ${login2.left.message}`) + if (login2._tag === "Failure") { + console.log(`βœ— Error: ${login2.failure.message}`) } return { payment, login } diff --git a/content/published/patterns/schema/composition/extend-schemas.mdx b/content/published/patterns/schema/composition/extend-schemas.mdx index 7e33d59b..dd7ec909 100644 --- a/content/published/patterns/schema/composition/extend-schemas.mdx +++ b/content/published/patterns/schema/composition/extend-schemas.mdx @@ -34,7 +34,7 @@ const BaseUser = Schema.Struct({ id: Schema.String, username: Schema.String, email: Schema.String, - createdAt: Schema.Date, + createdAt: Schema.DateFromString, }) type BaseUser = typeof BaseUser.Type @@ -43,25 +43,19 @@ type BaseUser = typeof BaseUser.Type // 2. Extend with new fields using Schema.extend // ============================================ -const AdminUser = Schema.extend( - BaseUser, - Schema.Struct({ +const AdminUser = Schema.Struct({ ...BaseUser.fields, role: Schema.Literal("admin"), permissions: Schema.Array(Schema.String), - lastLogin: Schema.Date, + lastLogin: Schema.DateFromString, }) -) type AdminUser = typeof AdminUser.Type -const PremiumUser = Schema.extend( - BaseUser, - Schema.Struct({ +const PremiumUser = Schema.Struct({ ...BaseUser.fields, tier: Schema.Enum({ gold: "gold", platinum: "platinum" }), - subscriptionEnd: Schema.Date, + subscriptionEnd: Schema.DateFromString, features: Schema.Array(Schema.String), }) -) type PremiumUser = typeof PremiumUser.Type @@ -69,15 +63,12 @@ type PremiumUser = typeof PremiumUser.Type // 3. Extend with optional fields // ============================================ -const UserWithProfile = Schema.extend( - BaseUser, - Schema.Struct({ +const UserWithProfile = Schema.Struct({ ...BaseUser.fields, bio: Schema.Optional(Schema.String), avatar: Schema.Optional(Schema.String), location: Schema.Optional(Schema.String), website: Schema.Optional(Schema.String), }) -) type UserWithProfile = typeof UserWithProfile.Type @@ -85,24 +76,18 @@ type UserWithProfile = typeof UserWithProfile.Type // 4. Multi-level extension // ============================================ -const VerifiedUser = Schema.extend( - UserWithProfile, - Schema.Struct({ +const VerifiedUser = Schema.Struct({ ...UserWithProfile.fields, emailVerified: Schema.Boolean, phoneVerified: Schema.Boolean, - verificationDate: Schema.Optional(Schema.Date), + verificationDate: Schema.Optional(Schema.DateFromString), }) -) type VerifiedUser = typeof VerifiedUser.Type -const VerifiedAdmin = Schema.extend( - VerifiedUser, - Schema.Struct({ +const VerifiedAdmin = Schema.Struct({ ...VerifiedUser.fields, adminRole: Schema.String, adminTeam: Schema.String, }) -) type VerifiedAdmin = typeof VerifiedAdmin.Type @@ -110,7 +95,7 @@ type VerifiedAdmin = typeof VerifiedAdmin.Type // 5. Union of extended schemas // ============================================ -const AnyUser = Schema.Union(BaseUser, AdminUser, PremiumUser) +const AnyUser = Schema.Union([BaseUser, AdminUser, PremiumUser]) type AnyUser = typeof AnyUser.Type @@ -140,7 +125,7 @@ const parseUser = ( : type === "premium" ? PremiumUser : BaseUser - return await Schema.decodeUnknown(schema)(raw) + return await Schema.decodeUnknownEffect(schema)(raw) }, catch: (error) => { const msg = error instanceof Error ? error.message : String(error) @@ -224,7 +209,7 @@ const appLogic = Effect.gen(function* () { } const verifiedAdmin = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(VerifiedAdmin)(verifiedAdminData), + try: () => Schema.decodeUnknownEffect(VerifiedAdmin)(verifiedAdminData), catch: (error) => new Error(String(error)), }) diff --git a/content/published/patterns/schema/composition/inheritance-patterns.mdx b/content/published/patterns/schema/composition/inheritance-patterns.mdx index 59370c42..6b8e0f8c 100644 --- a/content/published/patterns/schema/composition/inheritance-patterns.mdx +++ b/content/published/patterns/schema/composition/inheritance-patterns.mdx @@ -36,7 +36,7 @@ const BaseProduct = Schema.Struct({ description: Schema.String, price: Schema.Number, currency: Schema.Enum({ usd: "USD", eur: "EUR", gbp: "GBP" }), - createdAt: Schema.Date, + createdAt: Schema.DateFromString, tags: Schema.Array(Schema.String), }) @@ -46,9 +46,7 @@ type BaseProduct = typeof BaseProduct.Type // 2. Specializations with inheritance // ============================================ -const PhysicalProduct = Schema.extend( - BaseProduct, - Schema.Struct({ +const PhysicalProduct = Schema.Struct({ ...BaseProduct.fields, type: Schema.Literal("physical"), weight: Schema.Number, dimensions: Schema.Struct({ @@ -59,13 +57,10 @@ const PhysicalProduct = Schema.extend( warehouse: Schema.String, stock: Schema.Number, }) -) type PhysicalProduct = typeof PhysicalProduct.Type -const DigitalProduct = Schema.extend( - BaseProduct, - Schema.Struct({ +const DigitalProduct = Schema.Struct({ ...BaseProduct.fields, type: Schema.Literal("digital"), downloadUrl: Schema.String, fileSize: Schema.Number, @@ -80,20 +75,16 @@ const DigitalProduct = Schema.extend( cloud: "cloud", }), }) -) type DigitalProduct = typeof DigitalProduct.Type -const ServiceProduct = Schema.extend( - BaseProduct, - Schema.Struct({ +const ServiceProduct = Schema.Struct({ ...BaseProduct.fields, type: Schema.Literal("service"), duration: Schema.Number, // in minutes serviceCategory: Schema.String, includesSupport: Schema.Boolean, maxParticipants: Schema.Optional(Schema.Number), }) -) type ServiceProduct = typeof ServiceProduct.Type @@ -101,7 +92,7 @@ type ServiceProduct = typeof ServiceProduct.Type // 3. Union of specializations // ============================================ -const Product = Schema.Union(PhysicalProduct, DigitalProduct, ServiceProduct) +const Product = Schema.Union([PhysicalProduct, DigitalProduct, ServiceProduct]) type Product = typeof Product.Type @@ -155,7 +146,7 @@ const validateInventory = (product: Product): boolean => { const parseProduct = (raw: unknown): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(Product)(raw), + try: () => Schema.decodeUnknownEffect(Product)(raw), catch: (error) => new Error(String(error)), }) diff --git a/content/published/patterns/schema/composition/merge-schemas.mdx b/content/published/patterns/schema/composition/merge-schemas.mdx index 91bb13c1..6c5096af 100644 --- a/content/published/patterns/schema/composition/merge-schemas.mdx +++ b/content/published/patterns/schema/composition/merge-schemas.mdx @@ -32,8 +32,8 @@ import { Schema, Effect } from "effect" const BaseEntity = Schema.Struct({ id: Schema.String, - createdAt: Schema.Date, - updatedAt: Schema.Date, + createdAt: Schema.DateFromString, + updatedAt: Schema.DateFromString, }) const Auditable = Schema.Struct({ @@ -43,8 +43,8 @@ const Auditable = Schema.Struct({ }) const Timestamped = Schema.Struct({ - publishedAt: Schema.Optional(Schema.Date), - expiresAt: Schema.Optional(Schema.Date), + publishedAt: Schema.Optional(Schema.DateFromString), + expiresAt: Schema.Optional(Schema.DateFromString), }) // ============================================ @@ -65,16 +65,13 @@ type Article = typeof Article.Type // 3. Merge with Schema.extend for complex cases // ============================================ -const AuditableEntity = Schema.extend(BaseEntity, Auditable) +const AuditableEntity = Schema.Struct({ ...BaseEntity.fields, ...Auditable.fields }) -const Article2 = Schema.extend( - AuditableEntity, - Schema.Struct({ +const Article2 = Schema.Struct({ ...AuditableEntity.fields, title: Schema.String, content: Schema.String, tags: Schema.Array(Schema.String), }) -) type Article2 = typeof Article2.Type @@ -89,7 +86,7 @@ const ApiRequest = Schema.Struct({ update: "update", delete: "delete", }), - timestamp: Schema.Date, + timestamp: Schema.DateFromString, }) const BusinessRules = Schema.Struct({ @@ -104,7 +101,7 @@ const BusinessRules = Schema.Struct({ }), }) -const FullRequest = Schema.extend(ApiRequest, BusinessRules) +const FullRequest = Schema.Struct({ ...ApiRequest.fields, ...BusinessRules.fields }) type FullRequest = typeof FullRequest.Type @@ -118,23 +115,17 @@ const Document = Schema.Struct({ content: Schema.String, }) -const PublicDocument = Schema.extend( - Document, - Schema.Struct({ +const PublicDocument = Schema.Struct({ ...Document.fields, visibility: Schema.Literal("public"), - publishedAt: Schema.Date, + publishedAt: Schema.DateFromString, author: Schema.String, }) -) -const PrivateDocument = Schema.extend( - Document, - Schema.Struct({ +const PrivateDocument = Schema.Struct({ ...Document.fields, visibility: Schema.Literal("private"), owner: Schema.String, permissions: Schema.Array(Schema.String), }) -) type PublicDocument = typeof PublicDocument.Type type PrivateDocument = typeof PrivateDocument.Type @@ -145,13 +136,13 @@ type PrivateDocument = typeof PrivateDocument.Type const parseArticle = (raw: unknown): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(Article)(raw), + try: () => Schema.decodeUnknownEffect(Article)(raw), catch: (error) => new Error(String(error)), }) const parseFullRequest = (raw: unknown): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(FullRequest)(raw), + try: () => Schema.decodeUnknownEffect(FullRequest)(raw), catch: (error) => new Error(String(error)), }) @@ -210,7 +201,7 @@ const appLogic = Effect.gen(function* () { } const publicDoc = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(PublicDocument)(publicDocData), + try: () => Schema.decodeUnknownEffect(PublicDocument)(publicDocData), catch: (error) => new Error(String(error)), }) @@ -228,7 +219,7 @@ const appLogic = Effect.gen(function* () { } const privateDoc = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(PrivateDocument)(privateDocData), + try: () => Schema.decodeUnknownEffect(PrivateDocument)(privateDocData), catch: (error) => new Error(String(error)), }) diff --git a/content/published/patterns/schema/composition/pick-omit.mdx b/content/published/patterns/schema/composition/pick-omit.mdx index 442501e2..dd00729c 100644 --- a/content/published/patterns/schema/composition/pick-omit.mdx +++ b/content/published/patterns/schema/composition/pick-omit.mdx @@ -24,7 +24,7 @@ You have a comprehensive User schema with 15 fields. For the public profile API, # Solution ```typescript -import { Schema, Effect } from "effect" +import { Schema, Effect, Struct } from "effect" // ============================================ // 1. Comprehensive schema @@ -38,9 +38,9 @@ const User = Schema.Struct({ bio: Schema.Optional(Schema.String), avatar: Schema.Optional(Schema.String), role: Schema.String, - createdAt: Schema.Date, - updatedAt: Schema.Date, - lastLogin: Schema.Optional(Schema.Date), + createdAt: Schema.DateFromString, + updatedAt: Schema.DateFromString, + lastLogin: Schema.Optional(Schema.DateFromString), emailVerified: Schema.Boolean, phoneVerified: Schema.Boolean, twoFactorSecret: Schema.Optional(Schema.String), @@ -59,12 +59,12 @@ type User = typeof User.Type // ============================================ // Public profile - only certain fields -const PublicProfile = Schema.pick(User, ["id", "username", "bio", "avatar", "createdAt"]) +const PublicProfile = Schema.Struct(Struct.pick(User.fields, ["id", "username", "bio", "avatar", "createdAt"])) type PublicProfile = typeof PublicProfile.Type // Admin view - most fields -const AdminView = Schema.pick(User, [ +const AdminView = Schema.Struct(Struct.pick(User.fields, [ "id", "username", "email", @@ -74,12 +74,12 @@ const AdminView = Schema.pick(User, [ "emailVerified", "phoneVerified", "lastLogin", -]) +])) type AdminView = typeof AdminView.Type // API key response -const ApiTokenResponse = Schema.pick(User, ["id", "username", "apiToken", "createdAt"]) +const ApiTokenResponse = Schema.Struct(Struct.pick(User.fields, ["id", "username", "apiToken", "createdAt"])) type ApiTokenResponse = typeof ApiTokenResponse.Type @@ -88,21 +88,21 @@ type ApiTokenResponse = typeof ApiTokenResponse.Type // ============================================ // Sanitized for export (remove sensitive data) -const UserForExport = Schema.omit(User, [ +const UserForExport = Schema.Struct(Struct.omit(User.fields, [ "password", "twoFactorSecret", "apiToken", -]) +])) type UserForExport = typeof UserForExport.Type // User update input (exclude immutable fields) -const UpdateUserInput = Schema.omit(User, [ +const UpdateUserInput = Schema.Struct(Struct.omit(User.fields, [ "id", "createdAt", "updatedAt", "role", -]) +])) type UpdateUserInput = typeof UpdateUserInput.Type @@ -111,12 +111,12 @@ type UpdateUserInput = typeof UpdateUserInput.Type // ============================================ // User with credentials for login -const UserCredentials = Schema.pick(User, ["email", "password"]) +const UserCredentials = Schema.Struct(Struct.pick(User.fields, ["email", "password"])) type UserCredentials = typeof UserCredentials.Type // Minimal info for list views -const UserListItem = Schema.pick(User, ["id", "username", "role", "lastLogin"]) +const UserListItem = Schema.Struct(Struct.pick(User.fields, ["id", "username", "role", "lastLogin"])) type UserListItem = typeof UserListItem.Type @@ -139,7 +139,7 @@ const sanitizeForExport = (user: User): UserForExport => { const parsePublicProfile = (raw: unknown): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(PublicProfile)(raw), + try: () => Schema.decodeUnknownEffect(PublicProfile)(raw), catch: (error) => new Error(String(error)), }) @@ -182,13 +182,13 @@ const appLogic = Effect.gen(function* () { console.log(` - ${publicProfile.username}: ${publicProfile.bio}`) console.log("\n3. Admin View (picked fields):") - const adminView = Schema.pick(User, [ + const adminView = Schema.Struct(Struct.pick(User.fields, [ "id", "username", "email", "role", "emailVerified", - ]) as unknown as typeof AdminView + ])) as unknown as typeof AdminView const admin = (({ id, username, email, role, emailVerified }) => ({ id, username, diff --git a/content/published/patterns/schema/environment-config/config-layers.mdx b/content/published/patterns/schema/environment-config/config-layers.mdx index e3f43d6b..f8312139 100644 --- a/content/published/patterns/schema/environment-config/config-layers.mdx +++ b/content/published/patterns/schema/environment-config/config-layers.mdx @@ -31,13 +31,13 @@ import * as fs from "fs" // 1. Define configuration schema const DatabaseConfig = Schema.Struct({ host: Schema.String, - port: Schema.pipe(Schema.Number, Schema.int(), Schema.between(1024, 65535)), + port: Schema.pipe(Schema.Number, Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(1024, 65535))), database: Schema.String, - maxConnections: Schema.pipe(Schema.Number, Schema.int(), Schema.between(1, 1000)), + maxConnections: Schema.pipe(Schema.Number, Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(1, 1000))), }) const CacheConfig = Schema.Struct({ - ttl: Schema.pipe(Schema.Number, Schema.int(), Schema.between(0, 86400)), + ttl: Schema.pipe(Schema.Number, Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(0, 86400))), enabled: Schema.Boolean, }) @@ -154,7 +154,7 @@ const loadConfig = (configFilePath?: string): Effect.Effect => // Validate merged config const validated = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(AppConfig)(merged), + try: () => Schema.decodeUnknownEffect(AppConfig)(merged), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Config validation failed: ${msg}`) diff --git a/content/published/patterns/schema/environment-config/env-variables.mdx b/content/published/patterns/schema/environment-config/env-variables.mdx index 08bee09a..cbbad073 100644 --- a/content/published/patterns/schema/environment-config/env-variables.mdx +++ b/content/published/patterns/schema/environment-config/env-variables.mdx @@ -24,7 +24,7 @@ Environment variables power your applicationβ€”database URLs, API keys, ports. B # Solution ```typescript -import { Schema, Effect } from "effect" +import { Schema, Effect, Context, Layer } from "effect" // 1. Define environment schema const EnvironmentSchema = Schema.Struct({ @@ -32,21 +32,21 @@ const EnvironmentSchema = Schema.Struct({ Schema.annotations({ description: 'PostgreSQL connection string' }), ), API_KEY: Schema.String.pipe( - Schema.minLength(32), + Schema.check(Schema.isMinLength(32)), Schema.annotations({ description: 'API authentication key (min 32 chars)', }), ), PORT: Schema.String.pipe( Schema.parseNumber, - Schema.int(), - Schema.between(1024, 65535), + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(1024, 65535)), Schema.annotations({ description: 'Server port (1024-65535)' }), ), - LOG_LEVEL: Schema.Literal('debug', 'info', 'warn', 'error').pipe( + LOG_LEVEL: Schema.Literals(['debug', 'info', 'warn', 'error']).pipe( Schema.annotations({ description: 'Logging level' }), ), - NODE_ENV: Schema.Literal('development', 'staging', 'production').pipe( + NODE_ENV: Schema.Literals(['development', 'staging', 'production']).pipe( Schema.annotations({ description: 'Deployment environment' }), ), }) @@ -54,7 +54,7 @@ const EnvironmentSchema = Schema.Struct({ type Environment = typeof EnvironmentSchema.Type // 2. Create validator -const validateEnv = Schema.decodeUnknown(EnvironmentSchema) +const validateEnv = Schema.decodeUnknownEffect(EnvironmentSchema) // 3. Load and validate environment const loadEnvironment = Effect.fn(function* () { @@ -65,14 +65,12 @@ const loadEnvironment = Effect.fn(function* () { }) // 4. Create service to provide environment -export class EnvironmentService extends Context.Tag('@app/EnvironmentService')< - EnvironmentService, +export class EnvironmentService extends Context.Service boolean isStaging: () => boolean isProd: () => boolean - } ->() { + }>('@app/EnvironmentService') { static layer = Layer.effect( this, Effect.gen(function* () { diff --git a/content/published/patterns/schema/environment-config/feature-flags.mdx b/content/published/patterns/schema/environment-config/feature-flags.mdx index f9093dba..dcea19d8 100644 --- a/content/published/patterns/schema/environment-config/feature-flags.mdx +++ b/content/published/patterns/schema/environment-config/feature-flags.mdx @@ -33,7 +33,7 @@ const FeatureFlagSchema = Schema.Struct({ enabled: Schema.Boolean, rolloutPercentage: Schema.pipe( Schema.Number, - Schema.between(0, 100) + Schema.check(Schema.isBetween(0, 100)) ), allowedUserIds: Schema.Array(Schema.String), allowedGroups: Schema.Array(Schema.String), @@ -155,7 +155,7 @@ class FeatureFlagService { // 8. Load feature flags from configuration const loadFeatureFlags = (config: any): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(FeaturesConfig)(config), + try: () => Schema.decodeUnknownEffect(FeaturesConfig)(config), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Feature flag validation failed: ${msg}`) diff --git a/content/published/patterns/schema/environment-config/secrets-redaction.mdx b/content/published/patterns/schema/environment-config/secrets-redaction.mdx index 427bc767..f3228474 100644 --- a/content/published/patterns/schema/environment-config/secrets-redaction.mdx +++ b/content/published/patterns/schema/environment-config/secrets-redaction.mdx @@ -29,7 +29,7 @@ import { Schema, Effect, Data } from "effect" // 1. Define secret types const SecretSchema = Schema.String.pipe( - Schema.minLength(1), + Schema.check(Schema.isMinLength(1)), Schema.brand("Secret") ) @@ -195,7 +195,7 @@ class ConfigService { const loadConfig = (rawConfig: any): Effect.Effect => Effect.gen(function* () { const validated = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(ApiConfig)(rawConfig), + try: () => Schema.decodeUnknownEffect(ApiConfig)(rawConfig), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Config validation failed: ${msg}`) diff --git a/content/published/patterns/schema/error-handling/error-aggregation.mdx b/content/published/patterns/schema/error-handling/error-aggregation.mdx index a949df7d..84fee9d3 100644 --- a/content/published/patterns/schema/error-handling/error-aggregation.mdx +++ b/content/published/patterns/schema/error-handling/error-aggregation.mdx @@ -44,20 +44,20 @@ type ValidationErrors = FieldError[] const SignUpForm = Schema.Struct({ username: Schema.String.pipe( - Schema.minLength(3), - Schema.maxLength(20), - Schema.pattern(/^[a-zA-Z0-9_-]+$/) + Schema.check(Schema.isMinLength(3)), + Schema.check(Schema.isMaxLength(20)), + Schema.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) ), email: Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ), password: Schema.String.pipe( - Schema.minLength(8) + Schema.check(Schema.isMinLength(8)) ), confirmPassword: Schema.String, age: Schema.Number.pipe( - Schema.int(), - Schema.between(13, 120) + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(13, 120)) ), }) @@ -173,30 +173,30 @@ const validateFormErrors = ( // Validate each field and collect errors let username = "" const usernameResult = yield* validateUsername(parsed.username).pipe( - Effect.either + Effect.result ) - if (usernameResult._tag === "Left") { - errors.push(usernameResult.left) + if (usernameResult._tag === "Failure") { + errors.push(usernameResult.failure) } else { - username = usernameResult.right + username = usernameResult.success } let email = "" - const emailResult = yield* validateEmail(parsed.email).pipe(Effect.either) - if (emailResult._tag === "Left") { - errors.push(emailResult.left) + const emailResult = yield* validateEmail(parsed.email).pipe(Effect.result) + if (emailResult._tag === "Failure") { + errors.push(emailResult.failure) } else { - email = emailResult.right + email = emailResult.success } let password = "" const passwordResult = yield* validatePassword(parsed.password).pipe( - Effect.either + Effect.result ) - if (passwordResult._tag === "Left") { - errors.push(passwordResult.left) + if (passwordResult._tag === "Failure") { + errors.push(passwordResult.failure) } else { - password = passwordResult.right + password = passwordResult.success } // Validate password match only if both passwords valid @@ -204,18 +204,18 @@ const validateFormErrors = ( const matchResult = yield* validatePasswordMatch( password, parsed.confirmPassword - ).pipe(Effect.either) - if (matchResult._tag === "Left") { - errors.push(matchResult.left) + ).pipe(Effect.result) + if (matchResult._tag === "Failure") { + errors.push(matchResult.failure) } } let age = 0 - const ageResult = yield* validateAge(parsed.age).pipe(Effect.either) - if (ageResult._tag === "Left") { - errors.push(ageResult.left) + const ageResult = yield* validateAge(parsed.age).pipe(Effect.result) + if (ageResult._tag === "Failure") { + errors.push(ageResult.failure) } else { - age = ageResult.right + age = ageResult.success } // Return all errors if any found @@ -270,12 +270,12 @@ const appLogic = Effect.gen(function* () { } const result1 = yield* validateFormErrors(badFormData).pipe( - Effect.either + Effect.result ) - if (result1._tag === "Left") { + if (result1._tag === "Failure") { console.log("❌ Validation failed with multiple errors:\n") - console.log(formatErrorSummary(result1.left)) + console.log(formatErrorSummary(result1.failure)) } console.log("=== Scenario 2: Form with some errors ===\n") @@ -289,12 +289,12 @@ const appLogic = Effect.gen(function* () { } const result2 = yield* validateFormErrors(partialFormData).pipe( - Effect.either + Effect.result ) - if (result2._tag === "Left") { + if (result2._tag === "Failure") { console.log("❌ Validation failed:\n") - console.log(formatErrorSummary(result2.left)) + console.log(formatErrorSummary(result2.failure)) } console.log("=== Scenario 3: Valid form ===\n") @@ -308,12 +308,12 @@ const appLogic = Effect.gen(function* () { } const result3 = yield* validateFormErrors(goodFormData).pipe( - Effect.either + Effect.result ) - if (result3._tag === "Right") { + if (result3._tag === "Success") { console.log("βœ… Form valid!") - console.log(result3.right) + console.log(result3.success) } return result3 @@ -330,7 +330,7 @@ Effect.runPromise(appLogic) | Concept | Explanation | |---------|-------------| | **Collect all errors** | Don't stop at first error; validate entire form | -| **Effect.either** | Convert error to value for collection without failing | +| **Effect.result** | Convert error to value for collection without failing | | **Array accumulation** | Push errors into array as they occur | | **Error summary** | Group by field for clear UI presentation | | **No early exit** | All fields validated independently | diff --git a/content/published/patterns/schema/error-handling/recovery-strategies.mdx b/content/published/patterns/schema/error-handling/recovery-strategies.mdx index 8aa2b12a..3d02eb24 100644 --- a/content/published/patterns/schema/error-handling/recovery-strategies.mdx +++ b/content/published/patterns/schema/error-handling/recovery-strategies.mdx @@ -25,7 +25,7 @@ An API call fails. Retry immediatelyβ€”it works the second time. But retry too f # Solution ```typescript -import { Effect, Duration, Data } from "effect" +import { Effect, Duration, Data, Schema } from "effect" // ============================================ // 1. Define domain errors @@ -69,7 +69,7 @@ const retryWithBackoff = ( ): Effect.Effect => { const attempt = (retriesLeft: number, currentDelayMs: number): Effect.Effect => effect.pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { if (retriesLeft === 0) { return Effect.fail(error) } @@ -120,7 +120,7 @@ const withFallback = ( shouldUseFallback?: (error: E) => boolean ): Effect.Effect => effect.pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { if (shouldUseFallback && !shouldUseFallback(error)) { return Effect.fail(error) } @@ -140,7 +140,7 @@ const withRecovery = ( } ): Effect.Effect => effect.pipe( - Effect.catchAll((error: E) => { + Effect.catch((error: E) => { const handler = handlers[error._tag as E["_tag"]] if (handler) { return (handler as any)(error) @@ -263,7 +263,7 @@ class CircuitBreaker { } return effect.pipe( - Effect.catchAll((error) => { + Effect.catch((error) => { this.failureCount++ if (this.failureCount >= this.failureThreshold) { @@ -304,12 +304,12 @@ const appLogic = Effect.gen(function* () { for (let i = 0; i < 5; i++) { const result = yield* breaker.execute( Effect.fail(new NetworkError("Service down")).pipe( - Effect.either + Effect.result ) ) - if (result._tag === "Left") { - console.log(`Request ${i + 1}: Failed -`, result.left.message) + if (result._tag === "Failure") { + console.log(`Request ${i + 1}: Failed -`, result.failure.message) } } diff --git a/content/published/patterns/schema/error-handling/tagged-errors.mdx b/content/published/patterns/schema/error-handling/tagged-errors.mdx index 43c1c7f9..4a731679 100644 --- a/content/published/patterns/schema/error-handling/tagged-errors.mdx +++ b/content/published/patterns/schema/error-handling/tagged-errors.mdx @@ -24,7 +24,7 @@ Your application throws generic `Error` objects. When calling a function, you do # Solution ```typescript -import { Data, Effect } from "effect" +import { Data, Effect, Schema } from "effect" // ============================================ // 1. Define custom tagged errors diff --git a/content/published/patterns/schema/error-handling/user-friendly-messages.mdx b/content/published/patterns/schema/error-handling/user-friendly-messages.mdx index 477e3e8b..cc82293c 100644 --- a/content/published/patterns/schema/error-handling/user-friendly-messages.mdx +++ b/content/published/patterns/schema/error-handling/user-friendly-messages.mdx @@ -24,7 +24,7 @@ Internal error: "TypeError: Cannot read property 'value' of undefined at line 34 # Solution ```typescript -import { Data, Effect } from "effect" +import { Data, Effect, Schema } from "effect" // ============================================ // 1. Define errors with multiple representations @@ -252,11 +252,11 @@ const appLogic = Effect.gen(function* () { console.log("=== Scenario 1: Validation Error ===\n") const validationResult = yield* validateEmail("invalid-email").pipe( - Effect.either + Effect.result ) - if (validationResult._tag === "Left") { - const error = validationResult.left + if (validationResult._tag === "Failure") { + const error = validationResult.failure console.log("πŸ“§ User sees:", error.userMessage()) console.log("πŸ”§ Dev sees:", error.techMessage()) Logger.error(error, { context: "email_validation" }) @@ -266,11 +266,11 @@ const appLogic = Effect.gen(function* () { console.log("=== Scenario 2: Network Error ===\n") const networkResult = yield* fetchUserData().pipe( - Effect.either + Effect.result ) - if (networkResult._tag === "Left") { - const error = networkResult.left + if (networkResult._tag === "Failure") { + const error = networkResult.failure console.log("πŸ‘€ User sees:", error.userMessage()) console.log("πŸ”§ Dev sees:", error.techMessage()) Logger.error(error, { context: "user_data_fetch" }) @@ -280,11 +280,11 @@ const appLogic = Effect.gen(function* () { console.log("=== Scenario 3: Payment Error ===\n") const paymentResult = yield* processPayment(50000).pipe( - Effect.either + Effect.result ) - if (paymentResult._tag === "Left") { - const error = paymentResult.left + if (paymentResult._tag === "Failure") { + const error = paymentResult.failure console.log("πŸ’³ User sees:", error.userMessage()) console.log("πŸ”§ Dev sees:", error.techMessage()) Logger.error(error, { context: "payment_processing", userId: "user_123" }) @@ -295,11 +295,11 @@ const appLogic = Effect.gen(function* () { const errors: (ValidationError | NetworkError | PaymentError)[] = [] - const emailErr = yield* validateEmail("bad").pipe(Effect.either) - if (emailErr._tag === "Left") errors.push(emailErr.left) + const emailErr = yield* validateEmail("bad").pipe(Effect.result) + if (emailErr._tag === "Failure") errors.push(emailErr.failure) - const paymentErr = yield* processPayment(15000).pipe(Effect.either) - if (paymentErr._tag === "Left") errors.push(paymentErr.left) + const paymentErr = yield* processPayment(15000).pipe(Effect.result) + if (paymentErr._tag === "Failure") errors.push(paymentErr.failure) console.log("πŸ“€ API Response to client:") console.log( diff --git a/content/published/patterns/schema/form-validation/async-validation.mdx b/content/published/patterns/schema/form-validation/async-validation.mdx index a1f26f12..2f440825 100644 --- a/content/published/patterns/schema/form-validation/async-validation.mdx +++ b/content/published/patterns/schema/form-validation/async-validation.mdx @@ -29,9 +29,9 @@ import { Effect, Schema, Schedule } from "effect" // 1. Sync validation: format only const Username = Schema.String.pipe( - Schema.minLength(3), - Schema.maxLength(20), - Schema.pattern(/^[a-zA-Z0-9_-]+$/) + Schema.check(Schema.isMinLength(3)), + Schema.check(Schema.isMaxLength(20)), + Schema.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) ) type Username = typeof Username.Type @@ -87,14 +87,14 @@ const checkEmailAvailable = (email: string) => const validateSignUp = (input: unknown) => Effect.gen(function* () { // Step 1: Sync validation - const syncData = yield* Schema.decodeUnknown( + const syncData = yield* Schema.decodeUnknownEffect( Schema.Struct({ username: Username, email: Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ), password: Schema.String.pipe( - Schema.minLength(8) + Schema.check(Schema.isMinLength(8)) ), }) )(input).pipe( @@ -129,7 +129,7 @@ const validateUsernameRealtime = ( ): Effect.Effect => Effect.gen(function* () { // Sync validation first - yield* Schema.decodeUnknown(Username)( + yield* Schema.decodeUnknownEffect(Username)( username ).pipe( Effect.mapError((error) => diff --git a/content/published/patterns/schema/form-validation/basic.mdx b/content/published/patterns/schema/form-validation/basic.mdx index 19e04ac0..466b2616 100644 --- a/content/published/patterns/schema/form-validation/basic.mdx +++ b/content/published/patterns/schema/form-validation/basic.mdx @@ -30,27 +30,27 @@ import { Schema, Effect } from "effect" const SignUpForm = Schema.Struct({ username: Schema.String.pipe( Schema.trimmed(), - Schema.minLength(3), - Schema.maxLength(20), - Schema.pattern(/^[a-zA-Z0-9_-]+$/) + Schema.check(Schema.isMinLength(3)), + Schema.check(Schema.isMaxLength(20)), + Schema.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) ), email: Schema.String.pipe( Schema.trimmed(), - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ), age: Schema.Number.pipe( - Schema.int(), - Schema.between(13, 120) + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(13, 120)) ), password: Schema.String.pipe( - Schema.minLength(8) + Schema.check(Schema.isMinLength(8)) ), }) type SignUpForm = typeof SignUpForm.Type // 2. Create validator -const validateForm = Schema.decodeUnknown(SignUpForm) +const validateForm = Schema.decodeUnknownEffect(SignUpForm) // 3. Format errors for UI const getErrorMessage = (error: any): string => { @@ -117,7 +117,7 @@ Effect.runPromise(submitForm(formData)) | `Schema.minLength/maxLength` | Enforce string length constraints | | `Schema.pattern` | Regex validation for format | | `Schema.between` | Numeric range validation | -| `Schema.decodeUnknown` | Parse form data with typed errors | +| `Schema.decodeUnknownEffect` | Parse form data with typed errors | | User-friendly messages | Map technical errors to readable text | # When to Use diff --git a/content/published/patterns/schema/form-validation/collect-all-errors.mdx b/content/published/patterns/schema/form-validation/collect-all-errors.mdx index 51353c46..7b6b2774 100644 --- a/content/published/patterns/schema/form-validation/collect-all-errors.mdx +++ b/content/published/patterns/schema/form-validation/collect-all-errors.mdx @@ -24,19 +24,19 @@ Standard validation stops at the first error: "Username is required". But users # Solution ```typescript -import { Schema, Effect, ParseResult } from "effect" +import { Schema, Effect } from "effect" // 1. Define form with multiple fields const RegistrationForm = Schema.Struct({ - firstName: Schema.String.pipe(Schema.minLength(1)), - lastName: Schema.String.pipe(Schema.minLength(1)), + firstName: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), + lastName: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), email: Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ), phone: Schema.String.pipe( - Schema.pattern(/^\d{10}$/) + Schema.check(Schema.isPattern(/^\d{10}$/)) ), - age: Schema.Number.pipe(Schema.between(18, 120)), + age: Schema.Number.pipe(Schema.check(Schema.isBetween(18, 120))), terms: Schema.Boolean.pipe( Schema.refine((b) => b === true, { message: "You must accept terms", @@ -49,7 +49,7 @@ type RegistrationForm = typeof RegistrationForm.Type // 2. Collect all errors const validateWithAllErrors = (input: unknown) => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown( + const result = yield* Schema.decodeUnknownEffect( RegistrationForm )(input).pipe( Effect.matchEffect({ diff --git a/content/published/patterns/schema/form-validation/dependent-fields.mdx b/content/published/patterns/schema/form-validation/dependent-fields.mdx index dfe84aec..51b3d950 100644 --- a/content/published/patterns/schema/form-validation/dependent-fields.mdx +++ b/content/published/patterns/schema/form-validation/dependent-fields.mdx @@ -30,10 +30,10 @@ import { Schema, Effect } from "effect" // 1. Define form with dependent fields const PasswordChangeForm = Schema.Struct({ currentPassword: Schema.String.pipe( - Schema.minLength(1) + Schema.check(Schema.isMinLength(1)) ), newPassword: Schema.String.pipe( - Schema.minLength(8) + Schema.check(Schema.isMinLength(8)) ), confirmPassword: Schema.String, }).pipe( @@ -76,10 +76,10 @@ type RegistrationForm = typeof RegistrationForm.Type const EventForm = Schema.Struct({ name: Schema.String, startDate: Schema.String.pipe( - Schema.pattern(/^\d{4}-\d{2}-\d{2}$/) + Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/)) ), endDate: Schema.String.pipe( - Schema.pattern(/^\d{4}-\d{2}-\d{2}$/) + Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/)) ), }).pipe( Schema.refine((form) => { @@ -96,7 +96,7 @@ type EventForm = typeof EventForm.Type // 4. Validate with field-specific error handling const validatePasswordChange = (input: unknown) => Effect.gen(function* () { - const result = yield* Schema.decodeUnknown( + const result = yield* Schema.decodeUnknownEffect( PasswordChangeForm )(input).pipe( Effect.matchEffect({ @@ -143,7 +143,7 @@ const eventData = { } Effect.runPromise( - Schema.decodeUnknown(EventForm)(eventData) + Schema.decodeUnknownEffect(EventForm)(eventData) ).catch((error) => { console.error("Date error:", error.message) }) diff --git a/content/published/patterns/schema/form-validation/nested-forms.mdx b/content/published/patterns/schema/form-validation/nested-forms.mdx index 20babefe..a2c4d844 100644 --- a/content/published/patterns/schema/form-validation/nested-forms.mdx +++ b/content/published/patterns/schema/form-validation/nested-forms.mdx @@ -29,11 +29,11 @@ import { Schema, Effect } from "effect" // 1. Define reusable sub-schemas const Address = Schema.Struct({ - street: Schema.String.pipe(Schema.minLength(1)), - city: Schema.String.pipe(Schema.minLength(1)), - state: Schema.String.pipe(Schema.length(2)), + street: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), + city: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), + state: Schema.String.pipe(Schema.check(Schema.isMinLength(2), Schema.isMaxLength(2))), zipCode: Schema.String.pipe( - Schema.pattern(/^\d{5}(-\d{4})?$/) + Schema.check(Schema.isPattern(/^\d{5}(-\d{4})?$/)) ), }) @@ -42,10 +42,10 @@ type Address = typeof Address.Type const LineItem = Schema.Struct({ productId: Schema.String, quantity: Schema.Number.pipe( - Schema.int(), - Schema.positive() + Schema.check(Schema.isInt()), + Schema.check(Schema.isGreaterThan(0)) ), - price: Schema.Number.pipe(Schema.positive()), + price: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0))), }) type LineItem = typeof LineItem.Type @@ -65,7 +65,7 @@ const Order = Schema.Struct({ description: "Order must have at least one item", }) ), - total: Schema.Number.pipe(Schema.positive()), + total: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0))), }) type Order = typeof Order.Type @@ -73,7 +73,7 @@ type Order = typeof Order.Type // 3. Validate entire nested structure const validateOrder = (input: unknown) => Effect.gen(function* () { - const order = yield* Schema.decodeUnknown(Order)( + const order = yield* Schema.decodeUnknownEffect(Order)( input ).pipe( Effect.mapError((error) => ({ @@ -110,14 +110,14 @@ const UserProfile = Schema.Struct({ Schema.Struct({ name: Schema.String, phone: Schema.String.pipe( - Schema.pattern(/^\d{10}$/) + Schema.check(Schema.isPattern(/^\d{10}$/)) ), - relationship: Schema.Literal( + relationship: Schema.Literals([ "spouse", "parent", "sibling", "other" - ), + ]), }) ).pipe(Schema.optional), }) diff --git a/content/published/patterns/schema/getting-started/handling-errors.mdx b/content/published/patterns/schema/getting-started/handling-errors.mdx index 67b9f3ec..32c6b577 100644 --- a/content/published/patterns/schema/getting-started/handling-errors.mdx +++ b/content/published/patterns/schema/getting-started/handling-errors.mdx @@ -23,7 +23,7 @@ When validation fails, you need to know what went wrong. The default parse error # Solution ```typescript -import { Schema, Effect, ParseResult } from "effect" +import { Schema, Effect } from "effect" const User = Schema.Struct({ name: Schema.String, @@ -40,35 +40,35 @@ function parseUserSync(data: unknown) { try { return decodeSync(data) } catch (error) { - if (ParseResult.isParseError(error)) { - console.log("Parse error:", ParseResult.TreeFormatter.formatErrorSync(error)) + if (Schema.isSchemaError(error)) { + console.log("Parse error:", error.message) } throw error } } -// 2. SYNC RETURNING EITHER (no exceptions) -const decodeEither = Schema.decodeUnknownEither(User) +// 2. SYNC RETURNING Result (no exceptions) +const decodeEither = Schema.decodeUnknownExit(User) function parseUserSafe(data: unknown) { const result = decodeEither(data) - if (result._tag === "Left") { - const message = ParseResult.TreeFormatter.formatErrorSync(result.left) + if (result._tag === "Failure") { + const message = result.failure.message return { success: false, error: message } } - return { success: true, user: result.right } + return { success: true, user: result.success } } // 3. ASYNC WITH EFFECT (recommended) -const decodeEffect = Schema.decodeUnknown(User) +const decodeEffect = Schema.decodeUnknownEffect(User) const parseUserEffect = (data: unknown) => decodeEffect(data).pipe( Effect.mapError((error) => ({ _tag: "ValidationError" as const, - message: ParseResult.TreeFormatter.formatErrorSync(error), + message: error.message, })) ) @@ -80,7 +80,7 @@ try { // Error already logged } -console.log("\n=== Sync returning Either ===") +console.log("\n=== Sync returning Result ===") const result = parseUserSafe({ name: 123, age: 30, email: "a@b.com" }) if (!result.success) { console.log("Validation failed:", result.error) @@ -101,9 +101,9 @@ Effect.runPromise( | Concept | Explanation | |---------|-------------| -| **ParseResult.isParseError** | Type guard to check if error is from Schema | +| **Schema.isSchemaError** | Type guard to check if error is from Schema | | **TreeFormatter** | Human-readable error messages | -| **decodeUnknownEither** | Returns Either instead of throwing | +| **decodeUnknownEither** | Returns Result instead of throwing | | **decodeUnknown** | Returns Effect for async composition | | **Effect.mapError** | Transform error into your error type | diff --git a/content/published/patterns/schema/getting-started/schema-vs-zod.mdx b/content/published/patterns/schema/getting-started/schema-vs-zod.mdx index da071dee..f141d549 100644 --- a/content/published/patterns/schema/getting-started/schema-vs-zod.mdx +++ b/content/published/patterns/schema/getting-started/schema-vs-zod.mdx @@ -23,7 +23,7 @@ You're familiar with Zod (or similar libraries) and want to understand how Effec # Solution ```typescript -import { Schema } from "effect" +import { Schema, SchemaTransformation } from "effect" // ============================================ // BASIC SCHEMA DEFINITION @@ -68,10 +68,10 @@ const List = Schema.Array(Schema.String) // const StatusZ = z.union([z.literal("active"), z.literal("inactive")]) // Effect Schema: -const Status = Schema.Union( +const Status = Schema.Union([ Schema.Literal("active"), Schema.Literal("inactive") -) +]) // ============================================ // REFINEMENTS (custom validation) @@ -82,7 +82,7 @@ const Status = Schema.Union( // Effect Schema: const Positive = Schema.Number.pipe( - Schema.filter((n) => n > 0, { message: () => "Must be positive" }) + Schema.check(Schema.makeFilter((n) => n > 0, { message: () => "Must be positive" })) ) // ============================================ @@ -93,14 +93,10 @@ const Positive = Schema.Number.pipe( // const TrimmedZ = z.string().transform(s => s.trim()) // Effect Schema (bidirectional!): -const Trimmed = Schema.transform( - Schema.String, - Schema.String, - { +const Trimmed = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (s) => s.trim(), encode: (s) => s, - } -) + }))) // ============================================ // KEY DIFFERENCES @@ -114,7 +110,7 @@ const user = decode({ name: "Alice", age: 30 }) const json = encode(user) // Zod can't do this! // 2. EFFECT INTEGRATION: Returns Effect for async composition -const parseAsync = Schema.decodeUnknown(User) +const parseAsync = Schema.decodeUnknownEffect(User) // Returns Effect - composable with Effect ecosystem // 3. DEFECTS VS ERRORS: Schema distinguishes recoverable vs non-recoverable diff --git a/content/published/patterns/schema/json-validation/config-files.mdx b/content/published/patterns/schema/json-validation/config-files.mdx index 0154f74a..4f0fc654 100644 --- a/content/published/patterns/schema/json-validation/config-files.mdx +++ b/content/published/patterns/schema/json-validation/config-files.mdx @@ -25,27 +25,27 @@ Configuration files are often JSON and must be loaded and validated at applicati ```typescript import { Schema, Effect } from "effect"; -import { FileSystem } from "@effect/platform"; +import { FileSystem } from "effect"; import { NodeFileSystem } from "@effect/platform-node"; // 1. Define configuration schema const AppConfig = Schema.Struct({ // Required fields - appName: Schema.String.pipe(Schema.minLength(1)), + appName: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), port: Schema.Number.pipe( - Schema.int(), - Schema.between(1, 65535) + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(1, 65535)) ), - environment: Schema.Literal("development", "staging", "production"), + environment: Schema.Literals(["development", "staging", "production"]), // Optional fields with defaults handled later debug: Schema.Boolean.pipe(Schema.optional), - logLevel: Schema.Literal("error", "warn", "info", "debug").pipe( + logLevel: Schema.Literals(["error", "warn", "info", "debug"]).pipe( Schema.optional ), maxConnections: Schema.Number.pipe( - Schema.int(), - Schema.positive() + Schema.check(Schema.isInt()), + Schema.check(Schema.isGreaterThan(0)) ).pipe(Schema.optional), }); @@ -75,7 +75,7 @@ const loadConfig = (filePath: string) => } // Validate config - const config = yield* Schema.decodeUnknown(AppConfig)( + const config = yield* Schema.decodeUnknownEffect(AppConfig)( jsonData ).pipe( Effect.mapError((error) => ({ @@ -112,7 +112,7 @@ const loadConfigWithFallback = ( ) => Effect.gen(function* () { const config = yield* loadConfig(primaryPath).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.gen(function* () { console.log( `⚠️ Failed to load ${primaryPath}, trying fallback...` @@ -158,7 +158,7 @@ Effect.runPromise( | `Schema.Literal` | Restrict values to allowed options (enums) | | `Schema.between` | Validate numeric ranges (port, connections) | | `Schema.optional` | Mark fields as not required | -| `Effect.catchAll` | Recover from first file load, try fallback | +| `Effect.catch` | Recover from first file load, try fallback | | Clear error hierarchy | Different error tags for different failure modes | | Startup validation | Fail fast if config is invalid before app starts | | Type safety | TypeScript prevents accessing missing config fields | diff --git a/content/published/patterns/schema/json-validation/database-columns.mdx b/content/published/patterns/schema/json-validation/database-columns.mdx index f0501489..9ccc6d03 100644 --- a/content/published/patterns/schema/json-validation/database-columns.mdx +++ b/content/published/patterns/schema/json-validation/database-columns.mdx @@ -28,17 +28,17 @@ import { Schema, Effect } from "effect"; // 1. Define schema for database JSON column const UserMetadata = Schema.Struct({ - theme: Schema.Literal("light", "dark", "auto").pipe( + theme: Schema.Literals(["light", "dark", "auto"]).pipe( Schema.optionalWith({ default: () => "auto" }) ), notifications: Schema.Boolean.pipe( Schema.optionalWith({ default: () => true }) ), - language: Schema.Literal("en", "es", "fr", "de").pipe( + language: Schema.Literals(["en", "es", "fr", "de"]).pipe( Schema.optionalWith({ default: () => "en" }) ), lastLogin: Schema.String.pipe( - Schema.pattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/) + Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/)) ).pipe(Schema.optional), preferences: Schema.Struct({ compactView: Schema.Boolean, @@ -72,7 +72,7 @@ const validateMetadataFromDb = (userId: string) => ); // 2. Validate against schema - const metadata = yield* Schema.decodeUnknown(UserMetadata)( + const metadata = yield* Schema.decodeUnknownEffect(UserMetadata)( rawData ).pipe( Effect.mapError((error) => ({ @@ -125,7 +125,7 @@ const validateMultipleMetadata = (userIds: string[]) => const results = yield* Effect.all( userIds.map((userId) => validateMetadataFromDb(userId).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed({ userId, valid: false, @@ -148,7 +148,7 @@ const validateMultipleMetadata = (userIds: string[]) => | Concept | Explanation | |---------|-------------| | Schema as contract | Define shape of JSON before retrieving from DB | -| `Schema.decodeUnknown()` | Convert untyped database JSON to typed data | +| `Schema.decodeUnknownEffect()` | Convert untyped database JSON to typed data | | Error channel | Validation failures don't throw; flow through Effect | | Defaults via `optionalWith` | Apply missing-field defaults at parse time | | Type-safe after validation | TypeScript knows validated data matches schema | diff --git a/content/published/patterns/schema/json-validation/file-validation.mdx b/content/published/patterns/schema/json-validation/file-validation.mdx index 007d28d8..a41e1f53 100644 --- a/content/published/patterns/schema/json-validation/file-validation.mdx +++ b/content/published/patterns/schema/json-validation/file-validation.mdx @@ -25,17 +25,17 @@ You have a JSON file on disk and need to read it, parse it, and validate the str ```typescript import { Schema, Effect } from "effect"; -import { FileSystem } from "@effect/platform"; +import { FileSystem } from "effect"; import { NodeFileSystem } from "@effect/platform-node"; // 1. Define schema for expected structure const UserProfile = Schema.Struct({ id: Schema.String, - name: Schema.String.pipe(Schema.minLength(1)), + name: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), email: Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ), - age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)), + age: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(0, 150))), }); type UserProfile = typeof UserProfile.Type; @@ -65,7 +65,7 @@ const validateJsonFile = (filePath: string) => } // Validate against schema - const profile = yield* Schema.decodeUnknown(UserProfile)( + const profile = yield* Schema.decodeUnknownEffect(UserProfile)( jsonData ).pipe( Effect.mapError((error) => ({ @@ -104,7 +104,7 @@ const parseJsonString = (jsonString: string) => ); } - const profile = yield* Schema.decodeUnknown(UserProfile)(data); + const profile = yield* Schema.decodeUnknownEffect(UserProfile)(data); return profile; }); @@ -124,7 +124,7 @@ const parseJsonString = (jsonString: string) => | `FileSystem` service | Platform-agnostic file operations bound to runtime | | `readFileString` | Reads entire file into memory safely | | `JSON.parse()` wrapped in try/catch | Captures JSON parsing errors without throwing | -| `Schema.decodeUnknown()` | Type-safe validation returning typed result | +| `Schema.decodeUnknownEffect()` | Type-safe validation returning typed result | | Effect error channel | No exceptions; errors flow through typed handler | | `provideLayer(NodeFileSystem.layer)` | Injects file system implementation at runtime | | Type derivation `typeof X.Type` | Full TypeScript type inference from schema | diff --git a/content/published/patterns/schema/json-validation/multiple-files.mdx b/content/published/patterns/schema/json-validation/multiple-files.mdx index b7baa50f..bee96f61 100644 --- a/content/published/patterns/schema/json-validation/multiple-files.mdx +++ b/content/published/patterns/schema/json-validation/multiple-files.mdx @@ -26,13 +26,13 @@ Complex applications often need to load multiple configuration files: database c ```typescript import { Schema, Effect } from "effect"; -import { FileSystem } from "@effect/platform"; +import { FileSystem } from "effect"; import { NodeFileSystem } from "@effect/platform-node"; // 1. Define schemas for different config types const DatabaseConfig = Schema.Struct({ - host: Schema.String.pipe(Schema.minLength(1)), - port: Schema.Number.pipe(Schema.int(), Schema.between(1, 65535)), + host: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), + port: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(1, 65535))), username: Schema.String, password: Schema.String, database: Schema.String, @@ -41,11 +41,11 @@ const DatabaseConfig = Schema.Struct({ type DatabaseConfig = typeof DatabaseConfig.Type; const ApiConfig = Schema.Struct({ - baseUrl: Schema.String.pipe(Schema.minLength(1)), - apiKey: Schema.String.pipe(Schema.minLength(10)), + baseUrl: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), + apiKey: Schema.String.pipe(Schema.check(Schema.isMinLength(10))), timeout: Schema.Number.pipe( - Schema.int(), - Schema.positive(), + Schema.check(Schema.isInt()), + Schema.check(Schema.isGreaterThan(0)), Schema.optionalWith({ default: () => 5000 }) ), }); @@ -76,7 +76,7 @@ const loadJsonFile = ( const content = yield* fs.readFileString(filePath); let jsonData: unknown = JSON.parse(content); - const result = yield* Schema.decodeUnknown(schema)(jsonData); + const result = yield* Schema.decodeUnknownEffect(schema)(jsonData); return result; }); @@ -129,7 +129,7 @@ const loadConfigsWithFallback = (configDir: string) => FeatureFlags, `${configDir}/features.json` ).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.succeed({ enableNewUI: false, enableAnalytics: true, @@ -221,7 +221,7 @@ Effect.runPromise( |---------|-------------| | `Effect.all([...], { concurrency: 3 })` | Load configs in parallel for speed | | Generic `loadJsonFile` function | Reusable for different schema types | -| `Effect.catchAll` for optional files | Gracefully fall back if feature flags missing | +| `Effect.catch` for optional files | Gracefully fall back if feature flags missing | | Typed error tags | Know exactly which config failed | | Required vs optional separation | Database/API required, features optional | | Parallel failure handling | If any required config fails, whole Effect fails | diff --git a/content/published/patterns/schema/json-validation/partial-documents.mdx b/content/published/patterns/schema/json-validation/partial-documents.mdx index 2e165f73..07c90f70 100644 --- a/content/published/patterns/schema/json-validation/partial-documents.mdx +++ b/content/published/patterns/schema/json-validation/partial-documents.mdx @@ -24,35 +24,35 @@ REST APIs support PATCH requests that update only specific fieldsβ€”users don't # Solution ```typescript -import { Schema, Effect } from "effect"; +import { Schema, Effect, Struct } from "effect"; // 1. Define full document schema const ProductSchema = Schema.Struct({ id: Schema.String, - name: Schema.String.pipe(Schema.minLength(1)), - description: Schema.String.pipe(Schema.minLength(10)), - price: Schema.Number.pipe(Schema.positive()), - stock: Schema.Number.pipe(Schema.int(), Schema.between(0, 10000)), - category: Schema.Literal( + name: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), + description: Schema.String.pipe(Schema.check(Schema.isMinLength(10))), + price: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0))), + stock: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(0, 10000))), + category: Schema.Literals([ "electronics", "clothing", "books", "other" - ), + ]), active: Schema.Boolean, }); type Product = typeof ProductSchema.Type; // 2. Create partial schema (all fields optional) -const ProductPatchSchema = Schema.partial(ProductSchema); +const ProductPatchSchema = Schema.Struct(Struct.map(ProductSchema.fields, Schema.optional)); type ProductPatch = typeof ProductPatchSchema.Type; // 3. Validate full product (PUT) const validateFullProduct = (input: unknown) => Effect.gen(function* () { - const product = yield* Schema.decodeUnknown(ProductSchema)( + const product = yield* Schema.decodeUnknownEffect(ProductSchema)( input ).pipe( Effect.mapError((error) => ({ @@ -67,7 +67,7 @@ const validateFullProduct = (input: unknown) => // 4. Validate partial product (PATCH) const validateProductPatch = (input: unknown) => Effect.gen(function* () { - const patch = yield* Schema.decodeUnknown( + const patch = yield* Schema.decodeUnknownEffect( ProductPatchSchema )(input).pipe( Effect.mapError((error) => ({ @@ -152,7 +152,7 @@ const batchUpdateProducts = ( const results = yield* Effect.all( updates.map(({ id, patch }) => updateProductInDb(id, patch).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed({ id, success: false, @@ -279,7 +279,7 @@ Effect.runPromise( | Concept | Explanation | |---------|-------------| -| `Schema.partial()` | Makes all fields optional while keeping type constraints | +| `Schema.Struct(Struct.map(.fields, Schema.optional))` | Makes all fields optional while keeping type constraints | | PATCH vs PUT | Different validation strategies for full vs partial updates | | Field merging | Only updated fields override existing values | | Re-validation | Full document valid after patch applied | diff --git a/content/published/patterns/schema/json-validation/postgres-jsonb.mdx b/content/published/patterns/schema/json-validation/postgres-jsonb.mdx index d5294ab6..24985ecf 100644 --- a/content/published/patterns/schema/json-validation/postgres-jsonb.mdx +++ b/content/published/patterns/schema/json-validation/postgres-jsonb.mdx @@ -31,18 +31,18 @@ const OrderMetadata = Schema.Struct({ // Required fields orderId: Schema.String, customerId: Schema.String, - status: Schema.Literal( + status: Schema.Literals([ "pending", "processing", "shipped", "delivered", "cancelled" - ), + ]), // Optional tracking data trackingNumber: Schema.String.pipe(Schema.optional), estimatedDelivery: Schema.String.pipe( - Schema.pattern(/^\d{4}-\d{2}-\d{2}$/) + Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/)) ).pipe(Schema.optional), // Nested JSONB structure @@ -50,15 +50,15 @@ const OrderMetadata = Schema.Struct({ Schema.Struct({ productId: Schema.String, quantity: Schema.Number.pipe( - Schema.int(), - Schema.positive() + Schema.check(Schema.isInt()), + Schema.check(Schema.isGreaterThan(0)) ), - price: Schema.Number.pipe(Schema.positive()), + price: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0))), }) ).pipe(Schema.minItems(1)), // Metadata with defaults - source: Schema.Literal("web", "mobile", "api").pipe( + source: Schema.Literals(["web", "mobile", "api"]).pipe( Schema.optionalWith({ default: () => "web" }) ), notes: Schema.String.pipe(Schema.optional), @@ -69,7 +69,7 @@ type OrderMetadata = typeof OrderMetadata.Type; // 2. Validate JSONB before INSERT const validateForInsert = (data: unknown) => Effect.gen(function* () { - const order = yield* Schema.decodeUnknown(OrderMetadata)( + const order = yield* Schema.decodeUnknownEffect(OrderMetadata)( data ).pipe( Effect.mapError((error) => ({ @@ -94,7 +94,7 @@ const validateFromPostgres = ( jsonbData: unknown ) => Effect.gen(function* () { - const order = yield* Schema.decodeUnknown(OrderMetadata)( + const order = yield* Schema.decodeUnknownEffect(OrderMetadata)( jsonbData ).pipe( Effect.mapError((error) => ({ @@ -170,7 +170,7 @@ const updateOrderStatus = ( }; // Validate before sending to DB - const validated = yield* Schema.decodeUnknown( + const validated = yield* Schema.decodeUnknownEffect( Schema.Struct({ orderId: Schema.String, status: OrderMetadata.fields.status, diff --git a/content/published/patterns/schema/json-validation/schema-evolution.mdx b/content/published/patterns/schema/json-validation/schema-evolution.mdx index 0f0aaa7f..784346e3 100644 --- a/content/published/patterns/schema/json-validation/schema-evolution.mdx +++ b/content/published/patterns/schema/json-validation/schema-evolution.mdx @@ -56,7 +56,7 @@ const UserProfileV3 = Schema.Struct({ phone: Schema.String.pipe(Schema.optional), createdAt: Schema.String, updatedAt: Schema.String.pipe(Schema.optional), - schemaVersion: Schema.Literal("v1", "v2", "v3").pipe( + schemaVersion: Schema.Literals(["v1", "v2", "v3"]).pipe( Schema.optionalWith({ default: () => "v3" }) ), }); @@ -151,7 +151,7 @@ const validateAndMigrate = (rawData: unknown) => switch (version) { case "v1": { - const v1 = yield* Schema.decodeUnknown(UserProfileV1)( + const v1 = yield* Schema.decodeUnknownEffect(UserProfileV1)( rawData ).pipe( Effect.mapError((error) => ({ @@ -164,7 +164,7 @@ const validateAndMigrate = (rawData: unknown) => } case "v2": { - const v2 = yield* Schema.decodeUnknown(UserProfileV2)( + const v2 = yield* Schema.decodeUnknownEffect(UserProfileV2)( rawData ).pipe( Effect.mapError((error) => ({ @@ -177,7 +177,7 @@ const validateAndMigrate = (rawData: unknown) => } case "v3": { - profile = yield* Schema.decodeUnknown(UserProfile)( + profile = yield* Schema.decodeUnknownEffect(UserProfile)( rawData ).pipe( Effect.mapError((error) => ({ @@ -200,7 +200,7 @@ const migrateHistoricalRecords = (records: unknown[]) => const results = yield* Effect.all( records.map((record, index) => validateAndMigrate(record).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed({ index, success: false, diff --git a/content/published/patterns/schema/json-validation/with-defaults.mdx b/content/published/patterns/schema/json-validation/with-defaults.mdx index 91ea1c22..6d24ccc2 100644 --- a/content/published/patterns/schema/json-validation/with-defaults.mdx +++ b/content/published/patterns/schema/json-validation/with-defaults.mdx @@ -25,32 +25,32 @@ Configuration files often have optional fields where users can omit them and use ```typescript import { Schema, Effect } from "effect"; -import { FileSystem } from "@effect/platform"; +import { FileSystem } from "effect"; import { NodeFileSystem } from "@effect/platform-node"; // 1. Define schema with default values using pipe const ServerConfig = Schema.Struct({ // Required fields - host: Schema.String.pipe(Schema.minLength(1)), - port: Schema.Number.pipe(Schema.int(), Schema.between(1, 65535)), + host: Schema.String.pipe(Schema.check(Schema.isMinLength(1))), + port: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(1, 65535))), // Optional fields with defaults applied at parse time timeout: Schema.Number.pipe( - Schema.int(), - Schema.positive(), + Schema.check(Schema.isInt()), + Schema.check(Schema.isGreaterThan(0)), Schema.optionalWith({ default: () => 5000 }) // 5 second default ), maxRetries: Schema.Number.pipe( - Schema.int(), - Schema.between(0, 10), + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(0, 10)), Schema.optionalWith({ default: () => 3 }) ), enableSSL: Schema.Boolean.pipe( Schema.optionalWith({ default: () => false }) ), compressionLevel: Schema.Number.pipe( - Schema.int(), - Schema.between(0, 9), + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(0, 9)), Schema.optionalWith({ default: () => 6 }) // Mid-range compression ), }); @@ -80,7 +80,7 @@ const loadServerConfig = (filePath: string) => } // Decode with defaults applied automatically - const config = yield* Schema.decodeUnknown(ServerConfig)( + const config = yield* Schema.decodeUnknownEffect(ServerConfig)( jsonData ).pipe( Effect.mapError((error) => ({ diff --git a/content/published/patterns/schema/objects/optional-fields.mdx b/content/published/patterns/schema/objects/optional-fields.mdx index d14ed384..bbb88ae7 100644 --- a/content/published/patterns/schema/objects/optional-fields.mdx +++ b/content/published/patterns/schema/objects/optional-fields.mdx @@ -67,7 +67,7 @@ console.log(`βœ… ${full.name}, ${full.age} years old`) const DatabaseRecord = Schema.Struct({ id: Schema.String, // Field exists but value may be null - deletedAt: Schema.NullOr(Schema.Date), + deletedAt: Schema.NullOr(Schema.DateFromString), parentId: Schema.NullOr(Schema.String), }) diff --git a/content/published/patterns/schema/primitives/date-validation.mdx b/content/published/patterns/schema/primitives/date-validation.mdx index ee1912f5..be0adcff 100644 --- a/content/published/patterns/schema/primitives/date-validation.mdx +++ b/content/published/patterns/schema/primitives/date-validation.mdx @@ -24,14 +24,14 @@ Dates arrive in many formats: ISO strings, Unix timestamps, Date objects. JavaSc # Solution ```typescript -import { Schema } from "effect" +import { Schema, SchemaTransformation } from "effect" // ============================================ // BUILT-IN DATE SCHEMAS // ============================================ // Date object validation -const DateSchema = Schema.Date +const DateSchema = Schema.DateFromString // βœ… new Date() β†’ Date // ❌ "not a date" β†’ ParseError @@ -45,62 +45,54 @@ const DateFromString = Schema.DateFromString // ============================================ // Unix timestamp (seconds) to Date -const DateFromUnix = Schema.transform( - Schema.Number, - Schema.DateFromSelf, - { +const DateFromUnix = Schema.Number.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (timestamp) => new Date(timestamp * 1000), encode: (date) => Math.floor(date.getTime() / 1000), - } -) + }))) // Unix timestamp (milliseconds) to Date -const DateFromMillis = Schema.transform( - Schema.Number, - Schema.DateFromSelf, - { +const DateFromMillis = Schema.Number.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (ms) => new Date(ms), encode: (date) => date.getTime(), - } -) + }))) // ============================================ // DATE REFINEMENTS // ============================================ // Valid date (not "Invalid Date") -const ValidDate = Schema.Date.pipe( - Schema.filter( +const ValidDate = Schema.DateFromString.pipe( + Schema.check(Schema.makeFilter( (d) => !isNaN(d.getTime()), { message: () => "Invalid date" } - ) + )) ) // Date must be in the future -const FutureDate = Schema.Date.pipe( - Schema.filter( +const FutureDate = Schema.DateFromString.pipe( + Schema.check(Schema.makeFilter( (d) => d.getTime() > Date.now(), { message: () => "Date must be in the future" } - ) + )) ) // Date must be in the past -const PastDate = Schema.Date.pipe( - Schema.filter( +const PastDate = Schema.DateFromString.pipe( + Schema.check(Schema.makeFilter( (d) => d.getTime() < Date.now(), { message: () => "Date must be in the past" } - ) + )) ) // Date within range -const RecentDate = Schema.Date.pipe( - Schema.filter( +const RecentDate = Schema.DateFromString.pipe( + Schema.check(Schema.makeFilter( (d) => { const oneYearAgo = Date.now() - 365 * 24 * 60 * 60 * 1000 return d.getTime() >= oneYearAgo && d.getTime() <= Date.now() }, { message: () => "Date must be within the last year" } - ) + )) ) // ============================================ @@ -147,11 +139,11 @@ console.log(`βœ… Created: ${payload.createdAt.toISOString()}`) | Schema | Purpose | |--------|---------| -| **Schema.Date** | Validates Date objects | +| **Schema.Date** | Validates Date instances | | **Schema.DateFromString** | Parses ISO strings to Date | -| **Schema.DateFromSelf** | Date-to-Date (for transforms) | -| **transform** | Convert timestamps to Date | -| **filter** | Custom date constraints | +| **Schema.Date** | Date-to-Date (for transforms) | +| **decodeTo** | Convert timestamps to Date | +| **check** | Custom date constraints | # When to Use diff --git a/content/published/patterns/schema/primitives/enums-literals.mdx b/content/published/patterns/schema/primitives/enums-literals.mdx index 23a1c376..fcc3838c 100644 --- a/content/published/patterns/schema/primitives/enums-literals.mdx +++ b/content/published/patterns/schema/primitives/enums-literals.mdx @@ -40,15 +40,15 @@ const Active = Schema.Literal("active") // ============================================ // Status enum -const Status = Schema.Literal("active", "inactive", "pending") +const Status = Schema.Literals(["active", "inactive", "pending"]) type Status = typeof Status.Type // "active" | "inactive" | "pending" // Role enum -const Role = Schema.Literal("admin", "user", "guest") +const Role = Schema.Literals(["admin", "user", "guest"]) type Role = typeof Role.Type // "admin" | "user" | "guest" // HTTP methods -const HttpMethod = Schema.Literal("GET", "POST", "PUT", "DELETE", "PATCH") +const HttpMethod = Schema.Literals(["GET", "POST", "PUT", "DELETE", "PATCH"]) type HttpMethod = typeof HttpMethod.Type // ============================================ @@ -56,11 +56,11 @@ type HttpMethod = typeof HttpMethod.Type // ============================================ // HTTP status codes -const SuccessCode = Schema.Literal(200, 201, 204) -const ErrorCode = Schema.Literal(400, 401, 403, 404, 500) +const SuccessCode = Schema.Literals([200, 201, 204]) +const ErrorCode = Schema.Literals([400, 401, 403, 404, 500]) // Priority levels -const Priority = Schema.Literal(1, 2, 3) +const Priority = Schema.Literals([1, 2, 3]) type Priority = typeof Priority.Type // 1 | 2 | 3 // ============================================ diff --git a/content/published/patterns/schema/primitives/number-validation.mdx b/content/published/patterns/schema/primitives/number-validation.mdx index 541f7979..ff03da49 100644 --- a/content/published/patterns/schema/primitives/number-validation.mdx +++ b/content/published/patterns/schema/primitives/number-validation.mdx @@ -52,21 +52,21 @@ const Int = Schema.Int // Between values (inclusive) const Percentage = Schema.Number.pipe( - Schema.between(0, 100, { + Schema.check(Schema.isBetween(0, 100, { message: () => "Percentage must be 0-100" - }) + })) ) // Greater than const OverEighteen = Schema.Number.pipe( - Schema.greaterThan(18, { + Schema.check(Schema.isGreaterThan(18, { message: () => "Must be over 18" - }) + })) ) // Less than or equal const MaxItems = Schema.Number.pipe( - Schema.lessThanOrEqualTo(1000) + Schema.check(Schema.isLessThanOrEqualTo(1000)) ) // ============================================ @@ -75,23 +75,23 @@ const MaxItems = Schema.Number.pipe( // Positive integer const PositiveInt = Schema.Number.pipe( - Schema.int(), - Schema.positive() + Schema.check(Schema.isInt()), + Schema.check(Schema.isGreaterThan(0)) ) // Valid age const Age = Schema.Number.pipe( - Schema.int({ message: () => "Age must be a whole number" }), - Schema.between(0, 150, { message: () => "Invalid age" }) + Schema.check(Schema.isInt({ message: () => "Age must be a whole number" })), + Schema.check(Schema.isBetween(0, 150, { message: () => "Invalid age" })) ) // Price (positive with 2 decimal places max) const Price = Schema.Number.pipe( - Schema.positive({ message: () => "Price must be positive" }), - Schema.filter( + Schema.check(Schema.isGreaterThan(0, { message: () => "Price must be positive" })), + Schema.check(Schema.makeFilter( (n) => Number.isFinite(n) && Math.round(n * 100) === n * 100, { message: () => "Price must have at most 2 decimal places" } - ) + )) ) // ============================================ @@ -100,7 +100,7 @@ const Price = Schema.Number.pipe( // Finite only (no Infinity) const Finite = Schema.Number.pipe( - Schema.finite({ message: () => "Must be a finite number" }) + Schema.check(Schema.isFinite({ message: () => "Must be a finite number" })) ) // ============================================ diff --git a/content/published/patterns/schema/primitives/string-validation.mdx b/content/published/patterns/schema/primitives/string-validation.mdx index 07743d2a..a06727fe 100644 --- a/content/published/patterns/schema/primitives/string-validation.mdx +++ b/content/published/patterns/schema/primitives/string-validation.mdx @@ -41,33 +41,33 @@ const Trimmed = Schema.Trimmed // Min/max length const Username = Schema.String.pipe( - Schema.minLength(3), - Schema.maxLength(20) + Schema.check(Schema.isMinLength(3)), + Schema.check(Schema.isMaxLength(20)) ) // Exact length -const ZipCode = Schema.String.pipe(Schema.length(5)) +const ZipCode = Schema.String.pipe(Schema.check(Schema.isMinLength(5), Schema.isMaxLength(5))) // ============================================ // PATTERN MATCHING (REGEX) // ============================================ const Email = Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, { + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, { message: () => "Invalid email format" - }) + })) ) const PhoneUS = Schema.String.pipe( - Schema.pattern(/^\d{3}-\d{3}-\d{4}$/, { + Schema.check(Schema.isPattern(/^\d{3}-\d{3}-\d{4}$/, { message: () => "Phone must be XXX-XXX-XXXX" - }) + })) ) const Slug = Schema.String.pipe( - Schema.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { + Schema.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { message: () => "Slug must be lowercase with hyphens" - }) + })) ) // ============================================ @@ -75,11 +75,11 @@ const Slug = Schema.String.pipe( // ============================================ const Password = Schema.String.pipe( - Schema.minLength(8, { message: () => "Password must be at least 8 characters" }), - Schema.maxLength(100), - Schema.pattern(/[A-Z]/, { message: () => "Must contain uppercase letter" }), - Schema.pattern(/[a-z]/, { message: () => "Must contain lowercase letter" }), - Schema.pattern(/[0-9]/, { message: () => "Must contain a number" }) + Schema.check(Schema.isMinLength(8, { message: () => "Password must be at least 8 characters" })), + Schema.check(Schema.isMaxLength(100)), + Schema.check(Schema.isPattern(/[A-Z]/, { message: () => "Must contain uppercase letter" })), + Schema.check(Schema.isPattern(/[a-z]/, { message: () => "Must contain lowercase letter" })), + Schema.check(Schema.isPattern(/[0-9]/, { message: () => "Must contain a number" })) ) // ============================================ diff --git a/content/published/patterns/schema/recursive/basic-recursive.mdx b/content/published/patterns/schema/recursive/basic-recursive.mdx index 00588df7..56f93822 100644 --- a/content/published/patterns/schema/recursive/basic-recursive.mdx +++ b/content/published/patterns/schema/recursive/basic-recursive.mdx @@ -32,7 +32,7 @@ import { Schema, Effect } from "effect" // Forward declare the schema (will be defined below) const LinkedListNode: Schema.Schema = Schema.suspend(() => - Schema.Union( + Schema.Union([ // Base case: End of list Schema.Struct({ type: Schema.Literal("empty"), @@ -43,7 +43,7 @@ const LinkedListNode: Schema.Schema = Schema.suspend(() => value: Schema.Number, next: LinkedListNode, }) - ) + ]) ) type LinkedListNode = { @@ -96,7 +96,7 @@ type MenuItem = { const parseLinkedList = (raw: unknown): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(LinkedListNode)(raw), + try: () => Schema.decodeUnknownEffect(LinkedListNode)(raw), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Invalid linked list: ${msg}`) @@ -105,7 +105,7 @@ const parseLinkedList = (raw: unknown): Effect.Effect => const parseTree = (raw: unknown): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(TreeNode)(raw), + try: () => Schema.decodeUnknownEffect(TreeNode)(raw), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Invalid tree: ${msg}`) @@ -114,7 +114,7 @@ const parseTree = (raw: unknown): Effect.Effect => const parseMenu = (raw: unknown): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(MenuItem)(raw), + try: () => Schema.decodeUnknownEffect(MenuItem)(raw), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Invalid menu: ${msg}`) diff --git a/content/published/patterns/schema/recursive/json-ast.mdx b/content/published/patterns/schema/recursive/json-ast.mdx index d7abb125..9b9f7842 100644 --- a/content/published/patterns/schema/recursive/json-ast.mdx +++ b/content/published/patterns/schema/recursive/json-ast.mdx @@ -40,7 +40,7 @@ type JsonValue = | { type: "object"; properties: Record } const JsonValue: Schema.Schema = Schema.suspend(() => - Schema.Union( + Schema.Union([ Schema.Struct({ type: Schema.Literal("null") }), Schema.Struct({ type: Schema.Literal("boolean"), @@ -62,7 +62,7 @@ const JsonValue: Schema.Schema = Schema.suspend(() => type: Schema.Literal("object"), properties: Schema.Record(Schema.String, JsonValue), }) - ) + ]) ) // ============================================ @@ -305,7 +305,7 @@ const appLogic = Effect.gen(function* () { // Validate AST structure const validationResult = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(JsonValue)(ast), + try: () => Schema.decodeUnknownEffect(JsonValue)(ast), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`AST validation failed: ${msg}`) diff --git a/content/published/patterns/schema/recursive/nested-comments.mdx b/content/published/patterns/schema/recursive/nested-comments.mdx index 8f28a6f4..288baa4f 100644 --- a/content/published/patterns/schema/recursive/nested-comments.mdx +++ b/content/published/patterns/schema/recursive/nested-comments.mdx @@ -54,9 +54,9 @@ const Comment: Schema.Schema = Schema.suspend(() => karma: Schema.Number, }), content: Schema.String, - createdAt: Schema.Date, + createdAt: Schema.DateFromString, score: Schema.Number, - edited: Schema.Optional(Schema.Date), + edited: Schema.Optional(Schema.DateFromString), replies: Schema.Array(Comment), }) ) @@ -80,7 +80,7 @@ const CommentThread = Schema.Struct({ title: Schema.String, author: Schema.String, content: Schema.String, - createdAt: Schema.Date, + createdAt: Schema.DateFromString, commentCount: Schema.Number, topComments: Schema.Array(Comment), }) diff --git a/content/published/patterns/schema/transformations/basic-transforms.mdx b/content/published/patterns/schema/transformations/basic-transforms.mdx index 1d87ac69..b59e195e 100644 --- a/content/published/patterns/schema/transformations/basic-transforms.mdx +++ b/content/published/patterns/schema/transformations/basic-transforms.mdx @@ -24,13 +24,10 @@ Your application receives data in one shape but needs it in another. API returns # Solution ```typescript -import { Schema, Effect } from "effect" +import { Schema, Effect, SchemaTransformation } from "effect" // 1. Basic string-to-number transformation -const StringToNumber = Schema.transform( - Schema.String, - Schema.Number, - { +const StringToNumber = Schema.String.pipe(Schema.decodeTo(Schema.Number, SchemaTransformation.transform({ decode: (input) => { const num = Number(input) if (isNaN(num)) { @@ -39,24 +36,16 @@ const StringToNumber = Schema.transform( return num }, encode: (num) => String(num), - } -) + }))) // 2. Unix timestamp to Date transformation -const UnixTimestamp = Schema.transform( - Schema.Number, - Schema.Date, - { +const UnixTimestamp = Schema.Number.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (timestamp) => new Date(timestamp * 1000), encode: (date) => Math.floor(date.getTime() / 1000), - } -) + }))) // 3. ISO date string to Date transformation -const ISODateString = Schema.transform( - Schema.String, - Schema.Date, - { +const ISODateString = Schema.String.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (input) => { const date = new Date(input) if (isNaN(date.getTime())) { @@ -65,28 +54,19 @@ const ISODateString = Schema.transform( return date }, encode: (date) => date.toISOString(), - } -) + }))) // 4. Trimmed string transformation -const Trimmed = Schema.transform( - Schema.String, - Schema.String, - { +const Trimmed = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.trim(), encode: (output) => output, // Already trimmed - } -) + }))) // 5. Uppercase transformation -const Uppercase = Schema.transform( - Schema.String, - Schema.String, - { +const Uppercase = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.toUpperCase(), encode: (output) => output, - } -) + }))) // 6. Define a user form with transformations const UserFormInput = Schema.Struct({ @@ -107,8 +87,8 @@ type User = { } // 7. Create decoder and encoder -const decodeUserForm = Schema.decodeUnknown(UserFormInput) -const encodeUserForm = Schema.encode(UserFormInput) +const decodeUserForm = Schema.decodeUnknownEffect(UserFormInput) +const encodeUserForm = Schema.encodeEffect(UserFormInput) // 8. Apply transformations in effect const processUserForm = (rawInput: unknown) => diff --git a/content/published/patterns/schema/transformations/bidirectional.mdx b/content/published/patterns/schema/transformations/bidirectional.mdx index 8f3b28ed..368817cc 100644 --- a/content/published/patterns/schema/transformations/bidirectional.mdx +++ b/content/published/patterns/schema/transformations/bidirectional.mdx @@ -26,7 +26,7 @@ Your application lives in three worlds: API contracts (what clients expect), dom # Solution ```typescript -import { Schema, Effect } from "effect" +import { Schema, Effect, SchemaTransformation } from "effect" // ============================================ // 1. Domain Layer (source of truth) @@ -79,37 +79,34 @@ type DbUserRow = { const ApiUserResponseSchema = Schema.Struct({ user_id: Schema.String, email: Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) ), full_name: Schema.String, - created_at: Schema.transform(Schema.Number, Schema.Date, { + created_at: Schema.Number.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (ms) => new Date(ms), encode: (date) => date.getTime(), - }), - updated_at: Schema.transform(Schema.Number, Schema.Date, { + }))), + updated_at: Schema.Number.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (ms) => new Date(ms), encode: (date) => date.getTime(), - }), + }))), is_active: Schema.Boolean, }).pipe( - Schema.transform( - Schema.Struct({ + Schema.Struct({ user_id: Schema.String, email: Schema.String, full_name: Schema.String, - created_at: Schema.Date, - updated_at: Schema.Date, + created_at: Schema.DateFromString, + updated_at: Schema.DateFromString, is_active: Schema.Boolean, - }), - Schema.Struct({ + }).pipe(Schema.decodeTo(Schema.Struct({ userId: Schema.String, email: Schema.String, fullName: Schema.String, - createdAt: Schema.Date, - updatedAt: Schema.Date, + createdAt: Schema.DateFromString, + updatedAt: Schema.DateFromString, isActive: Schema.Boolean, - }), - { + }), SchemaTransformation.transform({ decode: (api) => ({ userId: api.user_id, email: api.email, @@ -126,8 +123,7 @@ const ApiUserResponseSchema = Schema.Struct({ updated_at: domain.updatedAt, is_active: domain.isActive, }), - } - ) + }))) ) // ============================================ @@ -138,34 +134,31 @@ const DbUserRowSchema = Schema.Struct({ user_id: Schema.String, email: Schema.String, full_name: Schema.String, - created_at: Schema.transform(Schema.String, Schema.Date, { + created_at: Schema.String.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (isoString) => new Date(isoString), encode: (date) => date.toISOString(), - }), - updated_at: Schema.transform(Schema.String, Schema.Date, { + }))), + updated_at: Schema.String.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transform({ decode: (isoString) => new Date(isoString), encode: (date) => date.toISOString(), - }), + }))), is_active: Schema.Boolean, }).pipe( - Schema.transform( - Schema.Struct({ + Schema.Struct({ user_id: Schema.String, email: Schema.String, full_name: Schema.String, - created_at: Schema.Date, - updated_at: Schema.Date, + created_at: Schema.DateFromString, + updated_at: Schema.DateFromString, is_active: Schema.Boolean, - }), - Schema.Struct({ + }).pipe(Schema.decodeTo(Schema.Struct({ userId: Schema.String, email: Schema.String, fullName: Schema.String, - createdAt: Schema.Date, - updatedAt: Schema.Date, + createdAt: Schema.DateFromString, + updatedAt: Schema.DateFromString, isActive: Schema.Boolean, - }), - { + }), SchemaTransformation.transform({ decode: (db) => ({ userId: db.user_id, email: db.email, @@ -182,8 +175,7 @@ const DbUserRowSchema = Schema.Struct({ updated_at: domain.updatedAt, is_active: domain.isActive, }), - } - ) + }))) ) // ============================================ @@ -203,13 +195,13 @@ class UserRepository { is_active: true, } - const decoded = await Schema.decodeUnknown(DbUserRowSchema)(dbRow) + const decoded = await Schema.decodeUnknownEffect(DbUserRowSchema)(dbRow) return decoded } // Encode: Domain model β†’ DB row async save(user: DomainUser): Promise { - const dbRow = await Schema.encode(DbUserRowSchema)(user) + const dbRow = await Schema.encodeEffect(DbUserRowSchema)(user) console.log("πŸ“¦ Saving to database:", dbRow) } } @@ -223,7 +215,7 @@ class UserApiHandler { // Receive API response β†’ Domain β†’ Store async handleIncomingUser(apiData: unknown): Promise { - const domainUser = await Schema.decodeUnknown(ApiUserResponseSchema)(apiData) + const domainUser = await Schema.decodeUnknownEffect(ApiUserResponseSchema)(apiData) await this.repo.save(domainUser) return domainUser } @@ -236,7 +228,7 @@ class UserApiHandler { throw new Error("User not found") } - const apiResponse = await Schema.encode(ApiUserResponseSchema)(domainUser) + const apiResponse = await Schema.encodeEffect(ApiUserResponseSchema)(domainUser) return apiResponse } } diff --git a/content/published/patterns/schema/transformations/branded-types.mdx b/content/published/patterns/schema/transformations/branded-types.mdx index ad4c9a55..941f5a79 100644 --- a/content/published/patterns/schema/transformations/branded-types.mdx +++ b/content/published/patterns/schema/transformations/branded-types.mdx @@ -29,16 +29,16 @@ import { Schema, Effect } from "effect" // 1. Define branded types with validation const UserId = Schema.String.pipe( - Schema.minLength(1), - Schema.pattern(/^user_[a-z0-9]{12}$/), + Schema.check(Schema.isMinLength(1)), + Schema.check(Schema.isPattern(/^user_[a-z0-9]{12}$/)), Schema.brand("UserId") ) type UserId = typeof UserId.Type const ProductId = Schema.String.pipe( - Schema.minLength(1), - Schema.pattern(/^prod_[a-z0-9]{12}$/), + Schema.check(Schema.isMinLength(1)), + Schema.check(Schema.isPattern(/^prod_[a-z0-9]{12}$/)), Schema.brand("ProductId") ) @@ -46,7 +46,7 @@ type ProductId = typeof ProductId.Type // 2. Email branded type const Email = Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), Schema.brand("Email") ) @@ -54,9 +54,9 @@ type Email = typeof Email.Type // 3. UUID branded type const UUID = Schema.String.pipe( - Schema.pattern( + Schema.check(Schema.isPattern( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i - ), + )), Schema.brand("UUID") ) @@ -64,8 +64,8 @@ type UUID = typeof UUID.Type // 4. Positive integer branded type const PositiveInt = Schema.Number.pipe( - Schema.int(), - Schema.greaterThan(0), + Schema.check(Schema.isInt()), + Schema.check(Schema.isGreaterThan(0)), Schema.brand("PositiveInt") ) @@ -73,8 +73,8 @@ type PositiveInt = typeof PositiveInt.Type // 5. Slug branded type (URL-safe string) const Slug = Schema.String.pipe( - Schema.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), - Schema.maxLength(50), + Schema.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)), + Schema.check(Schema.isMaxLength(50)), Schema.brand("Slug") ) @@ -99,11 +99,11 @@ const Product = Schema.Struct({ type Product = typeof Product.Type // 7. Create validators -const validateUserId = Schema.decodeUnknown(UserId) -const validateEmail = Schema.decodeUnknown(Email) -const validateUUID = Schema.decodeUnknown(UUID) -const validateUser = Schema.decodeUnknown(User) -const validateProduct = Schema.decodeUnknown(Product) +const validateUserId = Schema.decodeUnknownEffect(UserId) +const validateEmail = Schema.decodeUnknownEffect(Email) +const validateUUID = Schema.decodeUnknownEffect(UUID) +const validateUser = Schema.decodeUnknownEffect(User) +const validateProduct = Schema.decodeUnknownEffect(Product) // 8. Database access with branded types class UserRepository { diff --git a/content/published/patterns/schema/transformations/data-normalization.mdx b/content/published/patterns/schema/transformations/data-normalization.mdx index 67f1038d..eecff2d3 100644 --- a/content/published/patterns/schema/transformations/data-normalization.mdx +++ b/content/published/patterns/schema/transformations/data-normalization.mdx @@ -25,32 +25,32 @@ Raw data is messy: extra whitespace, inconsistent casing, duplicates, mixed form # Solution ```typescript -import { Schema, Effect } from "effect" +import { Schema, Effect, SchemaTransformation } from "effect" // ============================================ // 1. String normalization transformations // ============================================ // Trim whitespace -const Trimmed = Schema.transform(Schema.String, Schema.String, { +const Trimmed = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.trim(), encode: (output) => output, -}) +}))) // Lowercase normalization -const Lowercase = Schema.transform(Schema.String, Schema.String, { +const Lowercase = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.toLowerCase(), encode: (output) => output, -}) +}))) // Uppercase normalization -const Uppercase = Schema.transform(Schema.String, Schema.String, { +const Uppercase = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.toUpperCase(), encode: (output) => output, -}) +}))) // Title case normalization -const TitleCase = Schema.transform(Schema.String, Schema.String, { +const TitleCase = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input .toLowerCase() @@ -58,18 +58,18 @@ const TitleCase = Schema.transform(Schema.String, Schema.String, { .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "), encode: (output) => output, -}) +}))) // ============================================ // 2. Email normalization (trim + lowercase) // ============================================ const Email = Schema.String.pipe( - Schema.transform(Schema.String, Schema.String, { + Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.trim().toLowerCase(), encode: (output) => output, - }), - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), + }))), + Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), Schema.brand("Email") ) @@ -79,10 +79,7 @@ type Email = typeof Email.Type // 3. Phone number normalization (remove non-digits, format) // ============================================ -const PhoneNumber = Schema.transform( - Schema.String, - Schema.String, - { +const PhoneNumber = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => { // Remove all non-digit characters const digits = input.replace(/\D/g, "") @@ -101,17 +98,13 @@ const PhoneNumber = Schema.transform( return digits }, encode: (output) => output, - } -) + }))) // ============================================ // 4. URL normalization (lowercase, trailing slash) // ============================================ -const NormalizedUrl = Schema.transform( - Schema.String, - Schema.String, - { +const NormalizedUrl = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => { let url = input.toLowerCase() // Remove trailing slash for consistency @@ -121,17 +114,13 @@ const NormalizedUrl = Schema.transform( return url }, encode: (output) => output, - } -) + }))) // ============================================ // 5. Tag/category normalization (trim, lowercase, deduplication) // ============================================ -const Tags = Schema.transform( - Schema.Array(Schema.String), - Schema.Array(Schema.String), - { +const Tags = Schema.Array(Schema.String).pipe(Schema.decodeTo(Schema.Array(Schema.String), SchemaTransformation.transform({ decode: (input) => { // Trim each tag, lowercase, remove duplicates const normalized = Array.from( @@ -144,8 +133,7 @@ const Tags = Schema.transform( return normalized.sort() // Sort for canonical order }, encode: (output) => output, - } -) + }))) // ============================================ // 6. Complex entity normalization @@ -166,16 +154,16 @@ type Product = typeof Product.Type // ============================================ const Address = Schema.Struct({ - street: Schema.transform(Schema.String, Schema.String, { + street: Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.trim().toUpperCase(), encode: (output) => output, - }), + }))), city: TitleCase, - state: Uppercase.pipe(Schema.maxLength(2)), - zip: Schema.transform(Schema.String, Schema.String, { + state: Uppercase.pipe(Schema.check(Schema.isMaxLength(2))), + zip: Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({ decode: (input) => input.replace(/\D/g, ""), // Only digits encode: (output) => output, - }), + }))), }) type Address = typeof Address.Type @@ -185,23 +173,23 @@ type Address = typeof Address.Type // ============================================ class NormalizationService { - normalizeProduct = Schema.decodeUnknown(Product) - normalizeAddress = Schema.decodeUnknown(Address) + normalizeProduct = Schema.decodeUnknownEffect(Product) + normalizeAddress = Schema.decodeUnknownEffect(Address) async normalizeEmail(email: string): Promise { - return Schema.decodeUnknown(Email)(email) + return Schema.decodeUnknownEffect(Email)(email) } async normalizePhoneNumber(phone: string): Promise { - return Schema.decodeUnknown(PhoneNumber)(phone) + return Schema.decodeUnknownEffect(PhoneNumber)(phone) } async normalizeUrl(url: string): Promise { - return Schema.decodeUnknown(NormalizedUrl)(url) + return Schema.decodeUnknownEffect(NormalizedUrl)(url) } async normalizeTags(tags: string[]): Promise { - return Schema.decodeUnknown(Tags)(tags) + return Schema.decodeUnknownEffect(Tags)(tags) } } diff --git a/content/published/patterns/schema/unions/basic-unions.mdx b/content/published/patterns/schema/unions/basic-unions.mdx index 6d4148b0..4cb8bcbd 100644 --- a/content/published/patterns/schema/unions/basic-unions.mdx +++ b/content/published/patterns/schema/unions/basic-unions.mdx @@ -31,10 +31,10 @@ import { Schema, Effect } from "effect" // ============================================ // A response is either a number or a string -const NumberOrString = Schema.Union( +const NumberOrString = Schema.Union([ Schema.Number, Schema.String -) +]) type NumberOrString = typeof NumberOrString.Type @@ -55,7 +55,7 @@ const GuestUser = Schema.Struct({ sessionId: Schema.String, }) -const User = Schema.Union(AuthenticatedUser, GuestUser) +const User = Schema.Union([AuthenticatedUser, GuestUser]) type User = typeof User.Type @@ -67,7 +67,7 @@ const PaymentSuccess = Schema.Struct({ status: Schema.Literal("success"), transactionId: Schema.String, amount: Schema.Number, - timestamp: Schema.Date, + timestamp: Schema.DateFromString, }) const PaymentFailure = Schema.Struct({ @@ -76,7 +76,7 @@ const PaymentFailure = Schema.Struct({ retryable: Schema.Boolean, }) -const PaymentResult = Schema.Union(PaymentSuccess, PaymentFailure) +const PaymentResult = Schema.Union([PaymentSuccess, PaymentFailure]) type PaymentResult = typeof PaymentResult.Type @@ -95,7 +95,7 @@ const ErrorResponse = Schema.Struct({ code: Schema.Number, }) -const ApiResponse = Schema.Union(SuccessResponse, ErrorResponse) +const ApiResponse = Schema.Union([SuccessResponse, ErrorResponse]) type ApiResponse = typeof ApiResponse.Type @@ -131,7 +131,7 @@ const parsePaymentResult = ( raw: unknown ): Effect.Effect => Effect.tryPromise({ - try: () => Schema.decodeUnknown(PaymentResult)(raw), + try: () => Schema.decodeUnknownEffect(PaymentResult)(raw), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Invalid payment result: ${msg}`) @@ -167,7 +167,7 @@ const appLogic = Effect.gen(function* () { console.log("=== Scenario 1: Parse simple union ===\n") const value1 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(NumberOrString)("hello"), + try: () => Schema.decodeUnknownEffect(NumberOrString)("hello"), catch: (error) => new Error(String(error)), }) @@ -183,7 +183,7 @@ const appLogic = Effect.gen(function* () { } const authenticated = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(User)(authenticatedData), + try: () => Schema.decodeUnknownEffect(User)(authenticatedData), catch: (error) => new Error(String(error)), }) @@ -195,7 +195,7 @@ const appLogic = Effect.gen(function* () { } const guest = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(User)(guestData), + try: () => Schema.decodeUnknownEffect(User)(guestData), catch: (error) => new Error(String(error)), }) @@ -230,7 +230,7 @@ const appLogic = Effect.gen(function* () { } const apiOk = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(ApiResponse)(okResponse), + try: () => Schema.decodeUnknownEffect(ApiResponse)(okResponse), catch: (error) => new Error(String(error)), }) @@ -245,7 +245,7 @@ const appLogic = Effect.gen(function* () { } const apiError = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(ApiResponse)(errorResponse), + try: () => Schema.decodeUnknownEffect(ApiResponse)(errorResponse), catch: (error) => new Error(String(error)), }) diff --git a/content/published/patterns/schema/unions/discriminated-unions.mdx b/content/published/patterns/schema/unions/discriminated-unions.mdx index 242da750..c4d7e294 100644 --- a/content/published/patterns/schema/unions/discriminated-unions.mdx +++ b/content/published/patterns/schema/unions/discriminated-unions.mdx @@ -34,13 +34,13 @@ const UserCreatedEvent = Schema.Struct({ _tag: Schema.Literal("UserCreated"), userId: Schema.String, email: Schema.String, - createdAt: Schema.Date, + createdAt: Schema.DateFromString, }) const UserDeletedEvent = Schema.Struct({ _tag: Schema.Literal("UserDeleted"), userId: Schema.String, - deletedAt: Schema.Date, + deletedAt: Schema.DateFromString, reason: Schema.String, }) @@ -52,11 +52,11 @@ const UserEmailChangedEvent = Schema.Struct({ verificationRequired: Schema.Boolean, }) -const UserEvent = Schema.Union( +const UserEvent = Schema.Union([ UserCreatedEvent, UserDeletedEvent, UserEmailChangedEvent -) +]) type UserEvent = typeof UserEvent.Type @@ -76,7 +76,7 @@ const OrderPlacedEvent = Schema.Struct({ }) ), totalAmount: Schema.Number, - timestamp: Schema.Date, + timestamp: Schema.DateFromString, }) const OrderShippedEvent = Schema.Struct({ @@ -84,7 +84,7 @@ const OrderShippedEvent = Schema.Struct({ orderId: Schema.String, trackingNumber: Schema.String, carrier: Schema.String, - estimatedDelivery: Schema.Date, + estimatedDelivery: Schema.DateFromString, }) const OrderCancelledEvent = Schema.Struct({ @@ -92,14 +92,14 @@ const OrderCancelledEvent = Schema.Struct({ orderId: Schema.String, reason: Schema.String, refundAmount: Schema.Number, - refundProcessedAt: Schema.Date, + refundProcessedAt: Schema.DateFromString, }) -const OrderEvent = Schema.Union( +const OrderEvent = Schema.Union([ OrderPlacedEvent, OrderShippedEvent, OrderCancelledEvent -) +]) type OrderEvent = typeof OrderEvent.Type @@ -218,7 +218,7 @@ const parseEvent = ( Effect.tryPromise({ try: async () => { const schema = eventType === "user" ? UserEvent : OrderEvent - return await Schema.decodeUnknown(schema)(raw) + return await Schema.decodeUnknownEffect(schema)(raw) }, catch: (error) => { const msg = error instanceof Error ? error.message : String(error) diff --git a/content/published/patterns/schema/unions/exhaustive-matching.mdx b/content/published/patterns/schema/unions/exhaustive-matching.mdx index 0489456f..9a510617 100644 --- a/content/published/patterns/schema/unions/exhaustive-matching.mdx +++ b/content/published/patterns/schema/unions/exhaustive-matching.mdx @@ -30,7 +30,7 @@ import { Schema, Effect } from "effect" // 1. Define comprehensive union // ============================================ -const OrderStatusEvent = Schema.Union( +const OrderStatusEvent = Schema.Union([ Schema.Struct({ _tag: Schema.Literal("OrderCreated"), orderId: Schema.String, @@ -49,14 +49,14 @@ const OrderStatusEvent = Schema.Union( Schema.Struct({ _tag: Schema.Literal("OrderDelivered"), orderId: Schema.String, - deliveryDate: Schema.Date, + deliveryDate: Schema.DateFromString, }), Schema.Struct({ _tag: Schema.Literal("OrderCancelled"), orderId: Schema.String, reason: Schema.String, }) -) +]) type OrderStatusEvent = typeof OrderStatusEvent.Type diff --git a/content/published/patterns/schema/unions/polymorphic-apis.mdx b/content/published/patterns/schema/unions/polymorphic-apis.mdx index 0e91d3fc..3c2aae96 100644 --- a/content/published/patterns/schema/unions/polymorphic-apis.mdx +++ b/content/published/patterns/schema/unions/polymorphic-apis.mdx @@ -37,7 +37,7 @@ const PaginatedResponse = Schema.Struct({ Schema.Struct({ id: Schema.String, name: Schema.String, - createdAt: Schema.Date, + createdAt: Schema.DateFromString, }) ), page: Schema.Number, @@ -52,8 +52,8 @@ const SingleItemResponse = Schema.Struct({ id: Schema.String, name: Schema.String, description: Schema.String, - createdAt: Schema.Date, - updatedAt: Schema.Date, + createdAt: Schema.DateFromString, + updatedAt: Schema.DateFromString, tags: Schema.Array(Schema.String), }) @@ -71,7 +71,7 @@ const BatchResponse = Schema.Struct({ data: Schema.Any, }) ), - processedAt: Schema.Date, + processedAt: Schema.DateFromString, failureCount: Schema.Number, }) @@ -82,16 +82,16 @@ const ErrorResponse = Schema.Struct({ message: Schema.String, details: Schema.Optional(Schema.Record(Schema.String, Schema.Any)), requestId: Schema.String, - timestamp: Schema.Date, + timestamp: Schema.DateFromString, }) // Unified polymorphic API response -const ApiResponse = Schema.Union( +const ApiResponse = Schema.Union([ PaginatedResponse, SingleItemResponse, BatchResponse, ErrorResponse -) +]) type ApiResponse = typeof ApiResponse.Type @@ -126,7 +126,7 @@ const fetchSearchResults = ( } return yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(ApiResponse)(response), + try: () => Schema.decodeUnknownEffect(ApiResponse)(response), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Search API error: ${msg}`) @@ -153,7 +153,7 @@ const fetchItemDetail = (id: string): Effect.Effect => } return yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(ApiResponse)(response), + try: () => Schema.decodeUnknownEffect(ApiResponse)(response), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Detail API error: ${msg}`) @@ -182,7 +182,7 @@ const processBatch = ( } return yield* Effect.tryPromise({ - try: () => Schema.decodeUnknown(ApiResponse)(response), + try: () => Schema.decodeUnknownEffect(ApiResponse)(response), catch: (error) => { const msg = error instanceof Error ? error.message : String(error) return new Error(`Batch API error: ${msg}`) diff --git a/content/published/patterns/schema/validating-api-responses/basic.mdx b/content/published/patterns/schema/validating-api-responses/basic.mdx index f8b5b2c6..6295a7f6 100644 --- a/content/published/patterns/schema/validating-api-responses/basic.mdx +++ b/content/published/patterns/schema/validating-api-responses/basic.mdx @@ -43,7 +43,7 @@ const User = Schema.Struct({ type User = typeof User.Type // 3. Create a decoder Effect -const parseUser = Schema.decodeUnknown(User) +const parseUser = Schema.decodeUnknownEffect(User) // 4. Use in an Effect pipeline const fetchUser = (id: number) => @@ -75,7 +75,7 @@ await Effect.runPromise(main) | Concept | Explanation | |---------|-------------| | **Schema.Struct** | Defines expected object shape with runtime + compile-time typesβ€”single source of truth | -| **Schema.decodeUnknown** | Returns `Effect` β€” validation errors flow through Effect's error channel, never thrown | +| **Schema.decodeUnknownEffect** | Returns `Effect` β€” validation errors flow through Effect's error channel, never thrown | | **typeof User.Type** | Extracts the TypeScript type from the schemaβ€”keeps type and validation in sync | | **Effect.gen** | Sequences async operations cleanly, errors propagate automatically through the pipeline | | **ParseError** | Contains detailed error info: what field failed, what was expected, what was received | diff --git a/content/published/patterns/schema/validating-api-responses/error-handling.mdx b/content/published/patterns/schema/validating-api-responses/error-handling.mdx index ef3c3fe3..63c845b6 100644 --- a/content/published/patterns/schema/validating-api-responses/error-handling.mdx +++ b/content/published/patterns/schema/validating-api-responses/error-handling.mdx @@ -37,7 +37,7 @@ const User = Schema.Struct({ type User = typeof User.Type -const parseUser = Schema.decodeUnknown(User) +const parseUser = Schema.decodeUnknownEffect(User) // Strategy 1: Catch and provide default const fetchUserWithDefault = (id: number, defaultUser: User) => @@ -90,14 +90,14 @@ const UserV2 = Schema.Struct({ const parseUserV1Or2 = (data: unknown) => Effect.gen(function* () { // Try V1 first - const v1Result = yield* Effect.exit(Schema.decodeUnknown(UserV1)(data)) + const v1Result = yield* Effect.exit(Schema.decodeUnknownEffect(UserV1)(data)) if (Exit.isSuccess(v1Result)) { return v1Result.value } // Fall back to V2 - const v2Result = yield* Schema.decodeUnknown(UserV2)(data) + const v2Result = yield* Schema.decodeUnknownEffect(UserV2)(data) return v2Result }) @@ -115,7 +115,7 @@ const fetchUsersStrict = (ids: number[]) => // This will fail on first invalid response const users = yield* Effect.forEach( responses, - (response) => Schema.decodeUnknown(User)(response) + (response) => Schema.decodeUnknownEffect(User)(response) ) yield* Effect.log(`Successfully decoded ${users.length} users`) diff --git a/content/published/patterns/schema/validating-api-responses/nested-responses.mdx b/content/published/patterns/schema/validating-api-responses/nested-responses.mdx index 90605d5c..fe873d6a 100644 --- a/content/published/patterns/schema/validating-api-responses/nested-responses.mdx +++ b/content/published/patterns/schema/validating-api-responses/nested-responses.mdx @@ -68,10 +68,10 @@ const User = Schema.Struct({ type User = typeof User.Type // Now create decoders at each level -const parseCoordinate = Schema.decodeUnknown(Coordinate) -const parseAddress = Schema.decodeUnknown(Address) -const parseProfile = Schema.decodeUnknown(Profile) -const parseUser = Schema.decodeUnknown(User) +const parseCoordinate = Schema.decodeUnknownEffect(Coordinate) +const parseAddress = Schema.decodeUnknownEffect(Address) +const parseProfile = Schema.decodeUnknownEffect(Profile) +const parseUser = Schema.decodeUnknownEffect(User) // Use in pipeline const fetchUser = (id: number) => @@ -99,7 +99,7 @@ const fetchUserPartial = (id: number) => ) // Validate user fields first - const userFields = yield* Schema.decodeUnknown( + const userFields = yield* Schema.decodeUnknownEffect( Schema.Struct({ id: Schema.Number, name: Schema.String, @@ -131,7 +131,7 @@ const fetchUserPartial = (id: number) => // Paginated response example const UserResponse = Schema.Struct({ - status: Schema.Literal("success", "error"), + status: Schema.Literals(["success", "error"]), data: Schema.Array(User), meta: Schema.Struct({ total: Schema.Number, @@ -142,7 +142,7 @@ const UserResponse = Schema.Struct({ type UserResponse = typeof UserResponse.Type -const parseUserResponse = Schema.decodeUnknown(UserResponse) +const parseUserResponse = Schema.decodeUnknownEffect(UserResponse) const fetchUsers = (page: number) => Effect.gen(function* () { diff --git a/content/published/patterns/schema/validating-api-responses/union-responses.mdx b/content/published/patterns/schema/validating-api-responses/union-responses.mdx index ae86fa3c..ae9e4b8d 100644 --- a/content/published/patterns/schema/validating-api-responses/union-responses.mdx +++ b/content/published/patterns/schema/validating-api-responses/union-responses.mdx @@ -55,15 +55,15 @@ const PendingResponse = Schema.Struct({ }) // Union: Response is one of these three shapes -const Response = Schema.Union( +const Response = Schema.Union([ SuccessResponse, ErrorResponse, PendingResponse -) +]) type Response = typeof Response.Type -const parseResponse = Schema.decodeUnknown(Response) +const parseResponse = Schema.decodeUnknownEffect(Response) // Use in a pipeline with pattern matching const createUser = (name: string, email: string) => @@ -138,7 +138,7 @@ const createUserWithMatch = (name: string, email: string) => }) // Handling discriminated unions with explicit discriminator -const PaymentResult = Schema.Union( +const PaymentResult = Schema.Union([ Schema.Struct({ type: Schema.Literal("approved"), transactionId: Schema.String, @@ -152,11 +152,11 @@ const PaymentResult = Schema.Union( type: Schema.Literal("requires-auth"), authUrl: Schema.String, }) -) +]) type PaymentResult = typeof PaymentResult.Type -const parsePayment = Schema.decodeUnknown(PaymentResult) +const parsePayment = Schema.decodeUnknownEffect(PaymentResult) const processPayment = (amount: number) => Effect.gen(function* () { diff --git a/content/published/patterns/schema/validating-api-responses/with-http-client.mdx b/content/published/patterns/schema/validating-api-responses/with-http-client.mdx index e93f7fb5..abb7a9d9 100644 --- a/content/published/patterns/schema/validating-api-responses/with-http-client.mdx +++ b/content/published/patterns/schema/validating-api-responses/with-http-client.mdx @@ -32,8 +32,8 @@ You need an HTTP client that: # Solution ```typescript -import { Effect, Schema, Duration, HttpClient, Layer } from "effect" -import { HttpClientRequest, HttpClientResponse } from "@effect/platform" +import { Effect, Schema, Duration, Layer, Context } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" // 1. Define the schema const User = Schema.Struct({ @@ -44,14 +44,14 @@ const User = Schema.Struct({ type User = typeof User.Type -const parseUser = Schema.decodeUnknown(User) +const parseUser = Schema.decodeUnknownEffect(User) // 2. Create a typed HTTP client service interface UserClient { readonly getUser: (id: number) => Effect.Effect } -const UserClient = Effect.Tag() +const UserClient = Context.Service() // 3. Implement the service const UserClientLive = Layer.succeed( @@ -127,8 +127,8 @@ await Effect.runPromise(main.pipe(Effect.provide(layer))) ## More Advanced: Custom Client with Middleware ```typescript -import { Effect, Schema, Duration, HttpClient, Layer, Fiber } from "effect" -import { HttpClientRequest } from "@effect/platform" +import { Effect, Schema, Duration, Layer, Fiber } from "effect" +import { HttpClient, HttpClientRequest } from "effect/http" // Schemas const User = Schema.Struct({ @@ -139,7 +139,7 @@ const User = Schema.Struct({ type User = typeof User.Type -const parseUser = Schema.decodeUnknown(User) +const parseUser = Schema.decodeUnknownEffect(User) // Error type class ApiError extends Error { @@ -162,7 +162,7 @@ interface ApiClient { readonly baseUrl: string } -const ApiClient = Effect.Tag() +const ApiClient = Context.Service() const createApiClient = (baseUrl: string): Layer.Layer => Layer.succeed(ApiClient, { @@ -200,7 +200,7 @@ const createApiClient = (baseUrl: string): Layer.Layer => // Parse and validate const body = yield* response.json - const parsed = yield* Schema.decodeUnknown(schema)(body).pipe( + const parsed = yield* Schema.decodeUnknownEffect(schema)(body).pipe( Effect.mapError( (error) => new ApiError( diff --git a/content/published/patterns/schema/validating-api-responses/with-retry.mdx b/content/published/patterns/schema/validating-api-responses/with-retry.mdx index bde608bb..4c6919c7 100644 --- a/content/published/patterns/schema/validating-api-responses/with-retry.mdx +++ b/content/published/patterns/schema/validating-api-responses/with-retry.mdx @@ -42,7 +42,7 @@ const User = Schema.Struct({ type User = typeof User.Type -const parseUser = Schema.decodeUnknown(User) +const parseUser = Schema.decodeUnknownEffect(User) // Custom errors to distinguish failure modes class NetworkError extends Error { @@ -68,7 +68,7 @@ const fetchUserRaw = (id: number) => return r.json() }) ).pipe( - Effect.catchAll((error) => + Effect.catch((error) => // Wrap fetch errors as NetworkError Effect.fail(new NetworkError(String(error))) ) diff --git a/content/published/patterns/schema/web-standards-validation/email.mdx b/content/published/patterns/schema/web-standards-validation/email.mdx index 07265df6..c9429c5c 100644 --- a/content/published/patterns/schema/web-standards-validation/email.mdx +++ b/content/published/patterns/schema/web-standards-validation/email.mdx @@ -30,11 +30,11 @@ import { Schema, Effect } from "effect" // 1. Define branded Email type with validation const Email = Schema.String.pipe( Schema.trimmed(), - Schema.minLength(5), - Schema.maxLength(254), - Schema.pattern( + Schema.check(Schema.isMinLength(5)), + Schema.check(Schema.isMaxLength(254)), + Schema.check(Schema.isPattern( /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - ).pipe( + )).pipe( Schema.annotations({ description: "Valid email format (local@domain.tld)", }) @@ -45,13 +45,13 @@ const Email = Schema.String.pipe( type Email = typeof Email.Type // 2. Create parser -const parseEmail = Schema.decodeUnknown(Email) +const parseEmail = Schema.decodeUnknownEffect(Email) // 3. Use in request schemas const CreateUserRequest = Schema.Struct({ name: Schema.String, email: Email, - password: Schema.String.pipe(Schema.minLength(8)), + password: Schema.String.pipe(Schema.check(Schema.isMinLength(8))), }) type CreateUserRequest = typeof CreateUserRequest.Type @@ -59,7 +59,7 @@ type CreateUserRequest = typeof CreateUserRequest.Type // 4. Validate and use const createUser = (input: unknown) => Effect.gen(function* () { - const request = yield* Schema.decodeUnknown( + const request = yield* Schema.decodeUnknownEffect( CreateUserRequest )(input).pipe( Effect.mapError((error) => ({ @@ -97,7 +97,7 @@ Effect.runPromise(createUser(userInput)) |---------|-------------| | `Schema.pattern` | Regex validation at runtimeβ€”catches invalid format | | `Schema.trimmed()` | Remove leading/trailing whitespace | -| `Schema.maxLength(254)` | Email RFC 5321 limit | +| `Schema.check(Schema.isMaxLength(254))` | Email RFC 5321 limit | | `Schema.brand("Email")` | Creates nominal typeβ€”`Email β‰  string` | | Compile-time safety | Functions requiring `Email` reject raw strings at compile time | | Composable | Reuse `Email` schema in any struct | diff --git a/content/published/patterns/schema/web-standards-validation/http-headers.mdx b/content/published/patterns/schema/web-standards-validation/http-headers.mdx index d871ccfe..725b02f8 100644 --- a/content/published/patterns/schema/web-standards-validation/http-headers.mdx +++ b/content/published/patterns/schema/web-standards-validation/http-headers.mdx @@ -28,28 +28,28 @@ Your HTTP server receives headers as stringsβ€”`Content-Type: text/plain`, `Auth import { Schema, Effect } from "effect" // 1. Define common header schemas -const ContentType = Schema.Union( - Schema.Literal( +const ContentType = Schema.Union([ + Schema.Literals([ "application/json", "text/plain", "text/html", "application/octet-stream" - ), + ]), Schema.String.pipe( - Schema.pattern(/^[\w\-]+\/[\w\.\+\-]+$/), + Schema.check(Schema.isPattern(/^[\w\-]+\/[\w\.\+\-]+$/)), Schema.annotations({ description: "MIME type (type/subtype)", }) ) -).pipe(Schema.brand("ContentType")) +]).pipe(Schema.brand("ContentType")) type ContentType = typeof ContentType.Type // 2. Authorization header const Authorization = Schema.String.pipe( - Schema.pattern( + Schema.check(Schema.isPattern( /^Bearer [A-Za-z0-9\-\._~\+\/]+=*$/ - ).pipe( + )).pipe( Schema.annotations({ description: "Authorization: Bearer ", @@ -62,12 +62,12 @@ type Authorization = typeof Authorization.Type // 3. Custom header validation const CustomHeader = Schema.String.pipe( - Schema.minLength(1), - Schema.maxLength(1024), - Schema.filter((s) => { + Schema.check(Schema.isMinLength(1)), + Schema.check(Schema.isMaxLength(1024)), + Schema.check(Schema.makeFilter((s) => { // No control characters return !s.match(/[\x00-\x1F\x7F]/g) - }).pipe( + })).pipe( Schema.annotations({ description: "Valid HTTP header value", }) @@ -95,7 +95,7 @@ const validateRequest = ( raw: Record ) => Effect.gen(function* () { - const headers = yield* Schema.decodeUnknown( + const headers = yield* Schema.decodeUnknownEffect( RequestHeaders )(raw).pipe( Effect.mapError((error) => ({ diff --git a/content/published/patterns/schema/web-standards-validation/iso-date.mdx b/content/published/patterns/schema/web-standards-validation/iso-date.mdx index 1bada7f3..f3b7cbe3 100644 --- a/content/published/patterns/schema/web-standards-validation/iso-date.mdx +++ b/content/published/patterns/schema/web-standards-validation/iso-date.mdx @@ -29,15 +29,15 @@ import { Schema, Effect } from "effect" // 1. ISO 8601 datetime with timezone const ISODateTime = Schema.String.pipe( - Schema.pattern( + Schema.check(Schema.isPattern( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z?$/ - ).pipe( + )).pipe( Schema.annotations({ description: "ISO 8601 datetime (YYYY-MM-DDTHH:mm:ss[.sss][Z])", }) ), - Schema.filter((s) => { + Schema.check(Schema.makeFilter((s) => { try { const date = new Date(s) // Check it parses to valid Date @@ -50,7 +50,7 @@ const ISODateTime = Schema.String.pipe( } catch { return false } - }).pipe( + })).pipe( Schema.annotations({ description: "Valid date between 1900-2100", }) @@ -62,15 +62,15 @@ type ISODateTime = typeof ISODateTime.Type // 2. ISO 8601 date only (no time) const ISODate = Schema.String.pipe( - Schema.pattern(/^\d{4}-\d{2}-\d{2}$/).pipe( + Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/)).pipe( Schema.annotations({ description: "ISO 8601 date (YYYY-MM-DD)", }) ), - Schema.filter((s) => { + Schema.check(Schema.makeFilter((s) => { const date = new Date(s + "T00:00:00Z") return !isNaN(date.getTime()) - }), + })), Schema.brand("ISODate") ) @@ -93,7 +93,7 @@ type Event = typeof Event.Type // 4. Validate and work with dates const createEvent = (input: unknown) => Effect.gen(function* () { - const event = yield* Schema.decodeUnknown(Event)( + const event = yield* Schema.decodeUnknownEffect(Event)( input ).pipe( Effect.mapError((error) => ({ diff --git a/content/published/patterns/schema/web-standards-validation/mime-types.mdx b/content/published/patterns/schema/web-standards-validation/mime-types.mdx index 9205be33..4941f842 100644 --- a/content/published/patterns/schema/web-standards-validation/mime-types.mdx +++ b/content/published/patterns/schema/web-standards-validation/mime-types.mdx @@ -28,47 +28,47 @@ Your application accepts file uploads or specifies content types. Users submit u import { Schema, Effect } from "effect" // 1. Define known MIME types as literals -const ImageMimeType = Schema.Literal( +const ImageMimeType = Schema.Literals([ "image/jpeg", "image/png", "image/webp", "image/gif" -) +]) type ImageMimeType = typeof ImageMimeType.Type -const DocumentMimeType = Schema.Literal( +const DocumentMimeType = Schema.Literals([ "application/pdf", "text/plain", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" -) +]) type DocumentMimeType = typeof DocumentMimeType.Type // 2. Create branded MIME types -const ValidMimeType = Schema.Union( +const ValidMimeType = Schema.Union([ ImageMimeType, DocumentMimeType, - Schema.Literal( + Schema.Literals([ "application/json", "text/html", "application/xml" - ) -).pipe(Schema.brand("ValidMimeType")) + ]) +]).pipe(Schema.brand("ValidMimeType")) type ValidMimeType = typeof ValidMimeType.Type // 3. File upload schema const FileUpload = Schema.Struct({ filename: Schema.String.pipe( - Schema.minLength(1), - Schema.maxLength(255) + Schema.check(Schema.isMinLength(1)), + Schema.check(Schema.isMaxLength(255)) ), mimeType: ValidMimeType, size: Schema.Number.pipe( - Schema.int(), - Schema.between(1, 10 * 1024 * 1024) // 10MB limit + Schema.check(Schema.isInt()), + Schema.check(Schema.isBetween(1, 10 * 1024 * 1024)) // 10MB limit ), }) @@ -99,7 +99,7 @@ const getMimeTypeFromExtension = ( // 5. File handler with validation const handleFileUpload = (input: unknown) => Effect.gen(function* () { - const file = yield* Schema.decodeUnknown( + const file = yield* Schema.decodeUnknownEffect( FileUpload )(input).pipe( Effect.mapError((error) => ({ @@ -136,7 +136,7 @@ const selectOutputFormat = ( // Validate accepted types const validated = yield* Effect.all( accepted.map((type) => - Schema.decodeUnknown(ValidMimeType)(type).pipe( + Schema.decodeUnknownEffect(ValidMimeType)(type).pipe( Effect.catchTag("ParseError", () => Effect.none() ) @@ -153,9 +153,9 @@ const selectOutputFormat = ( // 7. Media type with parameters const MediaTypeWithParams = Schema.String.pipe( - Schema.pattern( + Schema.check(Schema.isPattern( /^[\w\-]+\/[\w\.\+\-]+(\s*;\s*[\w\-]+\s*=\s*[\w\-]+)*$/ - ).pipe( + )).pipe( Schema.annotations({ description: "MIME type with optional parameters (e.g., text/html; charset=utf-8)", @@ -169,7 +169,7 @@ type MediaTypeWithParams = const parseMediaType = (raw: string) => Effect.gen(function* () { - const media = yield* Schema.decodeUnknown( + const media = yield* Schema.decodeUnknownEffect( MediaTypeWithParams )(raw).pipe( Effect.mapError((error) => ({ diff --git a/content/published/patterns/schema/web-standards-validation/url.mdx b/content/published/patterns/schema/web-standards-validation/url.mdx index 71273e1a..036b3bd5 100644 --- a/content/published/patterns/schema/web-standards-validation/url.mdx +++ b/content/published/patterns/schema/web-standards-validation/url.mdx @@ -30,7 +30,7 @@ import { Schema, Effect } from "effect" // 1. Define branded URL type const HttpUrl = Schema.String.pipe( Schema.trimmed(), - Schema.filter((s) => { + Schema.check(Schema.makeFilter((s) => { try { const url = new URL(s) // Only allow http/https @@ -40,7 +40,7 @@ const HttpUrl = Schema.String.pipe( } catch { return false } - }).pipe( + })).pipe( Schema.annotations({ description: "Valid HTTP/HTTPS URL", }) @@ -53,14 +53,14 @@ type HttpUrl = typeof HttpUrl.Type // 2. Alternative: Allow multiple protocols const WebUrl = Schema.String.pipe( Schema.trimmed(), - Schema.filter((s) => { + Schema.check(Schema.makeFilter((s) => { try { new URL(s) return true } catch { return false } - }).pipe( + })).pipe( Schema.annotations({ description: "Valid URL (any protocol)", }) @@ -75,10 +75,10 @@ const WebhookConfig = Schema.Struct({ name: Schema.String, url: HttpUrl, events: Schema.Array( - Schema.Literal("user.created", "user.deleted") + Schema.Literals(["user.created", "user.deleted"]) ), retryLimit: Schema.Number.pipe( - Schema.between(0, 10) + Schema.check(Schema.isBetween(0, 10)) ).pipe(Schema.withDefault(3)), }) @@ -87,7 +87,7 @@ type WebhookConfig = typeof WebhookConfig.Type // 4. Validate and extract URL parts const configureWebhook = (input: unknown) => Effect.gen(function* () { - const config = yield* Schema.decodeUnknown( + const config = yield* Schema.decodeUnknownEffect( WebhookConfig )(input).pipe( Effect.mapError((error) => ({ diff --git a/content/published/patterns/schema/web-standards-validation/uuid.mdx b/content/published/patterns/schema/web-standards-validation/uuid.mdx index 741221cd..f53394e4 100644 --- a/content/published/patterns/schema/web-standards-validation/uuid.mdx +++ b/content/published/patterns/schema/web-standards-validation/uuid.mdx @@ -29,9 +29,9 @@ import { Schema, Effect } from "effect" // 1. Define UUID v4 validator const UUIDv4 = Schema.String.pipe( - Schema.pattern( + Schema.check(Schema.isPattern( /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ).pipe( + )).pipe( Schema.annotations({ description: "Valid UUID v4 (36 chars with hyphens)", }) @@ -43,9 +43,9 @@ type UUIDv4 = typeof UUIDv4.Type // 2. Define UUID v7 validator (sortable) const UUIDv7 = Schema.String.pipe( - Schema.pattern( + Schema.check(Schema.isPattern( /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ).pipe( + )).pipe( Schema.annotations({ description: "Valid UUID v7 (time-based, sortable)", }) @@ -56,7 +56,7 @@ const UUIDv7 = Schema.String.pipe( type UUIDv7 = typeof UUIDv7.Type // 3. Union of both versions -const UUID = Schema.Union(UUIDv4, UUIDv7).pipe( +const UUID = Schema.Union([UUIDv4, UUIDv7]).pipe( Schema.brand("UUID") ) @@ -75,7 +75,7 @@ type UserProfile = typeof UserProfile.Type // 5. Fetch and validate resource const getUserProfile = (userId: unknown) => Effect.gen(function* () { - const id = yield* Schema.decodeUnknown(UUIDv4)(userId).pipe( + const id = yield* Schema.decodeUnknownEffect(UUIDv4)(userId).pipe( Effect.mapError((error) => ({ _tag: "InvalidId" as const, message: `Invalid user ID: ${error.message}`, @@ -104,7 +104,7 @@ const createResourceId = () => const v4 = crypto.randomUUID() as string // Validate it matches schema - const validated = yield* Schema.decodeUnknown(UUIDv4)( + const validated = yield* Schema.decodeUnknownEffect(UUIDv4)( v4 ).pipe( Effect.catchTag("ParseError", (error) => diff --git a/content/published/patterns/streams/sinks/sink-pattern-fall-back-to-alternative-sink-on-failure.mdx b/content/published/patterns/streams/sinks/sink-pattern-fall-back-to-alternative-sink-on-failure.mdx index 0818048e..944efdfc 100644 --- a/content/published/patterns/streams/sinks/sink-pattern-fall-back-to-alternative-sink-on-failure.mdx +++ b/content/published/patterns/streams/sinks/sink-pattern-fall-back-to-alternative-sink-on-failure.mdx @@ -61,7 +61,7 @@ With fallback sinks: This example demonstrates a system that tries to write order records to a fast in-memory cache first, falls back to database if cache fails, and falls back to a dead letter file if database fails. ```typescript -import { Effect, Stream, Sink, Chunk, Either, Data } from "effect"; +import { Effect, Stream, Sink, Chunk, Result, Data } from "effect"; interface Order { readonly orderId: string; @@ -173,33 +173,33 @@ const createFallbackSink = (): Sink.Sink< // Try cache first const cacheResult = yield* createCacheSink() .pipe(Sink.feed(Chunk.of(order))) - .pipe(Effect.either); + .pipe(Effect.result); - if (Either.isRight(cacheResult)) { + if (Result.isSuccess(cacheResult)) { return { ...state, - cached: state.cached + cacheResult.right, + cached: state.cached + cacheResult.success, }; } console.log( - `[FALLBACK] Cache failed (${cacheResult.left.reason}), trying database` + `[FALLBACK] Cache failed (${cacheResult.failure.reason}), trying database` ); // Cache failed, try database const dbResult = yield* createDatabaseSink() .pipe(Sink.feed(Chunk.of(order))) - .pipe(Effect.either); + .pipe(Effect.result); - if (Either.isRight(dbResult)) { + if (Result.isSuccess(dbResult)) { return { ...state, - persisted: state.persisted + dbResult.right, + persisted: state.persisted + dbResult.success, }; } console.log( - `[FALLBACK] Database failed (${dbResult.left.reason}), falling back to dead letter` + `[FALLBACK] Database failed (${dbResult.failure.reason}), falling back to dead letter` ); // Database failed, use dead letter @@ -311,21 +311,21 @@ const createIntelligentFallbackSink = (): Sink.Sink< // Try cache const cacheAttempt = yield* createCacheSink() .pipe(Sink.feed(Chunk.of(order))) - .pipe(Effect.either); + .pipe(Effect.result); - if (Either.isRight(cacheAttempt)) { + if (Result.isSuccess(cacheAttempt)) { return { ...state, cached: state.cached + 1 }; } - const strategy = determineFallback(cacheAttempt.left); + const strategy = determineFallback(cacheAttempt.failure); if (strategy === "retry") { // Try database const dbAttempt = yield* createDatabaseSink() .pipe(Sink.feed(Chunk.of(order))) - .pipe(Effect.either); + .pipe(Effect.result); - if (Either.isRight(dbAttempt)) { + if (Result.isSuccess(dbAttempt)) { return { ...state, persisted: state.persisted + 1 }; } } @@ -387,9 +387,9 @@ const createResilientFallbackSink = ( ), }) ) - .pipe(Effect.either); + .pipe(Effect.result); - if (Either.isRight(primaryAttempt)) { + if (Result.isSuccess(primaryAttempt)) { return { ...state, primary: state.primary + 1 }; } @@ -399,7 +399,7 @@ const createResilientFallbackSink = ( const fallbackResult = yield* createDatabaseSink() .pipe(Sink.feed(Chunk.of(order))) .pipe( - Effect.catchAll(() => + Effect.catch(() => createDeadLetterSink().pipe(Sink.feed(Chunk.of(order))) ) ); diff --git a/content/published/patterns/streams/sinks/sink-pattern-retry-failed-stream-operations.mdx b/content/published/patterns/streams/sinks/sink-pattern-retry-failed-stream-operations.mdx index 445bb132..a45f93d8 100644 --- a/content/published/patterns/streams/sinks/sink-pattern-retry-failed-stream-operations.mdx +++ b/content/published/patterns/streams/sinks/sink-pattern-retry-failed-stream-operations.mdx @@ -305,20 +305,19 @@ const createEffectRetrySink = ( const result = yield* database .insertUser(user) .pipe( - // Retry with exponential backoff + // Retry with exponential backoff, up to maxRetries attempts Effect.retry( - Schedule.exponential("100 millis").pipe( - Schedule.addDelay((attempt) => - Duration.millis(Math.random() * 10) // Add jitter + Schedule.max([ + Schedule.exponential("100 millis").pipe( + Schedule.addDelay(() => + Duration.millis(Math.random() * 10) // Add jitter + ) ), - Schedule.compose( - Schedule.recurs(maxRetries), - Schedule.resetAfter(Duration.seconds(30)) // Reset backoff after 30s - ) - ) + Schedule.recurs(maxRetries) + ]) ), // Handle permanent failures - Effect.catchAll((error: WriteError) => + Effect.catch((error: WriteError) => Effect.gen(function* () { if (!error.isTransient) { return "permanent-failed" as const; diff --git a/content/published/patterns/streams/sinks/sink-pattern-send-stream-records-to-message-queue.mdx b/content/published/patterns/streams/sinks/sink-pattern-send-stream-records-to-message-queue.mdx index b2b241e1..14afcd7d 100644 --- a/content/published/patterns/streams/sinks/sink-pattern-send-stream-records-to-message-queue.mdx +++ b/content/published/patterns/streams/sinks/sink-pattern-send-stream-records-to-message-queue.mdx @@ -344,7 +344,7 @@ const createResilientPublishSink = ( delay: () => Effect.sleep(`${config.backoffMs} millis`), }), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { console.error( `Failed to publish batch to ${partition}: ${error}` diff --git a/content/published/patterns/streams/sinks/sink-pattern-write-stream-lines-to-file.mdx b/content/published/patterns/streams/sinks/sink-pattern-write-stream-lines-to-file.mdx index 2ca8daef..0c59dd93 100644 --- a/content/published/patterns/streams/sinks/sink-pattern-write-stream-lines-to-file.mdx +++ b/content/published/patterns/streams/sinks/sink-pattern-write-stream-lines-to-file.mdx @@ -271,7 +271,7 @@ const compressRotatedFile = (filePath: string): Effect.Effect => Effect.try(() => { execSync(`gzip -f ${filePath}`); }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.log(`Failed to compress ${filePath}: ${error}`) ) ); diff --git a/content/published/patterns/streams/stream-pattern-advanced-transformations.mdx b/content/published/patterns/streams/stream-pattern-advanced-transformations.mdx index 68a78ff7..4e8c4a70 100644 --- a/content/published/patterns/streams/stream-pattern-advanced-transformations.mdx +++ b/content/published/patterns/streams/stream-pattern-advanced-transformations.mdx @@ -282,7 +282,7 @@ const program = Effect.gen(function* () { ); } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[PARSE ERROR] ${error.message}`); diff --git a/content/published/patterns/streams/stream-pattern-backpressure-control.mdx b/content/published/patterns/streams/stream-pattern-backpressure-control.mdx index bd933949..445e6b6f 100644 --- a/content/published/patterns/streams/stream-pattern-backpressure-control.mdx +++ b/content/published/patterns/streams/stream-pattern-backpressure-control.mdx @@ -223,7 +223,7 @@ const adaptiveBuffer = ( } }); - yield* Effect.fork(monitor); + yield* Effect.forkChild(monitor); yield* adaptiveStream; }); ``` diff --git a/content/published/patterns/streams/stream-pattern-error-handling.mdx b/content/published/patterns/streams/stream-pattern-error-handling.mdx index 284c701c..fa5b8725 100644 --- a/content/published/patterns/streams/stream-pattern-error-handling.mdx +++ b/content/published/patterns/streams/stream-pattern-error-handling.mdx @@ -36,7 +36,7 @@ Stream error handling enables resilience: - **Terminate gracefully**: Controlled shutdown - **Propagate**: Let errors flow upstream -Pattern: `Stream.catchAll()`, `Stream.retry()`, `Stream.recover()`, `Stream.runCollect()` +Pattern: `Stream.catch()`, `Stream.retry()`, `Stream.recover()`, `Stream.runCollect()` --- @@ -143,7 +143,7 @@ const program = Effect.gen(function* () { Stream.mapEffect((record) => processElement(record).pipe( Effect.map((result) => ({ success: true, result })), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[ERROR] Record ${record.id} failed`); @@ -168,7 +168,7 @@ const program = Effect.gen(function* () { const recovered = yield* Stream.fromIterable(["ok1", "fail1", "ok2"]).pipe( Stream.mapEffect((id) => getData(id).pipe( - Effect.catchAll(() => + Effect.catch(() => Effect.gen(function* () { yield* Effect.log(`[FALLBACK] Using default for ${id}`); @@ -203,7 +203,7 @@ const program = Effect.gen(function* () { }, ]) ), - Effect.catchAll((error) => + Effect.catch((error) => Ref.modify(results, (r) => [ undefined, { @@ -253,11 +253,11 @@ const program = Effect.gen(function* () { const retried = unreliableOperation("test").pipe( Effect.retry( Schedule.exponential("10 millis").pipe( - Schedule.upTo("100 millis"), + Schedule.upTo({ duration: "100 millis" }), Schedule.recurs(3) ) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[EXHAUSTED] All retries failed`); @@ -291,7 +291,7 @@ const program = Effect.gen(function* () { return value * 2; }) ), - Stream.catchAll((error) => + Stream.catch((error) => Effect.gen(function* () { yield* Effect.log(`[CONTEXT ERROR] ${error.message}`); @@ -326,7 +326,7 @@ const program = Effect.gen(function* () { const partialResults = yield* Stream.fromIterable(mixedQuality).pipe( Stream.mapEffect((record) => processQuality(record).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[LOG] ${error.message}`); @@ -362,7 +362,7 @@ const program = Effect.gen(function* () { Stream.mapEffect((id) => slowOperation(id).pipe( Effect.timeout("100 millis"), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[TIMEOUT] Operation ${id} timed out`); @@ -388,7 +388,7 @@ const program = Effect.gen(function* () { ? Effect.fail(new Error("CRITICAL: System failure")) : Effect.succeed(value) ), - Stream.catchAll((error) => + Stream.catch((error) => Effect.gen(function* () { if (isCritical(error)) { yield* Effect.log(`[CRITICAL] Terminating stream`); @@ -405,7 +405,7 @@ const program = Effect.gen(function* () { yield* terminateOnCritical.pipe( Stream.runCollect, - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[STOPPED] Stream stopped: ${error.message}`); @@ -437,7 +437,7 @@ const applyRecoveryStrategies = ( strategies: RecoveryStrategy[] ) => stream.pipe( - Stream.catchAll((error) => + Stream.catch((error) => Effect.gen(function* () { const sorted = strategies.sort((a, b) => b.priority - a.priority @@ -446,7 +446,7 @@ const applyRecoveryStrategies = ( for (const strategy of sorted) { if (strategy.canRecover(error)) { const recovered = yield* strategy.recover(error, null).pipe( - Effect.catchAll(() => Effect.fail(error)) + Effect.catch(() => Effect.fail(error)) ); return Stream.succeed(recovered); diff --git a/content/published/patterns/streams/stream-pattern-merge-combine.mdx b/content/published/patterns/streams/stream-pattern-merge-combine.mdx index 29b14a12..3ec677fc 100644 --- a/content/published/patterns/streams/stream-pattern-merge-combine.mdx +++ b/content/published/patterns/streams/stream-pattern-merge-combine.mdx @@ -269,7 +269,7 @@ const mergeWithErrorHandling = ( streams.map((stream) => stream.pipe( Stream.map((value) => ({ tag: "success" as const, value })), - Stream.catchAll((error) => + Stream.catch((error) => Stream.succeed({ tag: "error" as const, error: error as Error, @@ -318,7 +318,7 @@ const roundRobinMerge = ( // Consume from queues in round-robin fashion return Stream.fromEffect( Effect.gen(function* () { - yield* Effect.all(forwarders.map((f) => Effect.fork(f))); + yield* Effect.all(forwarders.map((f) => Effect.forkChild(f))); let currentQueue = 0; diff --git a/content/published/patterns/streams/stream-pattern-resource-management.mdx b/content/published/patterns/streams/stream-pattern-resource-management.mdx index 64061edc..4831cb36 100644 --- a/content/published/patterns/streams/stream-pattern-resource-management.mdx +++ b/content/published/patterns/streams/stream-pattern-resource-management.mdx @@ -171,7 +171,7 @@ const program = Effect.gen(function* () { } } }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[ERROR] Caught: ${error.message}`); yield* Effect.log(`[CHECK] Closed handles: ${closedHandles} (verifying cleanup)\n`); @@ -295,7 +295,7 @@ const program = Effect.gen(function* () { return { level: 1, data: innerData }; }) ).pipe( - Effect.catchAll(() => Effect.succeed({ level: 0, data: null })) + Effect.catch(() => Effect.succeed({ level: 0, data: null })) ); yield* Effect.log(`[SCOPES] Cleanup order: inner β†’ outer\n`); @@ -348,12 +348,9 @@ const program = Effect.gen(function* () { // Retry with guaranteed cleanup const result2 = yield* safeRead(1).pipe( - Effect.retry( - Schedule.recurs(2).pipe( - Schedule.compose(Schedule.fixed("10 millis")) - ) + Effect.retry(Schedule.max([Schedule.recurs(2), Schedule.fixed("10 millis")]) ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log(`[FINAL] All retries failed: ${error.message}`); return "fallback"; @@ -499,7 +496,7 @@ const createGracefulStream = ( yield* config.shutdown.pipe( Effect.timeout(config.timeout), - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { yield* Effect.log( `[STREAM] Shutdown timeout or failed: ${error.message}` diff --git a/content/published/patterns/testing/mocking-dependencies-in-tests.mdx b/content/published/patterns/testing/mocking-dependencies-in-tests.mdx index 181f0247..20e8fa26 100644 --- a/content/published/patterns/testing/mocking-dependencies-in-tests.mdx +++ b/content/published/patterns/testing/mocking-dependencies-in-tests.mdx @@ -49,34 +49,39 @@ By providing a mock `Layer` in your test, you replace a real dependency (like an We want to test a `Notifier` service that uses an `EmailClient` to send emails. In our test, we provide a mock `EmailClient` that doesn't actually send emails but just returns a success value. ```typescript -import { Effect, Layer } from "effect"; +import { Effect, Layer, Context } from "effect"; // --- The Services --- interface EmailClientService { send: (address: string, body: string) => Effect.Effect; } -class EmailClient extends Effect.Service()("EmailClient", { - sync: () => ({ +class EmailClient extends Context.Service()("EmailClient", { + make: Effect.sync(() => ({ send: (address: string, body: string) => Effect.sync(() => Effect.log(`Sending email to ${address}: ${body}`)), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} interface NotifierService { notifyUser: (userId: number, message: string) => Effect.Effect; } -class Notifier extends Effect.Service()("Notifier", { - effect: Effect.gen(function* () { +class Notifier extends Context.Service()("Notifier", { + make: Effect.gen(function* () { const emailClient = yield* EmailClient; return { notifyUser: (userId: number, message: string) => emailClient.send(`user-${userId}@example.com`, message), }; }), - dependencies: [EmailClient.Default], -}) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(EmailClient.layer) + ) +} // Create a program that uses the Notifier service const program = Effect.gen(function* () { @@ -100,7 +105,7 @@ const program = Effect.gen(function* () { }); // Run the program -Effect.runPromise(Effect.provide(program, Notifier.Default)); +Effect.runPromise(Effect.provide(program, Notifier.layer)); ``` --- diff --git a/content/published/patterns/testing/organize-layers-into-composable-modules.mdx b/content/published/patterns/testing/organize-layers-into-composable-modules.mdx index 21c525af..c9387ca1 100644 --- a/content/published/patterns/testing/organize-layers-into-composable-modules.mdx +++ b/content/published/patterns/testing/organize-layers-into-composable-modules.mdx @@ -50,20 +50,20 @@ This example shows a `BaseLayer` with a `Logger`, a `UserModule` that uses the ` ```typescript // src/core/Logger.ts -import { Effect } from "effect"; +import { Effect, Context } from "effect"; -export class Logger extends Effect.Service()("App/Core/Logger", { - sync: () => ({ +export class Logger extends Context.Service()("App/Core/Logger", { + make: Effect.sync(() => ({ log: (msg: string) => Effect.log(`[LOG] ${msg}`), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // src/features/User/UserRepository.ts -export class UserRepository extends Effect.Service()( - "App/User/UserRepository", - { +export class UserRepository extends Context.Service()("App/User/UserRepository", { // Define implementation that uses Logger - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const logger = yield* Logger; return { findById: (id: number) => @@ -74,9 +74,11 @@ export class UserRepository extends Effect.Service()( }; }), // Declare Logger dependency - dependencies: [Logger.Default], - } -) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(Logger.layer) + ) +} // Example usage const program = Effect.gen(function* () { @@ -86,7 +88,7 @@ const program = Effect.gen(function* () { }); // Run with default implementations -Effect.runPromise(Effect.provide(program, UserRepository.Default)); +Effect.runPromise(Effect.provide(program, UserRepository.layer)); const programWithLogging = Effect.gen(function* () { const result = yield* program; @@ -94,7 +96,7 @@ const programWithLogging = Effect.gen(function* () { return result; }); -Effect.runPromise(Effect.provide(programWithLogging, UserRepository.Default)); +Effect.runPromise(Effect.provide(programWithLogging, UserRepository.layer)); ``` ### 2. The Feature Module Layer @@ -103,18 +105,18 @@ Effect.runPromise(Effect.provide(programWithLogging, UserRepository.Default)); // src/core/Logger.ts import { Effect } from "effect"; -export class Logger extends Effect.Service()("App/Core/Logger", { - sync: () => ({ +export class Logger extends Context.Service()("App/Core/Logger", { + make: Effect.sync(() => ({ log: (msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // src/features/User/UserRepository.ts -export class UserRepository extends Effect.Service()( - "App/User/UserRepository", - { +export class UserRepository extends Context.Service()("App/User/UserRepository", { // Define implementation that uses Logger - effect: Effect.gen(function* () { + make: Effect.gen(function* () { const logger = yield* Logger; return { findById: (id: number) => @@ -125,9 +127,11 @@ export class UserRepository extends Effect.Service()( }; }), // Declare Logger dependency - dependencies: [Logger.Default], - } -) {} +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(Logger.layer) + ) +} // Example usage const program = Effect.gen(function* () { @@ -137,7 +141,7 @@ const program = Effect.gen(function* () { }); // Run with default implementations -Effect.runPromise(Effect.provide(program, UserRepository.Default)).then( +Effect.runPromise(Effect.provide(program, UserRepository.layer)).then( console.log ); ``` diff --git a/content/published/patterns/testing/testing-concurrent-code.mdx b/content/published/patterns/testing/testing-concurrent-code.mdx index ed096e28..537afaad 100644 --- a/content/published/patterns/testing/testing-concurrent-code.mdx +++ b/content/published/patterns/testing/testing-concurrent-code.mdx @@ -70,7 +70,7 @@ describe("Concurrent Code Testing", () => { // Use TestClock to control time const result = await Effect.runPromise( Effect.gen(function* () { - const fiber = yield* Effect.fork(program) + const fiber = yield* Effect.forkChild(program) // Advance time to trigger both tasks yield* TestClock.adjust("100 millis") @@ -119,7 +119,7 @@ describe("Concurrent Code Testing", () => { const events: string[] = [] const program = Effect.gen(function* () { - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( Effect.gen(function* () { events.push("started") yield* Effect.sleep("1 second") @@ -156,7 +156,7 @@ describe("Concurrent Code Testing", () => { const result = await Effect.runPromise( Effect.gen(function* () { - const fiber = yield* Effect.fork( + const fiber = yield* Effect.forkChild( slowOperation.pipe(Effect.timeout("1 second")) ) @@ -182,7 +182,7 @@ describe("Concurrent Code Testing", () => { const results: string[] = [] // Consumer waits for producer - const consumer = Effect.fork( + const consumer = Effect.forkChild( Effect.gen(function* () { const value = yield* Deferred.await(deferred) results.push(`consumed: ${value}`) @@ -222,14 +222,14 @@ describe("Concurrent Code Testing", () => { const ref2 = yield* Ref.make(0) // Two fibers accessing refs in same order (no deadlock) - const fiber1 = yield* Effect.fork( + const fiber1 = yield* Effect.forkChild( Effect.gen(function* () { yield* Ref.update(ref1, (n) => n + 1) yield* Ref.update(ref2, (n) => n + 1) }) ) - const fiber2 = yield* Effect.fork( + const fiber2 = yield* Effect.forkChild( Effect.gen(function* () { yield* Ref.update(ref1, (n) => n + 1) yield* Ref.update(ref2, (n) => n + 1) diff --git a/content/published/patterns/testing/testing-property-based.mdx b/content/published/patterns/testing/testing-property-based.mdx index 0e1ec3a8..9772214a 100644 --- a/content/published/patterns/testing/testing-property-based.mdx +++ b/content/published/patterns/testing/testing-property-based.mdx @@ -41,7 +41,7 @@ Property-based testing finds bugs that example tests miss: ```typescript import { describe, it, expect } from "vitest" -import { Effect, Option, Either, Schema } from "effect" +import { Effect, Option, Result, Schema } from "effect" import * as fc from "fast-check" describe("Property-Based Testing with Effect", () => { @@ -127,7 +127,7 @@ describe("Property-Based Testing with Effect", () => { it("should roundtrip through Schema", async () => { const UserSchema = Schema.Struct({ name: Schema.String, - age: Schema.Number.pipe(Schema.int(), Schema.positive()), + age: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0))), }) const userArbitrary = fc.record({ @@ -137,8 +137,8 @@ describe("Property-Based Testing with Effect", () => { await fc.assert( fc.asyncProperty(userArbitrary, async (user) => { - const encode = Schema.encode(UserSchema) - const decode = Schema.decode(UserSchema) + const encode = Schema.encodeEffect(UserSchema) + const decode = Schema.decodeEffect(UserSchema) // Encode then decode should return equivalent value const encoded = await Effect.runPromise(encode(user)) @@ -163,7 +163,7 @@ describe("Property-Based Testing with Effect", () => { const result = await Effect.runPromise( failing.pipe( - Effect.catchAll(() => Effect.succeed(fallback)) + Effect.catch(() => Effect.succeed(fallback)) ) ) diff --git a/content/published/patterns/testing/testing-streams.mdx b/content/published/patterns/testing/testing-streams.mdx index 8544cd9c..c146f97b 100644 --- a/content/published/patterns/testing/testing-streams.mdx +++ b/content/published/patterns/testing/testing-streams.mdx @@ -103,7 +103,7 @@ describe("Stream Testing", () => { ? Effect.fail(new Error("Failed on 2")) : Effect.succeed(n * 10) ), - Stream.catchAll((error) => + Stream.catch((error) => Stream.succeed(-1) // Replace error with sentinel ), Stream.runCollect @@ -205,7 +205,7 @@ describe("Stream Testing", () => { await Effect.runPromise( Stream.runDrain(managedStream).pipe( - Effect.catchAll(() => Effect.void) + Effect.catch(() => Effect.void) ) ) @@ -266,7 +266,7 @@ describe("Stream Testing", () => { |---------|-------------| | Transformations | runCollect + compare array | | Aggregation | runFold, runCount | -| Errors | catchAll + verify recovery | +| Errors | catch + verify recovery | | Resources | Track acquire/release calls | | Side effects | Use tap + external tracking | diff --git a/content/published/patterns/testing/testing-with-services.mdx b/content/published/patterns/testing/testing-with-services.mdx index 2c27f6a9..de5b3e89 100644 --- a/content/published/patterns/testing/testing-with-services.mdx +++ b/content/published/patterns/testing/testing-with-services.mdx @@ -47,13 +47,12 @@ import { Effect, Context } from "effect" // 1. Define a service // ============================================ -class UserRepository extends Context.Tag("UserRepository")< - UserRepository, - { +class UserRepository extends Context.Service< + UserRepository, { readonly findById: (id: string) => Effect.Effect readonly save: (user: User) => Effect.Effect } ->() {} +>()("UserRepository") {} interface User { id: string diff --git a/content/published/patterns/testing/use-default-layer-for-tests.mdx b/content/published/patterns/testing/use-default-layer-for-tests.mdx index 4d820e5f..370a8dc2 100644 --- a/content/published/patterns/testing/use-default-layer-for-tests.mdx +++ b/content/published/patterns/testing/use-default-layer-for-tests.mdx @@ -1,18 +1,18 @@ --- -title: Use the Auto-Generated .Default Layer in Tests +title: Use the .layer Layer in Tests id: use-default-layer-for-tests skillLevel: intermediate applicationPatternId: testing summary: >- - When testing, always use the MyService.Default layer that is automatically - generated by the Effect.Service class for dependency injection. + When testing, always use the MyService.layer layer that you define + alongside your Context.Service class for dependency injection. tags: - testing - service - layers - dependency-injection rule: - description: Use the auto-generated .Default layer in tests. + description: Use the .layer layer in tests. related: - define-service-with-effect-service - test-the-code-as-is @@ -20,30 +20,32 @@ author: Paul Philp lessonOrder: 3 --- -# Use the Auto-Generated .Default Layer in Tests +# Use the .layer Layer in Tests ## Guideline -In your tests, provide service dependencies using the static `.Default` property that `Effect.Service` automatically attaches to your service class. +In your tests, provide service dependencies using the static `.layer` property that you define on your service class with `Layer.effect`. ## Rationale -The `.Default` layer is the canonical way to provide a service in a test environment. It's automatically created, correctly scoped, and handles resolving any transitive dependencies, making tests cleaner and more robust. +The `.layer` layer is the canonical way to provide a service in a test environment. You define it explicitly with `Layer.effect(this, this.make)` (wiring dependencies with `Layer.provide`), so it is correctly scoped and resolves transitive dependencies, making tests cleaner and more robust. ## Good Example ```typescript -import { Effect } from "effect"; +import { Effect, Context, Layer } from "effect"; -// Define MyService using Effect.Service pattern -class MyService extends Effect.Service()("MyService", { - sync: () => ({ +// Define MyService using Context.Service pattern +class MyService extends Context.Service()("MyService", { + make: Effect.sync(() => ({ doSomething: () => Effect.succeed("done").pipe( Effect.tap(() => Effect.log("MyService did something!")) ), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} // Create a program that uses MyService const program = Effect.gen(function* () { @@ -57,7 +59,7 @@ const program = Effect.gen(function* () { }); // Run the program with default service implementation -Effect.runPromise(Effect.provide(program, MyService.Default)); +Effect.runPromise(Effect.provide(program, MyService.layer)); ``` **Explanation:** diff --git a/content/published/patterns/testing/write-tests-that-adapt-to-application-code.mdx b/content/published/patterns/testing/write-tests-that-adapt-to-application-code.mdx index 8af441d2..558f008c 100644 --- a/content/published/patterns/testing/write-tests-that-adapt-to-application-code.mdx +++ b/content/published/patterns/testing/write-tests-that-adapt-to-application-code.mdx @@ -33,7 +33,7 @@ Treating application code as immutable during testing prevents the introduction ## Good Example ```typescript -import { Effect } from "effect"; +import { Effect, Context, Layer } from "effect"; // Define our types interface User { @@ -54,10 +54,8 @@ interface DatabaseServiceApi { } // Implement the service with mock data -class DatabaseService extends Effect.Service()( - "DatabaseService", - { - sync: () => ({ +class DatabaseService extends Context.Service()("DatabaseService", { + make: Effect.sync(() => ({ getUserById: (id: number) => { // Simulate database lookup if (id === 404) { @@ -65,15 +63,14 @@ class DatabaseService extends Effect.Service()( } return Effect.succeed({ id, name: `User ${id}` }); }, - }), - } -) {} + })), + }) { + static readonly layer = Layer.effect(this, this.make) +} // Test service implementation for testing -class TestDatabaseService extends Effect.Service()( - "TestDatabaseService", - { - sync: () => ({ +class TestDatabaseService extends Context.Service()("TestDatabaseService", { + make: Effect.sync(() => ({ getUserById: (id: number) => { // Test data with predictable responses const testUsers = [ @@ -88,9 +85,10 @@ class TestDatabaseService extends Effect.Service()( } return Effect.fail(new NotFoundError(id)); }, - }), - } -) {} + })), + }) { + static readonly layer = Layer.effect(this, this.make) +} // Business logic that uses the database service const getUserWithFallback = (id: number) => @@ -100,7 +98,7 @@ const getUserWithFallback = (id: number) => const user = yield* db.getUserById(id); return user; }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.gen(function* () { if (error instanceof NotFoundError) { yield* Effect.logInfo(`User ${id} not found, using fallback`); @@ -193,7 +191,7 @@ const program = Effect.gen(function* () { }); yield* Effect.logInfo(`Test result: ${JSON.stringify(testUser404)}`); }), - TestDatabaseService.Default + TestDatabaseService.layer ); yield* Effect.logInfo( @@ -206,7 +204,7 @@ const program = Effect.gen(function* () { // Run the program with the default database service Effect.runPromise( - Effect.provide(program, DatabaseService.Default) as Effect.Effect< + Effect.provide(program, DatabaseService.layer) as Effect.Effect< void, never, never diff --git a/content/published/patterns/tooling-and-debugging/supercharge-your-editor-with-the-effect-lsp.mdx b/content/published/patterns/tooling-and-debugging/supercharge-your-editor-with-the-effect-lsp.mdx index 31282742..2007ea89 100644 --- a/content/published/patterns/tooling-and-debugging/supercharge-your-editor-with-the-effect-lsp.mdx +++ b/content/published/patterns/tooling-and-debugging/supercharge-your-editor-with-the-effect-lsp.mdx @@ -48,19 +48,21 @@ This tool essentially makes the compiler's knowledge visible at a glance, reduci Imagine you have the following code. Without the LSP, hovering over `program` might show a complex, hard-to-read inferred type. ```typescript -import { Effect } from "effect"; +import { Effect, Context, Layer } from "effect"; -// Define Logger service using Effect.Service pattern -class Logger extends Effect.Service()("Logger", { - sync: () => ({ +// Define Logger service using Context.Service pattern +class Logger extends Context.Service()("Logger", { + make: Effect.sync(() => ({ log: (msg: string) => Effect.log(`LOG: ${msg}`), - }), -}) {} + })), +}) { + static readonly layer = Layer.effect(this, this.make) +} const program = Effect.succeed(42).pipe( Effect.map((n) => n.toString()), Effect.flatMap((s) => Effect.log(s)), - Effect.provide(Logger.Default) + Effect.provide(Logger.layer) ); // Run the program diff --git a/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx b/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx index 88f6b42a..13806705 100644 --- a/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx +++ b/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx @@ -68,8 +68,8 @@ const runWithDebug = debugProgram.pipe( const inspectFibers = Effect.gen(function* () { // Fork some fibers - const fiber1 = yield* Effect.fork(Effect.sleep("1 second")) - const fiber2 = yield* Effect.fork(Effect.sleep("2 seconds")) + const fiber1 = yield* Effect.forkChild(Effect.sleep("1 second")) + const fiber2 = yield* Effect.forkChild(Effect.sleep("2 seconds")) // Get fiber IDs yield* Effect.log(`Fiber 1 ID: ${fiber1.id()}`) @@ -117,12 +117,12 @@ const debugErrors = Effect.gen(function* () { ) yield* failingEffect.pipe( - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => Effect.gen(function* () { yield* Effect.log("=== Error Cause Analysis ===") yield* Effect.log(`Pretty printed:\n${Cause.pretty(cause)}`) - yield* Effect.log(`Is failure: ${Cause.isFailure(cause)}`) - yield* Effect.log(`Is interrupted: ${Cause.isInterrupted(cause)}`) + yield* Effect.log(`Is failure: ${Cause.hasFails(cause)}`) + yield* Effect.log(`Is interrupted: ${Cause.hasInterrupts(cause)}`) // Extract all failures const failures = Cause.failures(cause) @@ -140,7 +140,7 @@ const debugErrors = Effect.gen(function* () { import { Context } from "effect" -class Config extends Context.Tag("Config")() {} +class Config extends Context.Service()("Config") {} const inspectContext = Effect.gen(function* () { const context = yield* Effect.context() diff --git a/content/published/patterns/tooling-and-debugging/tooling-profiling.mdx b/content/published/patterns/tooling-and-debugging/tooling-profiling.mdx index 7cd71ce0..82005d35 100644 --- a/content/published/patterns/tooling-and-debugging/tooling-profiling.mdx +++ b/content/published/patterns/tooling-and-debugging/tooling-profiling.mdx @@ -168,7 +168,7 @@ const withCpuProfile = ( const result = yield* effect // Stop and save profile - yield* Effect.async((resume) => { + yield* Effect.callback((resume) => { inspector.post("Profiler.stop", (err: Error, { profile }: any) => { if (err) { resume(Effect.fail(err)) diff --git a/content/published/patterns/tooling-and-debugging/tooling-type-errors.mdx b/content/published/patterns/tooling-and-debugging/tooling-type-errors.mdx index 1eaaf1db..3d2f83eb 100644 --- a/content/published/patterns/tooling-and-debugging/tooling-type-errors.mdx +++ b/content/published/patterns/tooling-and-debugging/tooling-type-errors.mdx @@ -89,7 +89,7 @@ Type 'Effect' is not assignable to type 'Effect Effect.succeed(defaultUser)) + Effect.catch(() => Effect.succeed(defaultUser)) ) ) // βœ… } @@ -142,7 +142,7 @@ Type 'Effect' is not assignable to parameter of type 'Effe const program = Effect.gen(function* () { const a = yield* Effect.succeed(1) const b = yield* mayFail().pipe( - Effect.catchAll(() => Effect.succeed(0)) + Effect.catch(() => Effect.succeed(0)) ) return a + b // βœ… }) diff --git a/package.json b/package.json index 2c31e86c..dcd7418c 100644 --- a/package.json +++ b/package.json @@ -103,10 +103,7 @@ "lifecycle-harness": "bun run scripts/lifecycle-harness/src/index.ts" }, "dependencies": { - "@effect/cli": "0.73.2", - "@effect/platform": "0.94.5", - "@effect/platform-node": "0.104.1", - "@effect/rpc": "^0.73.1", + "@effect/platform-node": "4.0.0-rc.117", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.213.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.213.0", @@ -121,7 +118,7 @@ "conventional-recommended-bump": "^11.2.0", "dotenv": "^17.3.1", "drizzle-orm": "^0.45.1", - "effect": "3.19.19", + "effect": "4.0.0-rc.117", "effect-mdx": "^0.2.2", "glob": "^13.0.3", "gray-matter": "^4.0.3", @@ -138,8 +135,7 @@ "@ai-sdk/openai": "^3.0.29", "@biomejs/biome": "^2.4.0", "@effect/language-service": "^0.80.0", - "@effect/opentelemetry": "^0.61.0", - "@effect/schema": "^0.75.5", + "@effect/opentelemetry": "4.0.0-rc.117", "@modelcontextprotocol/sdk": "^1.26.0", "@opentelemetry/sdk-trace-base": "^2.5.1", "@types/bun": "^1.3.9", @@ -174,8 +170,7 @@ "author": "Paul", "license": "MIT", "resolutions": { - "@effect/platform": "0.94.5", - "@effect/platform-node": "0.104.1", - "effect": "3.19.19" + "@effect/platform-node": "4.0.0-rc.117", + "effect": "4.0.0-rc.117" } } diff --git a/packages/analysis-core/package.json b/packages/analysis-core/package.json index 1db8c9d0..c1c0740b 100644 --- a/packages/analysis-core/package.json +++ b/packages/analysis-core/package.json @@ -28,7 +28,7 @@ "test:all": "bun scripts/test-integrated.ts all --coverage" }, "dependencies": { - "effect": "3.19.19", + "effect": "4.0.0-rc.117", "typescript": "^5.5.0" } } \ No newline at end of file diff --git a/packages/api-server/package.json b/packages/api-server/package.json index 5eeff2d2..9b8ec216 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -47,10 +47,8 @@ "dependencies": { "@effect-patterns/analysis-core": "workspace:*", "@effect-patterns/toolkit": "workspace:*", - "@effect/opentelemetry": "^0.61.0", - "@effect/platform": "0.94.5", - "@effect/platform-node": "0.104.1", - "@effect/schema": "^0.75.5", + "@effect/opentelemetry": "4.0.0-rc.117", + "@effect/platform-node": "4.0.0-rc.117", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", "@opentelemetry/resources": "^2.2.0", @@ -60,7 +58,7 @@ "@opentelemetry/semantic-conventions": "^1.38.0", "@vercel/kv": "^3.0.0", "drizzle-orm": "^0.45.1", - "effect": "3.19.19", + "effect": "4.0.0-rc.117", "next": "^16.0.10", "postgres": "^3.4.8", "react": "^19.2.3", diff --git a/packages/ep-admin/package.json b/packages/ep-admin/package.json index 8ad720ee..540c605c 100644 --- a/packages/ep-admin/package.json +++ b/packages/ep-admin/package.json @@ -5,13 +5,11 @@ "@effect-patterns/ep-shared-services": "workspace:*", "@effect-patterns/pipeline-state": "workspace:*", "@effect-patterns/toolkit": "workspace:*", - "@effect/cli": "0.73.2", - "@effect/platform": "0.94.5", - "@effect/platform-node": "0.104.1", + "@effect/platform-node": "4.0.0-rc.117", "conventional-commits-parser": "^6.2.1", "conventional-recommended-bump": "^11.2.0", "dotenv": "^17.2.3", - "effect": "3.19.19", + "effect": "4.0.0-rc.117", "effect-cli-tui": "^2.2.0", "effect-env": "^0.4.1", "glob": "^13.0.1", diff --git a/packages/ep-cli/package.json b/packages/ep-cli/package.json index 6dd4ffac..ce2489f1 100644 --- a/packages/ep-cli/package.json +++ b/packages/ep-cli/package.json @@ -20,13 +20,10 @@ "dependencies": { "@effect-patterns/ep-shared-services": "workspace:*", "@effect-patterns/toolkit": "workspace:*", - "@effect/cli": "0.73.2", - "@effect/platform": "0.94.5", "@effect/cluster": "0.56.4", - "@effect/platform-node": "0.104.1", - "@effect/rpc": "0.73.2", - "@effect/sql": "0.49.0", - "effect": "3.19.19", + "@effect/platform-node": "4.0.0-rc.117", + "@effect/sql": "4.0.0-rc.117", + "effect": "4.0.0-rc.117", "effect-cli-tui": "^2.2.0", "effect-env": "^0.4.1" }, diff --git a/packages/ep-shared-services/package.json b/packages/ep-shared-services/package.json index 9141bf52..93fa2287 100644 --- a/packages/ep-shared-services/package.json +++ b/packages/ep-shared-services/package.json @@ -59,9 +59,8 @@ "node": ">=18.0.0" }, "dependencies": { - "effect": "3.19.19", - "@effect/platform": "0.94.5", - "@effect/platform-node": "0.104.1" + "effect": "4.0.0-rc.117", + "@effect/platform-node": "4.0.0-rc.117" }, "devDependencies": { "@biomejs/biome": "^2.3.13", @@ -80,4 +79,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/packages/mcp-transport/package.json b/packages/mcp-transport/package.json index 9eb52691..dfb25d13 100644 --- a/packages/mcp-transport/package.json +++ b/packages/mcp-transport/package.json @@ -33,7 +33,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@effect/opentelemetry": "^0.61.0", + "@effect/opentelemetry": "4.0.0-rc.117", "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.213.0", @@ -42,7 +42,7 @@ "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.38.0", - "effect": "3.19.19", + "effect": "4.0.0-rc.117", "zod": "^4.3.6" }, "devDependencies": { diff --git a/packages/pipeline-state/package.json b/packages/pipeline-state/package.json index 4a023d6e..043bf0f2 100644 --- a/packages/pipeline-state/package.json +++ b/packages/pipeline-state/package.json @@ -45,15 +45,11 @@ "author": "Effect Patterns Team", "license": "MIT", "dependencies": { - "@effect/platform": "0.94.5", - "effect": "3.19.19" + "effect": "4.0.0-rc.117" }, "devDependencies": { - "@effect/schema": "^0.75.5", "typescript": "^5.9.3", "vitest": "^4.0.17" }, - "peerDependencies": { - "@effect/schema": "^0.75.5" - } -} \ No newline at end of file + "peerDependencies": {} +} diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index e12053c8..0a0c6ed6 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -41,16 +41,12 @@ "prepublishOnly": "bun run build" }, "dependencies": { - "@effect/platform": "0.94.5", - "@effect/platform-node": "0.104.1", - "@effect/schema": "^0.75.5", - "effect": "3.19.19" + "@effect/platform-node": "4.0.0-rc.117", + "effect": "4.0.0-rc.117" }, "peerDependencies": { - "@effect/platform": "0.94.5", - "@effect/platform-node": "0.104.1", - "@effect/schema": "^0.75.5", - "effect": "3.19.19" + "@effect/platform-node": "4.0.0-rc.117", + "effect": "4.0.0-rc.117" }, "devDependencies": { "@types/node": "^25.0.6", @@ -87,4 +83,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} From 2d145378256e5bb6b0c2865d18db112c6066f649 Mon Sep 17 00:00:00 2001 From: Muse Spark Date: Thu, 24 Sep 2026 10:01:02 +0200 Subject: [PATCH 2/2] Review v4 courses: remove broken filterEffect courses, fix removed APIs, add missing v4 courses - Remove 5 Schema.filterEffect courses (API removed in v4, no drop-in): schema/async-validation/basic-async, batched-async, database-checks, external-api-validation, schema/form-validation/async-validation - Add schema/async-validation/async-validation-with-effects (decode + Effect, transformEffect inline alternative, verified vs 4.0.0-rc.117) - Fix Schema.optionalWith -> withDecodingDefaultKey in 6 files (optional-fields, with-defaults, multiple-files, database-columns, postgres-jsonb, schema-evolution) - Fix Cause.failures/defects -> reasons filter in error-management-extract-cause; FiberRef/Effect.runtime -> References/Effect.context in tooling-devtools - Fix schema-vs-zod filterEffect placeholder -> transformEffect - Update Result prose Left/Right -> Success/Failure (3 files) - Add missing v4 courses: Context.Reference, Effect.fn, context-not-runtime, cause reasons array, Scope.provide Verified with bun vs effect@4.0.0-rc.117. Yieldable/.asEffect intentionally omitted: documented but not functional in rc.117. --- ...ontext-reference-for-fiber-local-state.mdx | 75 +++++ .../patterns/core-concepts/data-either.mdx | 4 +- .../reusable-functions-with-effect-fn.mdx | 61 ++++ .../run-effects-with-context-not-runtime.mdx | 68 +++++ ...accumulate-multiple-errors-with-either.mdx | 4 +- .../cause-flattened-reasons-array.mdx | 70 +++++ .../error-management-extract-cause.mdx | 12 +- .../pattern-option-either-match.mdx | 2 +- .../scope-provide-and-layer-memoization.mdx | 56 ++++ .../async-validation-with-effects.mdx | 84 ++++++ .../schema/async-validation/basic-async.mdx | 250 --------------- .../schema/async-validation/batched-async.mdx | 254 ---------------- .../async-validation/database-checks.mdx | 284 ------------------ .../external-api-validation.mdx | 264 ---------------- .../form-validation/async-validation.mdx | 218 -------------- .../schema/getting-started/schema-vs-zod.mdx | 13 +- .../json-validation/database-columns.mdx | 8 +- .../schema/json-validation/multiple-files.mdx | 8 +- .../schema/json-validation/postgres-jsonb.mdx | 2 +- .../json-validation/schema-evolution.mdx | 2 +- .../schema/json-validation/with-defaults.mdx | 10 +- .../schema/objects/optional-fields.mdx | 10 +- .../tooling-devtools.mdx | 19 +- 23 files changed, 463 insertions(+), 1315 deletions(-) create mode 100644 content/published/patterns/core-concepts/context-reference-for-fiber-local-state.mdx create mode 100644 content/published/patterns/core-concepts/reusable-functions-with-effect-fn.mdx create mode 100644 content/published/patterns/core-concepts/run-effects-with-context-not-runtime.mdx create mode 100644 content/published/patterns/error-management/cause-flattened-reasons-array.mdx create mode 100644 content/published/patterns/resource-management/scope-provide-and-layer-memoization.mdx create mode 100644 content/published/patterns/schema/async-validation/async-validation-with-effects.mdx delete mode 100644 content/published/patterns/schema/async-validation/basic-async.mdx delete mode 100644 content/published/patterns/schema/async-validation/batched-async.mdx delete mode 100644 content/published/patterns/schema/async-validation/database-checks.mdx delete mode 100644 content/published/patterns/schema/async-validation/external-api-validation.mdx delete mode 100644 content/published/patterns/schema/form-validation/async-validation.mdx diff --git a/content/published/patterns/core-concepts/context-reference-for-fiber-local-state.mdx b/content/published/patterns/core-concepts/context-reference-for-fiber-local-state.mdx new file mode 100644 index 00000000..fea913aa --- /dev/null +++ b/content/published/patterns/core-concepts/context-reference-for-fiber-local-state.mdx @@ -0,0 +1,75 @@ +--- +title: Share Fiber-Local State with Context.Reference +id: context-reference-for-fiber-local-state +skillLevel: intermediate +applicationPatternId: core-concepts +summary: >- + Use Context.Reference for fiber-local state in Effect v4. FiberRef was removed; + yield references directly and override with Effect.provideService. +tags: + - context + - reference + - fiber-local + - v4 +rule: + description: >- + Use Context.Reference with a defaultValue for fiber-local state; read it + with yield* and override per-fiber with Effect.provideService. +related: + - understand-layers-for-dependency-injection + - access-config-in-context +author: effect_website +lessonOrder: 30 +--- + +# Problem + +You need request-scoped or fiber-local state (tenant id, log level, feature flag) without threading parameters through every function. In v4 `FiberRef` no longer exists. + +# Solution + +```typescript +import { Context, Effect, References } from "effect" + +class TenantId extends Context.Reference()("TenantId", { + defaultValue: () => "public" +}) {} + +const program = Effect.gen(function*() { + const before = yield* TenantId + yield* Effect.log(`tenant: ${before}`) + return before +}) + +// Override for a single fiber +const scoped = program.pipe( + Effect.provideService(TenantId, "acme") +) + +// Built-in references work the same way +const showLevel = Effect.gen(function*() { + const level = yield* References.CurrentLogLevel + yield* Effect.log(`level: ${level.label}`) +}) + +Effect.runPromise(scoped.pipe(Effect.zipRight(showLevel))) +``` + +# Why This Works + +| v3 | v4 | +|----|----| +| `FiberRef.make` / `FiberRef.get` | `Context.Reference` with `defaultValue`, `yield* Ref` directly | +| `FiberRef.currentLogLevel` etc. | `References.CurrentLogLevel`, `MinimumLogLevel`, `CurrentLogAnnotations`, … | +| `Effect.locally` | `Effect.provideService` | + +# When to Use + +- Tenant / request id propagation +- Per-fiber log level, annotations, spans +- Feature flags with safe defaults + +# Related Patterns + +- [Understand Layers](./understand-layers-for-dependency-injection.mdx) +- [Access Config](./access-config-in-context.mdx) diff --git a/content/published/patterns/core-concepts/data-either.mdx b/content/published/patterns/core-concepts/data-either.mdx index 61e43383..af03a17b 100644 --- a/content/published/patterns/core-concepts/data-either.mdx +++ b/content/published/patterns/core-concepts/data-either.mdx @@ -27,7 +27,7 @@ lessonOrder: 1 ## Guideline -Use the `Result` data type to represent computations that can fail (`Left`) or succeed (`Right`). +Use the `Result` data type to represent computations that can fail (`Failure`) or succeed (`Success`). This makes error handling explicit, type-safe, and composable. ## Rationale @@ -40,7 +40,7 @@ It allows you to accumulate errors, model domain-specific failures, and avoid ex ```typescript import { Result } from "effect"; -// Create a Right (success) or Left (failure) +// Create a Success or Failure const success = Result.succeed(42); // Result const failure = Result.fail("Something went wrong"); // Result diff --git a/content/published/patterns/core-concepts/reusable-functions-with-effect-fn.mdx b/content/published/patterns/core-concepts/reusable-functions-with-effect-fn.mdx new file mode 100644 index 00000000..f2108cc7 --- /dev/null +++ b/content/published/patterns/core-concepts/reusable-functions-with-effect-fn.mdx @@ -0,0 +1,61 @@ +--- +title: Write Reusable Functions with Effect.fn +id: reusable-functions-with-effect-fn +skillLevel: intermediate +applicationPatternId: core-concepts +summary: >- + Use Effect.fn for traced reusable functions and Effect.fnUntraced for hot + paths. Do not wrap Effect.gen in a plain function. +tags: + - effect-fn + - tracing + - functions + - v4 +rule: + description: >- + Define reusable effectful functions with Effect.fn("name") or + Effect.fnUntraced; add behavior via extra args, not .pipe after. +related: + - write-sequential-code-with-gen + - transform-effect-values +author: effect_website +lessonOrder: 32 +--- + +# Problem + +Wrapping `Effect.gen` in a plain arrow function loses span names, stack traces, and encourages `.pipe` after the fact instead of declarative combinators. + +# Solution + +```typescript +import { Effect } from "effect" + +// Traced boundary β€” creates a span named "getUser" +const getUser = Effect.fn("getUser")(function*(id: number) { + yield* Effect.logInfo(`fetch ${id}`) + return { id } +}) + +// Hot path β€” no tracing overhead +const isPositive = Effect.fnUntraced(function*(n: number) { + return n > 0 +}) + +Effect.runPromise(getUser(1).pipe(Effect.zipRight(isPositive(2)))) +``` + +# Why This Works + +- Name string matches function name β†’ better stacks + automatic `withSpan`. +- Extra args after the generator replace `.pipe(Effect.fn(...)(...))`. +- `Effect.fn.Return` types the return without annotating the generator. + +# When to Use + +- `Effect.fn("name")`: service methods, handlers, traced boundaries. +- `Effect.fnUntraced`: validators, mappers, tight loops. + +# Related Patterns + +- [Write Sequential Code](./write-sequential-code-with-gen.mdx) diff --git a/content/published/patterns/core-concepts/run-effects-with-context-not-runtime.mdx b/content/published/patterns/core-concepts/run-effects-with-context-not-runtime.mdx new file mode 100644 index 00000000..a20109f0 --- /dev/null +++ b/content/published/patterns/core-concepts/run-effects-with-context-not-runtime.mdx @@ -0,0 +1,68 @@ +--- +title: Run Effects with Context, Not Runtime +id: run-effects-with-context-not-runtime +skillLevel: intermediate +applicationPatternId: core-concepts +summary: >- + Runtime was removed in v4. Capture Effect.context and run with + Effect.runForkWith; use ManagedRuntime for long-lived apps. +tags: + - runtime + - context + - managed-runtime + - v4 +rule: + description: >- + Replace Effect.runtime + Runtime.runFork with Effect.context + + Effect.runForkWith. Use ManagedRuntime.make for servers/workers. +related: + - create-reusable-runtime-from-layers + - understand-layers-for-dependency-injection +author: effect_website +lessonOrder: 33 +--- + +# Problem + +v3 code that threads `Runtime` (`Effect.runtime`, `Runtime.runFork`) fails in v4 β€” `Effect.runtime` is `undefined` and `Runtime` only holds process lifecycle utilities. + +# Solution + +```typescript +import { Context, Effect, Layer, ManagedRuntime } from "effect" + +class Logger extends Context.Service void +}>()("Logger") {} + +const program = Effect.gen(function*() { + const logger = yield* Logger + logger.log("hello") +}) + +// One-shot: capture services, run later +const main = Effect.gen(function*() { + const services = yield* Effect.context() + return Effect.runForkWith(services)(program) +}).pipe( + Effect.provideContext(Context.make(Logger, { log: (m) => console.log(m) })) +) + +// Long-lived: prefer ManagedRuntime +const AppLive = Layer.succeed(Logger, { log: (m) => console.log(m) }) +const App = ManagedRuntime.make(AppLive) +void App +void main +``` + +# Why This Works + +| v3 | v4 | +|----|----| +| `Effect.runtime()` | `Effect.context()` | +| `Runtime.runFork(rt)(eff)` | `Effect.runForkWith(services)(eff)` | +| `Runtime` type | `Context` | + +# Related Patterns + +- [Create Reusable Runtime](./create-reusable-runtime-from-layers.mdx) diff --git a/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx b/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx index 81e0ba7a..073f2fbf 100644 --- a/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx +++ b/content/published/patterns/domain-modeling/accumulate-multiple-errors-with-either.mdx @@ -25,7 +25,7 @@ lessonOrder: 1 ## Guideline -When you need to perform multiple validation checks and collect all failures, use the `Result` data type. `Result` represents a value that can be one of two possibilities: a `Left` (typically for failure) or a `Right` (typically for success). +When you need to perform multiple validation checks and collect all failures, use the `Result` data type. `Result` represents a value that can be one of two possibilities: a `Failure` (typically for failure) or a `Success` (typically for success). --- @@ -35,7 +35,7 @@ The `Effect` error channel is designed to short-circuit. The moment an `Effect` However, for tasks like validating a user's input, this is poor user experience. You want to show the user all of their mistakes at once. -`Result` is the solution. Since it's a pure data structure, you can run multiple checks that each return an `Result`, and then combine the results to accumulate all the `Left` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation. +`Result` is the solution. Since it's a pure data structure, you can run multiple checks that each return a `Result`, and then combine the results to accumulate all the `Failure` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation. --- diff --git a/content/published/patterns/error-management/cause-flattened-reasons-array.mdx b/content/published/patterns/error-management/cause-flattened-reasons-array.mdx new file mode 100644 index 00000000..57de4af6 --- /dev/null +++ b/content/published/patterns/error-management/cause-flattened-reasons-array.mdx @@ -0,0 +1,70 @@ +--- +title: Inspect Flattened Causes via reasons Array +id: cause-flattened-reasons-array +skillLevel: intermediate +applicationPatternId: error-management +summary: >- + Cause is flat in v4: iterate cause.reasons (Fail/Die/Interrupt). Empty, + Sequential and Parallel variants are gone. +tags: + - cause + - error-handling + - v4 +rule: + description: >- + Match on cause.reasons with isFailReason/isDieReason/isInterruptReason; + use hasFails/hasDies/findFail/findDefect instead of failures/defects. +related: + - error-management-extract-cause + - handle-unexpected-errors-with-cause +author: effect_website +lessonOrder: 5 +--- + +# Problem + +v3 matched `Cause` as a tree (`Empty | Fail | Die | Sequential | Parallel`). That code does not compile in v4, and `Cause.failures/defects` are `undefined`. + +# Solution + +```typescript +import { Cause, Effect } from "effect" + +const handle = (cause: Cause.Cause) => { + for (const reason of cause.reasons) { + switch (reason._tag) { + case "Fail": + return `fail: ${reason.error}` + case "Die": + return `die: ${reason.defect}` + case "Interrupt": + return `interrupt` + } + } + return "empty" +} + +const program = Effect.die("bug").pipe( + Effect.catchCause((cause) => + Effect.gen(function*() { + const failures = cause.reasons.filter(Cause.isFailReason).map((r) => r.error) + const defects = cause.reasons.filter(Cause.isDieReason).map((r) => r.defect) + yield* Effect.log(`hasFails: ${Cause.hasFails(cause)}, hasDies: ${Cause.hasDies(cause)}`) + yield* Effect.log(`failures: ${JSON.stringify(failures)}, defects: ${defects.length}`) + return handle(cause) + }) + ) +) + +Effect.runPromise(program) +``` + +# Why This Works + +- `cause.reasons.length === 0` replaces `isEmptyType`. +- `Cause.isFailReason/isDieReason/isInterruptReason` replace type-level guards. +- `Cause.findFail/findDefect`, `hasFails/hasDies`, `Cause.pretty` still exist. + +# Related Patterns + +- [Extract Failures and Defects](./error-management-extract-cause.mdx) diff --git a/content/published/patterns/error-management/error-management-extract-cause.mdx b/content/published/patterns/error-management/error-management-extract-cause.mdx index b0f1faf6..5060f5d8 100644 --- a/content/published/patterns/error-management/error-management-extract-cause.mdx +++ b/content/published/patterns/error-management/error-management-extract-cause.mdx @@ -4,7 +4,7 @@ id: error-management-extract-cause skillLevel: intermediate applicationPatternId: error-management summary: >- - Use Cause.failures and Cause.defects to inspect a Cause and handle expected + Use Cause reason inspection to handle expected failures differently from unexpected defects. tags: - error management @@ -12,7 +12,7 @@ tags: - defects - failures rule: - description: Inspect Cause to separate expected failures from defects. + description: Inspect Cause.reasons to separate expected failures from defects. related: - handle-unexpected-errors-with-cause - pattern-catchtag @@ -24,7 +24,7 @@ lessonOrder: 4 ## Guideline -When you catch with `Effect.catchCause`, use `Cause.failures(cause)` to get expected typed errors and `Cause.defects(cause)` to get unexpected defects. Handle them differently: recover or retry failures; log and escalate defects. +When you catch with `Effect.catchCause`, iterate `cause.reasons` (`Fail` / `Die` / `Interrupt`) or use `Cause.findFail` / `Cause.findDefect`. Handle failures differently: recover or retry failures; log and escalate defects. ## Rationale @@ -38,8 +38,8 @@ import { Effect, Cause } from "effect" const program = Effect.die("unexpected bug").pipe( Effect.catchCause((cause) => Effect.gen(function* () { - const defects = Cause.defects(cause) - const failures = Cause.failures(cause) + const defects = cause.reasons.filter(Cause.isDieReason).map((r) => r.defect) + const failures = cause.reasons.filter(Cause.isFailReason).map((r) => r.error) yield* Effect.sync(() => { console.log("Defects:", defects) console.log("Failures:", failures) @@ -52,7 +52,7 @@ const program = Effect.die("unexpected bug").pipe( Effect.runPromise(program) ``` -**Explanation:** `Cause.defects` and `Cause.failures` extract the underlying values. Use them to decide: log defects, recover from failures, or return a fallback. +**Explanation:** Filter `cause.reasons` with `Cause.isFailReason` / `Cause.isDieReason` (or use `Cause.findFail` / `Cause.findDefect`). Use them to decide: log defects, recover from failures, or return a fallback. ## Anti-Pattern diff --git a/content/published/patterns/error-management/pattern-option-either-match.mdx b/content/published/patterns/error-management/pattern-option-either-match.mdx index e61220d8..55e7d367 100644 --- a/content/published/patterns/error-management/pattern-option-either-match.mdx +++ b/content/published/patterns/error-management/pattern-option-either-match.mdx @@ -25,7 +25,7 @@ lessonOrder: 3 ## Guideline -When you need to handle `Option` or `Result` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Result) in a single expression. +When you need to handle `Option` or `Result` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Success/Failure for Result) in a single expression. Use `.match()` when: - You need to handle both success and failure cases diff --git a/content/published/patterns/resource-management/scope-provide-and-layer-memoization.mdx b/content/published/patterns/resource-management/scope-provide-and-layer-memoization.mdx new file mode 100644 index 00000000..18430830 --- /dev/null +++ b/content/published/patterns/resource-management/scope-provide-and-layer-memoization.mdx @@ -0,0 +1,56 @@ +--- +title: Provide Scopes with Scope.provide +id: scope-provide-and-layer-memoization +skillLevel: intermediate +applicationPatternId: resource-management +summary: >- + Scope.extend was renamed to Scope.provide in v4. Layers memoize across + Effect.provide calls. +tags: + - scope + - layer + - resource-management + - v4 +rule: + description: >- + Use Scope.provide (data-first or curried) to run scoped effects; + rely on Layer memoization instead of manual caching. +related: + - manual-scope-management + - compose-scoped-layers +author: effect_website +lessonOrder: 11 +--- + +# Problem + +v3 `Scope.extend` does not exist in v4, and manual layer caching fights the runtime. + +# Solution + +```typescript +import { Effect, Scope } from "effect" + +const program = Effect.gen(function*() { + const scope = yield* Scope.make() + // data-last (curried) + yield* myEffect.pipe(Scope.provide(scope)) + // data-first equivalent: yield* Scope.provide(myEffect, scope) +}) + +const myEffect = Effect.log("using scoped resource") +void program +``` + +# Why This Works + +| v3 | v4 | +|----|----| +| `Scope.extend(eff, scope)` | `Scope.provide(eff, scope)` / `eff.pipe(Scope.provide(scope))` | + +Layers memoize across `Effect.provide` calls in v4 β€” building the same layer twice reuses the first result. Fibers also keep the process alive automatically (fiber keep-alive). + +# Related Patterns + +- [Manual Scope Management](./manual-scope-management.mdx) +- [Compose Scoped Layers](./compose-scoped-layers.mdx) diff --git a/content/published/patterns/schema/async-validation/async-validation-with-effects.mdx b/content/published/patterns/schema/async-validation/async-validation-with-effects.mdx new file mode 100644 index 00000000..146dba5d --- /dev/null +++ b/content/published/patterns/schema/async-validation/async-validation-with-effects.mdx @@ -0,0 +1,84 @@ +--- +id: schema-async-validation-with-effects +title: Async Validation by Composing Decode and Effects +category: async-validation +skillLevel: intermediate +tags: + - schema + - async-validation + - effects + - v4 +lessonOrder: 6 +rule: + description: >- + Validate synchronously with Schema, then run async checks as Effects. + Use decodeTo with transformEffect for inline async rules. +summary: >- + Schema.filterEffect was removed in v4. Decode first, check async after β€” + or embed async rules with SchemaGetter.transformEffect. +--- + +# Problem + +Username-taken, discount-code, email-domain checks need I/O. `Schema.filterEffect` does not exist in v4. + +# Solution + +```typescript +import { Effect, Schema, SchemaGetter } from "effect" + +// 1. Sync shape first +const SignUpForm = Schema.Struct({ + username: Schema.String.pipe( + Schema.check(Schema.isMinLength(3)), + Schema.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) + ), + email: Schema.String +}) + +// 2a. Preferred: decode sync, check async after +const checkUsernameTaken = (username: string): Effect.Effect => + Effect.gen(function*() { + yield* Effect.sleep("100 millis") + if (["admin", "root", "system"].includes(username)) { + return yield* Effect.fail(new Error(`Username "${username}" is taken`)) + } + return username + }) + +const validateSignup = (input: unknown): Effect.Effect => + Effect.gen(function*() { + const form = yield* Schema.decodeUnknownEffect(SignUpForm)(input) + yield* checkUsernameTaken(form.username) + return form + }) + +// 2b. Inline: embed async rule with transformEffect +const UsernameAvailable = Schema.String.pipe( + Schema.check(Schema.isMinLength(3)), + Schema.decodeTo(Schema.String, { + decode: SchemaGetter.transformEffect((s) => + s === "admin" ? Effect.fail(new Error("taken")) : Effect.succeed(s) + ) + }) +) + +Effect.runPromise(validateSignup({ username: "bob", email: "b@x.com" })) +void UsernameAvailable +``` + +# Why This Works + +- Keeps sync format rules in Schema (portable, encodable). +- Keeps I/O in `Effect` where retries, layers, and testing apply. +- `transformEffect` is the v4 escape hatch when the async rule must live inside the schema. + +# When to Use + +- DB / API uniqueness checks β†’ option 2a. +- Reusable schema with baked-in async rule β†’ option 2b. + +# Related Patterns + +- [Decode Encode](../getting-started/decode-encode.mdx) +- [Handling Errors](../getting-started/handling-errors.mdx) diff --git a/content/published/patterns/schema/async-validation/basic-async.mdx b/content/published/patterns/schema/async-validation/basic-async.mdx deleted file mode 100644 index 7664bd74..00000000 --- a/content/published/patterns/schema/async-validation/basic-async.mdx +++ /dev/null @@ -1,250 +0,0 @@ ---- -id: schema-basic-async -title: Basic Async Validation with Schema.filterEffect -category: async-validation -skillLevel: beginner -tags: - - schema - - async-validation - - effects - - filtering - - validation-rules -lessonOrder: 6 -rule: - description: >- - Basic Async Validation with Schema.filterEffect. -summary: >- - You have sync validation rules (format, length) handled by schema. But some rules require async operations: checking a username isn't taken, verifying a discount code is valid, calling an external... ---- - -# Problem - -You have sync validation rules (format, length) handled by schema. But some rules require async operations: checking a username isn't taken, verifying a discount code is valid, calling an external API. You need schemas that perform async validation without leaving the schema layer. - -# Solution - -```typescript -import { Schema, Effect } from "effect" - -// ============================================ -// 1. Basic async filter -// ============================================ - -const ValidUsername = Schema.String.pipe( - Schema.check(Schema.isMinLength(3)), - Schema.check(Schema.isMaxLength(20)), - Schema.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)), - Schema.filterEffect((username) => - Effect.gen(function* () { - // Simulate async check - yield* Effect.sleep(Duration.millis(100)) - - // Check if username is taken - const isTaken = ["admin", "root", "system"].includes(username) - - if (isTaken) { - return yield* Effect.fail( - new Error(`Username "${username}" is already taken`) - ) - } - - return username - }) - ) -) - -// ============================================ -// 2. Email with async verification -// ============================================ - -const VerifiedEmail = Schema.String.pipe( - Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), - Schema.filterEffect((email) => - Effect.gen(function* () { - // Simulate async email verification - yield* Effect.sleep(Duration.millis(200)) - - // Check against a denylist - const bannedDomains = ["temp-mail.com", "10minutemail.com"] - const [, domain] = email.split("@") - - if (bannedDomains.includes(domain)) { - return yield* Effect.fail( - new Error(`Email domain "${domain}" not allowed`) - ) - } - - return email - }) - ) -) - -// ============================================ -// 3. Discount code validation -// ============================================ - -const ValidDiscountCode = Schema.String.pipe( - Schema.toUpperCase(), - Schema.filterEffect((code) => - Effect.gen(function* () { - // Simulate database lookup - yield* Effect.sleep(Duration.millis(150)) - - const validCodes: Record = { - WELCOME10: 10, - SAVE20: 20, - VIP50: 50, - } - - if (!validCodes[code]) { - return yield* Effect.fail( - new Error(`Discount code "${code}" is invalid`) - ) - } - - return code - }) - ) -) - -// ============================================ -// 4. SignUp form with async validation -// ============================================ - -const SignUpForm = Schema.Struct({ - username: ValidUsername, - email: VerifiedEmail, - password: Schema.String.pipe(Schema.check(Schema.isMinLength(8))), - discountCode: Schema.optional(ValidDiscountCode), -}) - -type SignUpForm = typeof SignUpForm.Type - -// ============================================ -// 5. Processing async validation -// ============================================ - -const validateSignup = ( - data: unknown -): Effect.Effect => - Effect.gen(function* () { - console.log("Validating signup form...") - - const result = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(SignUpForm)(data), - catch: (error) => { - const msg = error instanceof Error ? error.message : String(error) - return new Error(`Validation failed: ${msg}`) - }, - }) - - console.log("βœ… All validations passed") - return result - }) - -// ============================================ -// 6. Application logic -// ============================================ - -const appLogic = Effect.gen(function* () { - console.log("=== Basic Async Validation ===\n") - - console.log("1. Valid signup:\n") - - const validData = { - username: "alice_dev", - email: "alice@example.com", - password: "SecurePassword123", - discountCode: "WELCOME10", - } - - const result1 = yield* validateSignup(validData) - console.log(`βœ“ User: ${result1.username}`) - console.log(`βœ“ Email: ${result1.email}`) - - console.log("\n2. Invalid username (taken):\n") - - const invalidUsername = { - username: "admin", - email: "admin@example.com", - password: "SecurePassword123", - } - - const result2 = yield* validateSignup(invalidUsername).pipe( - Effect.result - ) - - if (result2._tag === "Failure") { - console.log(`βœ— Error: ${result2.failure.message}`) - } - - console.log("\n3. Invalid email domain:\n") - - const invalidEmail = { - username: "bob_coder", - email: "bob@temp-mail.com", - password: "SecurePassword123", - } - - const result3 = yield* validateSignup(invalidEmail).pipe( - Effect.result - ) - - if (result3._tag === "Failure") { - console.log(`βœ— Error: ${result3.failure.message}`) - } - - console.log("\n4. Invalid discount code:\n") - - const invalidCode = { - username: "charlie", - email: "charlie@example.com", - password: "SecurePassword123", - discountCode: "INVALID99", - } - - const result4 = yield* validateSignup(invalidCode).pipe( - Effect.result - ) - - if (result4._tag === "Failure") { - console.log(`βœ— Error: ${result4.failure.message}`) - } - - return result1 -}) - -const { Duration } = require("effect") - -// Run application -Effect.runPromise(appLogic) - .then(() => console.log("\nβœ… Async validation complete")) - .catch((error) => console.error(`Error: ${error.message}`)) -``` - -# Why This Works - -| Concept | Explanation | -|---------|-------------| -| **filterEffect** | Run Effect during schema validation | -| **Composable** | Chain sync validation with async filters | -| **Error handling** | Async errors become validation errors | -| **Type safe** | Output type same as input schema | -| **Readable** | Validation logic near schema definition | -| **Reusable** | Each filter is independently composable | - -# When to Use - -- Username availability checks -- Email verification -- Discount code validation -- API-based validation rules -- Database lookup during parse -- Rate limit checks -- License verification - -# Related Patterns - -- [Database Checks](./database-checks.md) -- [External API Validation](./external-api-validation.md) -- [Batched Async](./batched-async.md) diff --git a/content/published/patterns/schema/async-validation/batched-async.mdx b/content/published/patterns/schema/async-validation/batched-async.mdx deleted file mode 100644 index 5988935a..00000000 --- a/content/published/patterns/schema/async-validation/batched-async.mdx +++ /dev/null @@ -1,254 +0,0 @@ ---- -id: schema-batched-async -title: Efficient Batched Async Validation and Deduplication -category: async-validation -skillLevel: advanced -tags: - - schema - - async-validation - - batching - - deduplication - - performance - - efficiency -lessonOrder: 1 -rule: - description: >- - Efficient Batched Async Validation and Deduplication using Schema. -summary: >- - Validating 100 items individually makes 100 API calls. But the API accepts batch operations. You need async validation that deduplicates requests and validates in batches for efficiency. Validate... ---- - -# Problem - -Validating 100 items individually makes 100 API calls. But the API accepts batch operations. You need async validation that deduplicates requests and validates in batches for efficiency. Validate many items with minimal API calls. - -# Solution - -```typescript -import { Schema, Effect } from "effect" -import { HashMap, Queue } from "effect" - -// ============================================ -// 1. Simulated batch API -// ============================================ - -class BatchValidationService { - async validateMultipleUsernames( - usernames: string[] - ): Promise> { - console.log(` [API Call] Validating ${usernames.length} usernames...`) - await new Promise((resolve) => setTimeout(resolve, 300)) - - const taken = new Set(["alice", "bob", "admin", "root"]) - const result = new Map() - - for (const username of usernames) { - result.set(username, !taken.has(username)) - } - - return result - } - - async validateMultipleEmails( - emails: string[] - ): Promise> { - console.log(` [API Call] Validating ${emails.length} emails...`) - await new Promise((resolve) => setTimeout(resolve, 250)) - - const result = new Map() - for (const email of emails) { - // Simple simulation: no "@taken.com" allowed - result.set(email, !email.includes("@taken.com")) - } - - return result - } -} - -// ============================================ -// 2. Batch validation cache -// ============================================ - -class ValidationCache { - private usernameCache = new Map() - private emailCache = new Map() - - async validateUsername(username: string): Promise { - if (this.usernameCache.has(username)) { - return this.usernameCache.get(username)! - } - - return new Promise((resolve) => { - // Defer for batching - setTimeout(() => { - const isValid = !["alice", "bob", "admin", "root"].includes(username) - this.usernameCache.set(username, isValid) - resolve(isValid) - }, 50) - }) - } - - async validateEmail(email: string): Promise { - if (this.emailCache.has(email)) { - return this.emailCache.get(email)! - } - - return new Promise((resolve) => { - setTimeout(() => { - const isValid = !email.includes("@taken.com") - this.emailCache.set(email, isValid) - resolve(isValid) - }, 50) - }) - } -} - -// ============================================ -// 3. Batch validation schemas -// ============================================ - -const cache = new ValidationCache() -const service = new BatchValidationService() - -const BatchValidatedUsername = Schema.String.pipe( - Schema.check(Schema.isMinLength(3)), - Schema.filterEffect((username) => - Effect.gen(function* () { - const isValid = yield* Effect.tryPromise({ - try: () => cache.validateUsername(username), - catch: () => false, - }) - - if (!isValid) { - return yield* Effect.fail( - new Error(`Username "${username}" is taken`) - ) - } - - return username - }) - ) -) - -const BatchValidatedEmail = Schema.String.pipe( - Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), - Schema.filterEffect((email) => - Effect.gen(function* () { - const isValid = yield* Effect.tryPromise({ - try: () => cache.validateEmail(email), - catch: () => false, - }) - - if (!isValid) { - return yield* Effect.fail( - new Error(`Email "${email}" is taken`) - ) - } - - return email - }) - ) -) - -// ============================================ -// 4. Bulk user validation -// ============================================ - -const BulkUserData = Schema.Array( - Schema.Struct({ - username: BatchValidatedUsername, - email: BatchValidatedEmail, - }) -) - -// ============================================ -// 5. Application logic -// ============================================ - -const appLogic = Effect.gen(function* () { - console.log("=== Batched Async Validation ===\n") - - console.log("1. Validating 5 users (demonstrates deduplication):\n") - - const users = [ - { username: "charlie", email: "charlie@example.com" }, - { username: "diana", email: "diana@example.com" }, - { username: "charlie", email: "charlie@example.com" }, // Duplicate - { username: "eve", email: "eve@example.com" }, - { username: "frank", email: "frank@taken.com" }, // Invalid email - ] - - console.log("Input:") - for (const user of users) { - console.log(` - ${user.username} (${user.email})`) - } - - console.log("\nValidating...") - - const result = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(BulkUserData)(users), - catch: (e) => new Error(String(e)), - }).pipe(Effect.result) - - if (result._tag === "Failure") { - console.log(`\nβœ— Validation failed: ${result.failure.message}`) - console.log("Note: Thanks to batching + cache, duplicates only validated once") - } else { - console.log("\nβœ“ All users validated") - for (const user of result.success) { - console.log(` - ${user.username}: OK`) - } - } - - console.log("\n2. Efficiency demonstration:\n") - - console.log("Without batching: 5 API calls") - console.log("With batching: 1 API call + deduplication cache") - console.log("With duplicates: Cache prevents re-validation") - - console.log("\n3. Batch statistics:\n") - - // Show unique usernames in batch - const uniqueUsernames = new Set(users.map((u) => u.username)) - const uniqueEmails = new Set(users.map((u) => u.email)) - - console.log(`Total items: ${users.length}`) - console.log(`Unique usernames: ${uniqueUsernames.size}`) - console.log(`Unique emails: ${uniqueEmails.size}`) - console.log(`Deduplication benefit: ${users.length - uniqueUsernames.size} avoided calls`) - - return result -}) - -// Run application -Effect.runPromise(appLogic) - .then(() => console.log("\nβœ… Batched validation complete")) - .catch((error) => console.error(`Error: ${error.message}`)) -``` - -# Why This Works - -| Concept | Explanation | -|---------|-------------| -| **Request batching** | Combine multiple validations into one API call | -| **Deduplication** | Cache prevents re-validating same values | -| **Lazy evaluation** | Batch validation deferred to collect all requests | -| **Efficient** | 100 items β†’ 1 API call instead of 100 | -| **Transparent** | Same schema interface; optimization invisible | -| **Type safe** | Decoded values guaranteed valid by batch API | - -# When to Use - -- Bulk user imports (validate 1000s of usernames) -- Batch email verification -- Large form submissions -- Bulk data validation -- Data migrations with external validation -- Webhook processing with duplicate events -- Rate-limited APIs requiring batching - -# Related Patterns - -- [Basic Async](./basic-async.md) -- [Database Checks](./database-checks.md) -- [External API Validation](./external-api-validation.md) diff --git a/content/published/patterns/schema/async-validation/database-checks.mdx b/content/published/patterns/schema/async-validation/database-checks.mdx deleted file mode 100644 index b22b4b88..00000000 --- a/content/published/patterns/schema/async-validation/database-checks.mdx +++ /dev/null @@ -1,284 +0,0 @@ ---- -id: schema-database-checks -title: 'Database Validation - Uniqueness, Foreign Keys, Constraints' -category: async-validation -skillLevel: intermediate -tags: - - schema - - async-validation - - database - - constraints - - uniqueness - - foreign-keys -lessonOrder: 7 -rule: - description: >- - Database Validation - Uniqueness, Foreign Keys, Constraints using Schema. -summary: >- - A username must be unique in the database. A product reference must exist. An email can't belong to two accounts. These database constraints can't be validated with sync schemas. You need async... ---- - -# Problem - -A username must be unique in the database. A product reference must exist. An email can't belong to two accounts. These database constraints can't be validated with sync schemas. You need async validation that queries the database during parsing. - -# Solution - -```typescript -import { Schema, Effect } from "effect" -import { Duration } from "effect" - -// ============================================ -// 1. Simulated database -// ============================================ - -class Database { - private users = new Map([ - ["user1", { id: "1", username: "alice" }], - ["user2", { id: "2", username: "bob" }], - ]) - - private products = new Map([ - ["prod1", "Laptop"], - ["prod2", "Monitor"], - ]) - - async checkUsernameUnique(username: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 100)) - return !Array.from(this.users.values()).some( - (u) => u.username === username - ) - } - - async productExists(id: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 50)) - return this.products.has(id) - } - - async checkEmailUnique(email: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 80)) - // Simulated check - return !email.includes("taken") - } - - async validateForeignKey( - parentId: string, - table: string - ): Promise { - await new Promise((resolve) => setTimeout(resolve, 100)) - if (table === "users") return this.users.has(parentId) - if (table === "products") return this.products.has(parentId) - return false - } -} - -// ============================================ -// 2. Create database service -// ============================================ - -const db = new Database() - -// ============================================ -// 3. Schemas with database validation -// ============================================ - -const UniqueUsername = Schema.String.pipe( - Schema.check(Schema.isMinLength(3)), - Schema.filterEffect((username) => - Effect.gen(function* () { - const isUnique = yield* Effect.tryPromise({ - try: () => db.checkUsernameUnique(username), - catch: () => false, - }) - - if (!isUnique) { - return yield* Effect.fail( - new Error(`Username "${username}" is taken`) - ) - } - - return username - }) - ) -) - -const UniqueEmail = Schema.String.pipe( - Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), - Schema.filterEffect((email) => - Effect.gen(function* () { - const isUnique = yield* Effect.tryPromise({ - try: () => db.checkEmailUnique(email), - catch: () => false, - }) - - if (!isUnique) { - return yield* Effect.fail( - new Error(`Email "${email}" already registered`) - ) - } - - return email - }) - ) -) - -const ExistingProduct = Schema.String.pipe( - Schema.filterEffect((productId) => - Effect.gen(function* () { - const exists = yield* Effect.tryPromise({ - try: () => db.productExists(productId), - catch: () => false, - }) - - if (!exists) { - return yield* Effect.fail( - new Error(`Product "${productId}" not found`) - ) - } - - return productId - }) - ) -) - -const ValidUserReference = Schema.String.pipe( - Schema.filterEffect((userId) => - Effect.gen(function* () { - const exists = yield* Effect.tryPromise({ - try: () => db.validateForeignKey(userId, "users"), - catch: () => false, - }) - - if (!exists) { - return yield* Effect.fail( - new Error(`User "${userId}" does not exist`) - ) - } - - return userId - }) - ) -) - -// ============================================ -// 4. Forms with foreign keys -// ============================================ - -const CreateOrderForm = Schema.Struct({ - userId: ValidUserReference, - productId: ExistingProduct, - quantity: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(1, 100))), -}) - -const CreateUserForm = Schema.Struct({ - username: UniqueUsername, - email: UniqueEmail, - password: Schema.String.pipe(Schema.check(Schema.isMinLength(8))), -}) - -// ============================================ -// 5. Application logic -// ============================================ - -const appLogic = Effect.gen(function* () { - console.log("=== Database Validation ===\n") - - console.log("1. Valid unique username:\n") - - const validUser = { - username: "charlie", - email: "charlie@example.com", - password: "SecurePass123", - } - - const user1 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(CreateUserForm)(validUser), - catch: (e) => new Error(String(e)), - }) - - console.log(`βœ“ User created: ${user1.username}`) - - console.log("\n2. Duplicate username:\n") - - const duplicateUser = { - username: "alice", - email: "newalice@example.com", - password: "SecurePass123", - } - - const user2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(CreateUserForm)(duplicateUser), - catch: (e) => new Error(String(e)), - }).pipe(Effect.result) - - if (user2._tag === "Failure") { - console.log(`βœ— Error: ${user2.failure.message}`) - } - - console.log("\n3. Valid order with foreign keys:\n") - - const validOrder = { - userId: "user1", - productId: "prod1", - quantity: 5, - } - - const order = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(CreateOrderForm)(validOrder), - catch: (e) => new Error(String(e)), - }) - - console.log(`βœ“ Order created for user ${order.userId}`) - console.log(` Product: ${order.productId}, Quantity: ${order.quantity}`) - - console.log("\n4. Invalid product reference:\n") - - const invalidOrder = { - userId: "user1", - productId: "prod999", - quantity: 2, - } - - const order2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(CreateOrderForm)(invalidOrder), - catch: (e) => new Error(String(e)), - }).pipe(Effect.result) - - if (order2._tag === "Failure") { - console.log(`βœ— Error: ${order2.failure.message}`) - } - - return { user1, order } -}) - -// Run application -Effect.runPromise(appLogic) - .then(() => console.log("\nβœ… Database validation complete")) - .catch((error) => console.error(`Error: ${error.message}`)) -``` - -# Why This Works - -| Concept | Explanation | -|---------|-------------| -| **Database query during parse** | Validate constraints at schema layer | -| **Foreign key checks** | Ensure referenced entities exist | -| **Uniqueness constraints** | Prevent duplicates in database | -| **Consistent validation** | All instances validated same way | -| **Error clarity** | Clear messages if constraints violated | -| **Type safety** | Decoded value guaranteed to pass all checks | - -# When to Use - -- Unique constraints (username, email, slug) -- Foreign key validation -- Reference existence checks -- Duplicate detection -- Business rule validation via database -- Permission checks - -# Related Patterns - -- [Basic Async](./basic-async.md) -- [External API Validation](./external-api-validation.md) -- [Batched Async](./batched-async.md) diff --git a/content/published/patterns/schema/async-validation/external-api-validation.mdx b/content/published/patterns/schema/async-validation/external-api-validation.mdx deleted file mode 100644 index 6460b5ce..00000000 --- a/content/published/patterns/schema/async-validation/external-api-validation.mdx +++ /dev/null @@ -1,264 +0,0 @@ ---- -id: schema-external-api-validation -title: External API Validation During Schema Parsing -category: async-validation -skillLevel: intermediate -tags: - - schema - - async-validation - - external-api - - integration - - third-party -lessonOrder: 15 -rule: - description: >- - External API Validation During Schema Parsing. -summary: >- - Your system integrates with external APIs (payment processor, geocoding, IP reputation). You need to validate user input against those services during parsing. A card must be valid with the payment... ---- - -# Problem - -Your system integrates with external APIs (payment processor, geocoding, IP reputation). You need to validate user input against those services during parsing. A card must be valid with the payment processor. A shipping address must be valid per geocoding API. An IP must not be flagged as malicious. - -# Solution - -```typescript -import { Schema, Effect } from "effect" - -// ============================================ -// 1. Simulated external APIs -// ============================================ - -class PaymentGateway { - async validateCard(cardNumber: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 200)) - // Simulate: cards starting with 4 are valid - return cardNumber.startsWith("4") - } - - async checkCardBalance(cardNumber: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 150)) - return 5000 - } -} - -class GeocodingService { - async validateAddress(address: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 300)) - // Simulate: addresses with "St." are valid - return address.includes("St.") - } -} - -class SecurityService { - async checkIPReputation(ip: string): Promise<{ safe: boolean; score: number }> { - await new Promise((resolve) => setTimeout(resolve, 250)) - // Simulate: IPs starting with 192 are suspicious - return { - safe: !ip.startsWith("192"), - score: ip.startsWith("192") ? 80 : 20, - } - } -} - -// ============================================ -// 2. Create service instances -// ============================================ - -const paymentGateway = new PaymentGateway() -const geocoding = new GeocodingService() -const security = new SecurityService() - -// ============================================ -// 3. Schemas with external validation -// ============================================ - -const ValidCreditCard = Schema.String.pipe( - Schema.regex(/^\d{16}$/), - Schema.filterEffect((cardNumber) => - Effect.gen(function* () { - const isValid = yield* Effect.tryPromise({ - try: () => paymentGateway.validateCard(cardNumber), - catch: () => false, - }) - - if (!isValid) { - return yield* Effect.fail( - new Error(`Credit card is invalid or not supported`) - ) - } - - return cardNumber - }) - ) -) - -const ValidShippingAddress = Schema.String.pipe( - Schema.check(Schema.isMinLength(10)), - Schema.filterEffect((address) => - Effect.gen(function* () { - const isValid = yield* Effect.tryPromise({ - try: () => geocoding.validateAddress(address), - catch: () => false, - }) - - if (!isValid) { - return yield* Effect.fail( - new Error(`Address could not be validated by geocoding service`) - ) - } - - return address - }) - ) -) - -const SafeIPAddress = Schema.String.pipe( - Schema.regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/), - Schema.filterEffect((ip) => - Effect.gen(function* () { - const reputation = yield* Effect.tryPromise({ - try: () => security.checkIPReputation(ip), - catch: () => ({ safe: false, score: 100 }), - }) - - if (!reputation.safe) { - return yield* Effect.fail( - new Error( - `IP address flagged as suspicious (score: ${reputation.score}/100)` - ) - ) - } - - return ip - }) - ) -) - -// ============================================ -// 4. Forms with external validation -// ============================================ - -const PaymentForm = Schema.Struct({ - cardNumber: ValidCreditCard, - amount: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0))), - shippingAddress: ValidShippingAddress, -}) - -const LoginForm = Schema.Struct({ - email: Schema.String, - password: Schema.String, - userIp: SafeIPAddress, -}) - -// ============================================ -// 5. Application logic -// ============================================ - -const appLogic = Effect.gen(function* () { - console.log("=== External API Validation ===\n") - - console.log("1. Valid payment (card + address):\n") - - const validPayment = { - cardNumber: "4532123456789012", - amount: 99.99, - shippingAddress: "123 Main St.", - } - - const payment = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(PaymentForm)(validPayment), - catch: (e) => new Error(String(e)), - }) - - console.log(`βœ“ Payment processed`) - console.log(` Card: ****${payment.cardNumber.slice(-4)}`) - console.log(` Amount: $${payment.amount}`) - console.log(` Address: ${payment.shippingAddress}`) - - console.log("\n2. Invalid credit card:\n") - - const invalidCard = { - cardNumber: "5532123456789012", - amount: 50.0, - shippingAddress: "456 Oak St.", - } - - const payment2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(PaymentForm)(invalidCard), - catch: (e) => new Error(String(e)), - }).pipe(Effect.result) - - if (payment2._tag === "Failure") { - console.log(`βœ— Error: ${payment2.failure.message}`) - } - - console.log("\n3. Valid login (safe IP):\n") - - const validLogin = { - email: "user@example.com", - password: "password123", - userIp: "203.0.113.45", - } - - const login = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(LoginForm)(validLogin), - catch: (e) => new Error(String(e)), - }) - - console.log(`βœ“ Login successful`) - console.log(` User: ${login.email}`) - console.log(` IP: ${login.userIp}`) - - console.log("\n4. Suspicious IP:\n") - - const suspiciousLogin = { - email: "attacker@example.com", - password: "wrong", - userIp: "192.168.1.1", - } - - const login2 = yield* Effect.tryPromise({ - try: () => Schema.decodeUnknownEffect(LoginForm)(suspiciousLogin), - catch: (e) => new Error(String(e)), - }).pipe(Effect.result) - - if (login2._tag === "Failure") { - console.log(`βœ— Error: ${login2.failure.message}`) - } - - return { payment, login } -}) - -// Run application -Effect.runPromise(appLogic) - .then(() => console.log("\nβœ… External API validation complete")) - .catch((error) => console.error(`Error: ${error.message}`)) -``` - -# Why This Works - -| Concept | Explanation | -|---------|-------------| -| **API calls during parse** | Validate against external services | -| **Error propagation** | API errors become validation errors | -| **Type safety** | Decoded value guaranteed valid by API | -| **Declarative** | Validation logic with schema definition | -| **Composable** | Chain with other validators | - -# When to Use - -- Payment card validation -- Geocoding/address validation -- IP reputation checks -- Email verification APIs -- Rate limit checking -- License validation -- Third-party authentication - -# Related Patterns - -- [Basic Async](./basic-async.md) -- [Database Checks](./database-checks.md) -- [Batched Async](./batched-async.md) diff --git a/content/published/patterns/schema/form-validation/async-validation.mdx b/content/published/patterns/schema/form-validation/async-validation.mdx deleted file mode 100644 index 2f440825..00000000 --- a/content/published/patterns/schema/form-validation/async-validation.mdx +++ /dev/null @@ -1,218 +0,0 @@ ---- -id: schema-form-async-validation -title: Async Validation (Username Availability) -category: form-validation -skillLevel: intermediate -tags: - - schema - - form - - async - - validation - - api-check - - debounce -lessonOrder: 2 -rule: - description: >- - Async Validation (Username Availability) using Schema. -summary: >- - Some validation requires checking a server: is this username available? Is this email already registered? Standard schema validation is synchronousβ€”you can't check a database or API. You need to... ---- - -# Problem - -Some validation requires checking a server: is this username available? Is this email already registered? Standard schema validation is synchronousβ€”you can't check a database or API. You need to validate form fields asynchronously after sync validation passes, then combine results. - -# Solution - -```typescript -import { Effect, Schema, Schedule } from "effect" - -// 1. Sync validation: format only -const Username = Schema.String.pipe( - Schema.check(Schema.isMinLength(3)), - Schema.check(Schema.isMaxLength(20)), - Schema.check(Schema.isPattern(/^[a-zA-Z0-9_-]+$/)) -) - -type Username = typeof Username.Type - -// 2. Async check: database lookup -const checkUsernameAvailable = ( - username: Username -): Effect.Effect => - Effect.gen(function* () { - // Simulate API call to check availability - const response = yield* Effect.tryPromise({ - try: () => - fetch( - `/api/check-username?username=${username}` - ).then((r) => r.json()), - catch: (error) => new Error(`API failed: ${error}`), - }) - - const available = response.available === true - - if (!available) { - return yield* Effect.fail( - new Error(`Username "${username}" is taken`) - ) - } - - return available - }) - -// 3. Email uniqueness check -const checkEmailAvailable = (email: string) => - Effect.gen(function* () { - const response = yield* Effect.tryPromise({ - try: () => - fetch(`/api/check-email?email=${email}`).then( - (r) => r.json() - ), - catch: (error) => new Error(`API failed: ${error}`), - }) - - if (!response.available) { - return yield* Effect.fail( - new Error( - `Email "${email}" is already registered` - ) - ) - } - - return true - }) - -// 4. Combined validation: sync then async -const validateSignUp = (input: unknown) => - Effect.gen(function* () { - // Step 1: Sync validation - const syncData = yield* Schema.decodeUnknownEffect( - Schema.Struct({ - username: Username, - email: Schema.String.pipe( - Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) - ), - password: Schema.String.pipe( - Schema.check(Schema.isMinLength(8)) - ), - }) - )(input).pipe( - Effect.mapError((error) => ({ - _tag: "SyncError" as const, - message: `Invalid input: ${error.message}`, - })) - ) - - // Step 2: Async validation (in parallel) - const [usernameOk, emailOk] = yield* Effect.all([ - checkUsernameAvailable(syncData.username).pipe( - Effect.mapError((error) => ({ - _tag: "UsernameError" as const, - message: error.message, - })) - ), - checkEmailAvailable(syncData.email).pipe( - Effect.mapError((error) => ({ - _tag: "EmailError" as const, - message: error.message, - })) - ), - ]) - - return { ...syncData, usernameOk, emailOk } - }) - -// 5. With debounce for real-time UI checking -const validateUsernameRealtime = ( - username: string -): Effect.Effect => - Effect.gen(function* () { - // Sync validation first - yield* Schema.decodeUnknownEffect(Username)( - username - ).pipe( - Effect.mapError((error) => - new Error(`Invalid format: ${error.message}`) - ) - ) - - // Debounce async check: wait 500ms before API call - yield* Effect.sleep("500 millis") - - // Async availability check - const available = yield* checkUsernameAvailable( - username as Username - ) - - return available - }) - -// 6. Usage -const signupData = { - username: "alice_123", - email: "alice@example.com", - password: "securepass123", -} - -Effect.runPromise(validateSignUp(signupData)) - .then((result) => { - console.log("βœ… Sign up valid!") - console.log(result) - }) - .catch((error) => { - console.error(`❌ Validation failed:`, error.message) - }) - -// 7. Real-time checking with debounce -const checkUsernameField = (username: string) => - validateUsernameRealtime(username) - .pipe( - Effect.timeout("2 seconds"), // Timeout if API is slow - Effect.match({ - onSuccess: (available) => ({ - valid: available, - message: "Username available!", - }), - onFailure: (error) => ({ - valid: false, - message: error.message, - }), - }) - ) - .pipe(Effect.runPromise) - -// Triggered on user input after 500ms of inactivity -export const onUsernameChange = (value: string) => { - checkUsernameField(value).then((result) => { - console.log(result) - // Update UI with result - }) -} -``` - -# Why This Works - -| Concept | Explanation | -|---------|-------------| -| Sync first | Fast validation catches obvious errors before API call | -| Async second | Check database/API for uniqueness | -| `Effect.all` | Parallel async checks (username AND email in parallel) | -| `Effect.sleep` | Debounce rapid input (wait 500ms before API call) | -| `Effect.timeout` | Prevent hanging if API is slow | -| Errors in channel | No thrown exceptions, typed error handling | - -# When to Use - -- Username/email availability checks -- Real-time field validation with API calls -- Cross-domain duplicate checks -- Any validation requiring external data -- UI feedback while user is typing - -# Related Patterns - -- [Basic Form Validation](./basic.md) -- [Collecting All Validation Errors](./collect-all-errors.md) -- [Dependent Field Validation](./dependent-fields.md) -- [Nested Form Structures](./nested-forms.md) diff --git a/content/published/patterns/schema/getting-started/schema-vs-zod.mdx b/content/published/patterns/schema/getting-started/schema-vs-zod.mdx index f141d549..520df2e4 100644 --- a/content/published/patterns/schema/getting-started/schema-vs-zod.mdx +++ b/content/published/patterns/schema/getting-started/schema-vs-zod.mdx @@ -116,12 +116,15 @@ const parseAsync = Schema.decodeUnknownEffect(User) // 3. DEFECTS VS ERRORS: Schema distinguishes recoverable vs non-recoverable // ParseError is typed and trackable through Effect's error channel -// 4. ASYNC VALIDATION: Built-in support +// 4. ASYNC VALIDATION: compose sync decode + async Effect checks +import { SchemaGetter } from "effect" const UsernameAvailable = Schema.String.pipe( - Schema.filterEffect((username) => - // Check database asynchronously - Effect.succeed(true) // Placeholder - ) + Schema.decodeTo(Schema.String, { + decode: SchemaGetter.transformEffect((username) => + // Check database asynchronously + Effect.succeed(username) + ) + }) ) console.log("βœ… Effect Schema and Zod both validate at runtime") diff --git a/content/published/patterns/schema/json-validation/database-columns.mdx b/content/published/patterns/schema/json-validation/database-columns.mdx index 9ccc6d03..65815b4a 100644 --- a/content/published/patterns/schema/json-validation/database-columns.mdx +++ b/content/published/patterns/schema/json-validation/database-columns.mdx @@ -29,13 +29,13 @@ import { Schema, Effect } from "effect"; // 1. Define schema for database JSON column const UserMetadata = Schema.Struct({ theme: Schema.Literals(["light", "dark", "auto"]).pipe( - Schema.optionalWith({ default: () => "auto" }) + Schema.withDecodingDefaultKey(Effect.succeed("auto")) ), notifications: Schema.Boolean.pipe( - Schema.optionalWith({ default: () => true }) + Schema.withDecodingDefaultKey(Effect.succeed(true)) ), language: Schema.Literals(["en", "es", "fr", "de"]).pipe( - Schema.optionalWith({ default: () => "en" }) + Schema.withDecodingDefaultKey(Effect.succeed("en")) ), lastLogin: Schema.String.pipe( Schema.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/)) @@ -150,7 +150,7 @@ const validateMultipleMetadata = (userIds: string[]) => | Schema as contract | Define shape of JSON before retrieving from DB | | `Schema.decodeUnknownEffect()` | Convert untyped database JSON to typed data | | Error channel | Validation failures don't throw; flow through Effect | -| Defaults via `optionalWith` | Apply missing-field defaults at parse time | +| Defaults via `withDecodingDefaultKey` | Apply missing-field defaults at parse time | | Type-safe after validation | TypeScript knows validated data matches schema | | Immediate validation | Catch bad data at DB boundary, not downstream | | Composable schemas | Nested structures validated automatically | diff --git a/content/published/patterns/schema/json-validation/multiple-files.mdx b/content/published/patterns/schema/json-validation/multiple-files.mdx index bee96f61..42bb61ce 100644 --- a/content/published/patterns/schema/json-validation/multiple-files.mdx +++ b/content/published/patterns/schema/json-validation/multiple-files.mdx @@ -46,7 +46,7 @@ const ApiConfig = Schema.Struct({ timeout: Schema.Number.pipe( Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0)), - Schema.optionalWith({ default: () => 5000 }) + Schema.withDecodingDefaultKey(Effect.succeed(5000)) ), }); @@ -54,13 +54,13 @@ type ApiConfig = typeof ApiConfig.Type; const FeatureFlags = Schema.Struct({ enableNewUI: Schema.Boolean.pipe( - Schema.optionalWith({ default: () => false }) + Schema.withDecodingDefaultKey(Effect.succeed(false)) ), enableAnalytics: Schema.Boolean.pipe( - Schema.optionalWith({ default: () => true }) + Schema.withDecodingDefaultKey(Effect.succeed(true)) ), maintenanceMode: Schema.Boolean.pipe( - Schema.optionalWith({ default: () => false }) + Schema.withDecodingDefaultKey(Effect.succeed(false)) ), }); diff --git a/content/published/patterns/schema/json-validation/postgres-jsonb.mdx b/content/published/patterns/schema/json-validation/postgres-jsonb.mdx index 24985ecf..b48a19a4 100644 --- a/content/published/patterns/schema/json-validation/postgres-jsonb.mdx +++ b/content/published/patterns/schema/json-validation/postgres-jsonb.mdx @@ -59,7 +59,7 @@ const OrderMetadata = Schema.Struct({ // Metadata with defaults source: Schema.Literals(["web", "mobile", "api"]).pipe( - Schema.optionalWith({ default: () => "web" }) + Schema.withDecodingDefaultKey(Effect.succeed("web")) ), notes: Schema.String.pipe(Schema.optional), }); diff --git a/content/published/patterns/schema/json-validation/schema-evolution.mdx b/content/published/patterns/schema/json-validation/schema-evolution.mdx index 784346e3..574e3e51 100644 --- a/content/published/patterns/schema/json-validation/schema-evolution.mdx +++ b/content/published/patterns/schema/json-validation/schema-evolution.mdx @@ -57,7 +57,7 @@ const UserProfileV3 = Schema.Struct({ createdAt: Schema.String, updatedAt: Schema.String.pipe(Schema.optional), schemaVersion: Schema.Literals(["v1", "v2", "v3"]).pipe( - Schema.optionalWith({ default: () => "v3" }) + Schema.withDecodingDefaultKey(Effect.succeed("v3")) ), }); diff --git a/content/published/patterns/schema/json-validation/with-defaults.mdx b/content/published/patterns/schema/json-validation/with-defaults.mdx index 6d24ccc2..2ab03b98 100644 --- a/content/published/patterns/schema/json-validation/with-defaults.mdx +++ b/content/published/patterns/schema/json-validation/with-defaults.mdx @@ -38,20 +38,20 @@ const ServerConfig = Schema.Struct({ timeout: Schema.Number.pipe( Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0)), - Schema.optionalWith({ default: () => 5000 }) // 5 second default + Schema.withDecodingDefaultKey(Effect.succeed(5000)) // 5 second default ), maxRetries: Schema.Number.pipe( Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(0, 10)), - Schema.optionalWith({ default: () => 3 }) + Schema.withDecodingDefaultKey(Effect.succeed(3)) ), enableSSL: Schema.Boolean.pipe( - Schema.optionalWith({ default: () => false }) + Schema.withDecodingDefaultKey(Effect.succeed(false)) ), compressionLevel: Schema.Number.pipe( Schema.check(Schema.isInt()), Schema.check(Schema.isBetween(0, 9)), - Schema.optionalWith({ default: () => 6 }) // Mid-range compression + Schema.withDecodingDefaultKey(Effect.succeed(6)) // Mid-range compression ), }); @@ -161,7 +161,7 @@ Effect.runPromise( | Concept | Explanation | |---------|-------------| -| `Schema.optionalWith({ default: () => value })` | Applies default when field is missing | +| `Schema.withDecodingDefaultKey(Effect.succeed(value))` | Applies default when field is missing | | Default factory function `() => value` | Computed at parse time (not shared mutable state) | | Type becomes non-optional | TypeScript knows field always exists | | Eliminates null/undefined checks | Config properties guaranteed to exist | diff --git a/content/published/patterns/schema/objects/optional-fields.mdx b/content/published/patterns/schema/objects/optional-fields.mdx index bbb88ae7..cc6c6d17 100644 --- a/content/published/patterns/schema/objects/optional-fields.mdx +++ b/content/published/patterns/schema/objects/optional-fields.mdx @@ -23,7 +23,7 @@ Not all fields are required. Some are optional (may be missing), some are nullab # Solution ```typescript -import { Schema } from "effect" +import { Schema, Effect } from "effect" // ============================================ // OPTIONAL FIELDS (may be missing) @@ -122,9 +122,9 @@ console.log(`Nickname: ${hasNickname.nickname}`) // "Ace" // ============================================ const Settings = Schema.Struct({ - theme: Schema.optionalWith(Schema.String, { default: () => "light" }), - pageSize: Schema.optionalWith(Schema.Number, { default: () => 20 }), - notifications: Schema.optionalWith(Schema.Boolean, { default: () => true }), + theme: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("light"))), + pageSize: Schema.Number.pipe(Schema.withDecodingDefaultKey(Effect.succeed(20))), + notifications: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))), }) type Settings = typeof Settings.Type @@ -147,7 +147,7 @@ console.log(`βœ… Theme: ${customSettings.theme}, Page: ${customSettings.pageSize | **optional(S)** | `T \| undefined` | Field may be omitted | | **NullOr(S)** | `T \| null` | Field present but nullable | | **optional(NullOr(S))** | `T \| null \| undefined` | Both patterns | -| **optionalWith(S, {default})** | `T` | Default when missing | +| **withDecodingDefaultKey(Effect.succeed(default))** | `T` | Default when missing | # When to Use diff --git a/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx b/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx index 13806705..e690b691 100644 --- a/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx +++ b/content/published/patterns/tooling-and-debugging/tooling-devtools.mdx @@ -39,7 +39,7 @@ Effect DevTools help you: ### 1. Enable Debug Mode ```typescript -import { Effect, Logger, LogLevel, FiberRef, Cause } from "effect" +import { Effect, Logger, LogLevel, References, Cause } from "effect" // ============================================ // 1. Verbose logging for development @@ -124,9 +124,9 @@ const debugErrors = Effect.gen(function* () { yield* Effect.log(`Is failure: ${Cause.hasFails(cause)}`) yield* Effect.log(`Is interrupted: ${Cause.hasInterrupts(cause)}`) - // Extract all failures - const failures = Cause.failures(cause) - yield* Effect.log(`Failures: ${JSON.stringify([...failures])}`) + // Extract all failures (v4: Cause is a flat reasons array) + const failures = cause.reasons.filter(Cause.isFailReason).map((r) => r.error) + yield* Effect.log(`Failures: ${JSON.stringify(failures)}`) return "recovered" }) @@ -176,13 +176,14 @@ const withDevLogger = (effect: Effect.Effect) => // ============================================ const showRuntimeMetrics = Effect.gen(function* () { - const runtime = yield* Effect.runtime() + const services = yield* Effect.context() yield* Effect.log("=== Runtime Info ===") - // Access runtime configuration - const fiberRefs = runtime.fiberRefs - - yield* Effect.log("FiberRefs available") + // v4: FiberRef / Runtime removed. Fiber-local state lives in Context.Reference. + // Read built-in references directly, e.g. `yield* References.CurrentLogLevel`. + const level = yield* References.CurrentLogLevel + yield* Effect.log(`Current log level: ${level.label}`) + void services }) // ============================================