From 8da3f604c800171b7547ad298acf11a2c10fcc23 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 10 Mar 2025 13:04:24 +1300 Subject: [PATCH 1/5] remove Scope from HttpClient requirements --- .changeset/lucky-shrimps-refuse.md | 7 + .../test/BrowserHttpClient.test.ts | 7 +- .../platform-node/test/HttpClient.test.ts | 23 +- .../platform-node/test/HttpServer.test.ts | 79 +++---- packages/platform/src/HttpApiClient.ts | 10 +- packages/platform/src/HttpClient.ts | 24 +- packages/platform/src/HttpClientResponse.ts | 3 +- packages/platform/src/internal/httpClient.ts | 217 +++++++++++++----- .../src/internal/httpClientResponse.ts | 2 +- packages/platform/test/HttpClient.test.ts | 30 +-- 10 files changed, 239 insertions(+), 163 deletions(-) create mode 100644 .changeset/lucky-shrimps-refuse.md diff --git a/.changeset/lucky-shrimps-refuse.md b/.changeset/lucky-shrimps-refuse.md new file mode 100644 index 00000000000..4bf1f6977e8 --- /dev/null +++ b/.changeset/lucky-shrimps-refuse.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-browser": minor +"@effect/platform-node": minor +"@effect/platform": minor +--- + +remove Scope from HttpClient requirements diff --git a/packages/platform-browser/test/BrowserHttpClient.test.ts b/packages/platform-browser/test/BrowserHttpClient.test.ts index 24b3d290868..cc961be4c7f 100644 --- a/packages/platform-browser/test/BrowserHttpClient.test.ts +++ b/packages/platform-browser/test/BrowserHttpClient.test.ts @@ -16,8 +16,7 @@ describe("BrowserHttpClient", () => { it.effect("json", () => Effect.gen(function*() { const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( - Effect.flatMap((_) => _.json), - Effect.scoped + Effect.flatMap((_) => _.json) ) assert.deepStrictEqual(body, { message: "Success!" }) }).pipe(Effect.provide(layer({ @@ -50,8 +49,7 @@ describe("BrowserHttpClient", () => { it.effect("cookies", () => Effect.gen(function*() { const cookies = yield* HttpClient.get("http://localhost:8080/my/url").pipe( - Effect.map((res) => res.cookies), - Effect.scoped + Effect.map((res) => res.cookies) ) assert.deepStrictEqual(Cookies.toRecord(cookies), { foo: "bar" @@ -69,7 +67,6 @@ describe("BrowserHttpClient", () => { Effect.gen(function*() { const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( Effect.flatMap((_) => _.arrayBuffer), - Effect.scoped, BrowserHttpClient.withXHRArrayBuffer ) assert.strictEqual(new TextDecoder().decode(body), "{ \"message\": \"Success!\" }") diff --git a/packages/platform-node/test/HttpClient.test.ts b/packages/platform-node/test/HttpClient.test.ts index 29768f8a0b2..0227072636b 100644 --- a/packages/platform-node/test/HttpClient.test.ts +++ b/packages/platform-node/test/HttpClient.test.ts @@ -27,8 +27,7 @@ const makeJsonPlaceholder = Effect.gen(function*(_) { HttpClientRequest.post("/todos").pipe( HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo), Effect.flatMap(client.execute), - Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) return { client, @@ -53,8 +52,7 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) Effect.gen(function*(_) { const response = yield* _( HttpClient.get("https://www.google.com/"), - Effect.flatMap((_) => _.text), - Effect.scoped + Effect.flatMap((_) => _.text) ) expect(response).toContain("Google") }).pipe(Effect.provide(layer))) @@ -65,8 +63,7 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) HttpClient.followRedirects() ) const response = yield* client.get("http://google.com/").pipe( - Effect.flatMap((_) => _.text), - Effect.scoped + Effect.flatMap((_) => _.text) ) expect(response).toContain("Google") }).pipe(Effect.provide(layer))) @@ -86,8 +83,7 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) Effect.gen(function*() { const jp = yield* JsonPlaceholder const response = yield* jp.client.get("/todos/1").pipe( - Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) expect(response.id).toBe(1) }).pipe(Effect.provide(JsonPlaceholderLive.pipe( @@ -113,8 +109,7 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) const response = yield* client.head("https://jsonplaceholder.typicode.com/todos").pipe( Effect.flatMap( HttpClientResponse.schemaJson(Schema.Struct({ status: Schema.Literal(200) })) - ), - Effect.scoped + ) ) expect(response).toEqual({ status: 200 }) }).pipe(Effect.provide(layer))) @@ -124,7 +119,6 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) const client = yield* HttpClient.HttpClient const response = yield* client.get("https://www.google.com/").pipe( Effect.flatMap((_) => _.text), - Effect.scoped, Effect.timeout(1), Effect.asSome, Effect.catchTag("TimeoutException", () => Effect.succeedNone) @@ -133,11 +127,8 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) }).pipe(Effect.provide(layer))) it.effect("close early", () => - Effect.gen(function*(_) { - const response = yield* _( - HttpClient.get("https://www.google.com/"), - Effect.scoped - ) + Effect.gen(function*() { + const response = yield* HttpClient.get("https://www.google.com/") expect(response.status).toBe(200) }).pipe(Effect.provide(layer))) }) diff --git a/packages/platform-node/test/HttpServer.test.ts b/packages/platform-node/test/HttpServer.test.ts index ee622e45969..5ae3117b219 100644 --- a/packages/platform-node/test/HttpServer.test.ts +++ b/packages/platform-node/test/HttpServer.test.ts @@ -47,8 +47,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const todo = yield* HttpClient.get("/todos/1").pipe( - Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) expect(todo).toEqual({ id: 1, title: "test" }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -76,8 +75,7 @@ describe("HttpServer", () => { const formData = new FormData() formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") const result = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( - Effect.flatMap((r) => r.json), - Effect.scoped + Effect.flatMap((r) => r.json) ) expect(result).toEqual({ ok: true }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -104,9 +102,7 @@ describe("HttpServer", () => { const formData = new FormData() formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") formData.append("test", "test") - const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( - Effect.scoped - ) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -132,9 +128,7 @@ describe("HttpServer", () => { const formData = new FormData() const data = new Uint8Array(1000) formData.append("file", new Blob([data], { type: "text/plain" }), "test.txt") - const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( - Effect.scoped - ) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) expect(response.status).toEqual(413) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -160,9 +154,7 @@ describe("HttpServer", () => { const formData = new FormData() const data = new Uint8Array(1000).fill(1) formData.append("file", new TextDecoder().decode(data)) - const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe( - Effect.scoped - ) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) expect(response.status).toEqual(413) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -177,9 +169,9 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text), Effect.scoped) + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text)) expect(todo).toEqual("/1") - const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text), Effect.scoped) + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text)) expect(root).toEqual("/") }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -194,9 +186,9 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text), Effect.scoped) + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text)) expect(todo).toEqual("/1") - const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text), Effect.scoped) + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text)) expect(root).toEqual("/") }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -217,9 +209,9 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text), Effect.scoped) + const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text)) expect(todo).toEqual("/child/1") - const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text), Effect.scoped) + const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text)) expect(root).toEqual("/child") }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -245,7 +237,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* client.get("/").pipe(Effect.scoped) + const res = yield* client.get("/") expect(res.status).toEqual(200) expect(res.headers["content-type"]).toEqual("text/plain") expect(res.headers["content-length"]).toEqual("27") @@ -276,7 +268,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* client.get("/").pipe(Effect.scoped) + const res = yield* client.get("/") expect(res.status).toEqual(200) expect(res.headers["content-type"]).toEqual("text/plain") expect(res.headers["content-length"]).toEqual("4") @@ -304,8 +296,7 @@ describe("HttpServer", () => { const todo = yield* HttpClientRequest.post("/todos").pipe( HttpClientRequest.bodyUrlParams({ id: "1", title: "test" }), HttpClient.execute, - Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) expect(todo).toEqual({ id: 1, title: "test" }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -327,7 +318,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const response = yield* client.get("/todos").pipe(Effect.scoped) + const response = yield* client.get("/todos") expect(response.status).toEqual(400) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -350,7 +341,7 @@ describe("HttpServer", () => { const client = yield* HttpClient.HttpClient const formData = new FormData() formData.append("json", JSON.stringify({ test: "content" })) - const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe(Effect.scoped) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -378,7 +369,7 @@ describe("HttpServer", () => { new Blob([JSON.stringify({ test: "content" })], { type: "application/json" }), "test.json" ) - const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe(Effect.scoped) + const response = yield* client.post("/upload", { body: HttpBody.formData(formData) }) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -403,7 +394,7 @@ describe("HttpServer", () => { body: HttpBody.urlParams(UrlParams.fromInput({ json: JSON.stringify({ test: "content" }) })) - }).pipe(Effect.scoped) + }) expect(response.status).toEqual(204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -423,7 +414,6 @@ describe("HttpServer", () => { const requestSpan = yield* Effect.makeSpan("client request") const body = yield* client.get("/").pipe( Effect.flatMap((r) => r.json), - Effect.scoped, Effect.withTracer(Tracer.make({ span(name, parent, _, __, ___, kind) { assert.strictEqual(name, "http.client GET") @@ -451,7 +441,7 @@ describe("HttpServer", () => { HttpServer.serveEffect((app) => Effect.onExit(app, (exit) => Deferred.complete(latch, exit))) ) const client = yield* HttpClient.HttpClient - const fiber = yield* client.get("/").pipe(Effect.scoped, Effect.fork) + const fiber = yield* client.get("/").pipe(Effect.fork) yield* Effect.sleep(100) yield* Fiber.interrupt(fiber) const cause = yield* Deferred.await(latch).pipe(Effect.sandbox, Effect.flip) @@ -474,8 +464,7 @@ describe("HttpServer", () => { HttpClientRequest.setHeader("host", "a.example.com") ) ).pipe( - Effect.flatMap((r) => r.text), - Effect.scoped + Effect.flatMap((r) => r.text) ) ).toEqual("A") expect( @@ -484,8 +473,7 @@ describe("HttpServer", () => { HttpClientRequest.setHeader("host", "b.example.com") ) ).pipe( - Effect.flatMap((r) => r.text), - Effect.scoped + Effect.flatMap((r) => r.text) ) ).toEqual("B") expect( @@ -494,8 +482,7 @@ describe("HttpServer", () => { HttpClientRequest.setHeader("host", "b.org") ) ).pipe( - Effect.flatMap((r) => r.text), - Effect.scoped + Effect.flatMap((r) => r.text) ) ).toEqual("B") expect( @@ -504,8 +491,7 @@ describe("HttpServer", () => { HttpClientRequest.setHeader("host", "c.example.com") ) ).pipe( - Effect.flatMap((r) => r.text), - Effect.scoped + Effect.flatMap((r) => r.text) ) ).toEqual("C") }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -525,11 +511,11 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const home = yield* client.get("/home").pipe(Effect.flatMap((r) => r.text), Effect.scoped) + const home = yield* client.get("/home").pipe(Effect.flatMap((r) => r.text)) expect(home).toEqual("") - const about = yield* client.get("/about").pipe(Effect.flatMap((r) => r.text), Effect.scoped) + const about = yield* client.get("/about").pipe(Effect.flatMap((r) => r.text)) expect(about).toEqual("") - const stream = yield* client.get("/stream").pipe(Effect.flatMap((r) => r.text), Effect.scoped) + const stream = yield* client.get("/stream").pipe(Effect.flatMap((r) => r.text)) expect(stream).toEqual("123hello") }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -555,7 +541,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* client.get("/home").pipe(Effect.scoped) + const res = yield* client.get("/home") assert.deepStrictEqual( res.cookies.toJSON(), Cookies.fromReadonlyRecord({ @@ -589,7 +575,7 @@ describe("HttpServer", () => { HttpServer.serveEffect() ) const client = yield* HttpClient.HttpClient - const res = yield* client.get("/home").pipe(Effect.scoped) + const res = yield* client.get("/home") assert.strictEqual(res.status, 204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -598,7 +584,7 @@ describe("HttpServer", () => { Effect.gen(function*() { yield* HttpRouter.empty.pipe(HttpServer.serveEffect()) const client = yield* HttpClient.HttpClient - const res = yield* client.get("/").pipe(Effect.scoped) + const res = yield* client.get("/") assert.strictEqual(res.status, 404) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -637,8 +623,7 @@ describe("HttpServer", () => { ) const client = yield* HttpClient.HttpClient const res = yield* client.get("/user").pipe( - Effect.flatMap(HttpClientResponse.schemaBodyJson(User)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(User)) ) assert.deepStrictEqual(res, new User({ name: "test" })) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -701,9 +686,9 @@ describe("HttpServer", () => { HttpRouter.get("/:param", HttpServerResponse.empty()), HttpServer.serveEffect() ) - let res = yield* HttpClient.get("/123456").pipe(Effect.scoped) + let res = yield* HttpClient.get("/123456") assert.strictEqual(res.status, 404) - res = yield* HttpClient.get("/12345").pipe(Effect.scoped) + res = yield* HttpClient.get("/12345") assert.strictEqual(res.status, 204) }).pipe( Effect.provide(NodeHttpServer.layerTest), diff --git a/packages/platform/src/HttpApiClient.ts b/packages/platform/src/HttpApiClient.ts index f9322f4513f..4eb98c5ba2c 100644 --- a/packages/platform/src/HttpApiClient.ts +++ b/packages/platform/src/HttpApiClient.ts @@ -10,7 +10,6 @@ import * as ParseResult from "effect/ParseResult" import type * as Predicate from "effect/Predicate" import * as Schema from "effect/Schema" import type * as AST from "effect/SchemaAST" -import type { Scope } from "effect/Scope" import type { Simplify } from "effect/Types" import * as HttpApi from "./HttpApi.js" import type { HttpApiEndpoint } from "./HttpApiEndpoint.js" @@ -130,7 +129,7 @@ const makeClient = void readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined readonly transformResponse?: - | ((effect: Effect.Effect) => Effect.Effect) + | ((effect: Effect.Effect) => Effect.Effect) | undefined readonly baseUrl?: string | undefined } @@ -223,7 +222,6 @@ const makeClient = Context.merge(context, input)) ) @@ -244,7 +242,7 @@ export const make = HttpClient.HttpClient) | undefined readonly transformResponse?: - | ((effect: Effect.Effect) => Effect.Effect) + | ((effect: Effect.Effect) => Effect.Effect) | undefined readonly baseUrl?: string | undefined } @@ -282,7 +280,7 @@ export const group = < options?: { readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined readonly transformResponse?: - | ((effect: Effect.Effect) => Effect.Effect) + | ((effect: Effect.Effect) => Effect.Effect) | undefined readonly baseUrl?: string | undefined } @@ -324,7 +322,7 @@ export const endpoint = < options?: { readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined readonly transformResponse?: - | ((effect: Effect.Effect) => Effect.Effect) + | ((effect: Effect.Effect) => Effect.Effect) | undefined readonly baseUrl?: string | undefined } diff --git a/packages/platform/src/HttpClient.ts b/packages/platform/src/HttpClient.ts index 25193a84c8c..8afd2b325ab 100644 --- a/packages/platform/src/HttpClient.ts +++ b/packages/platform/src/HttpClient.ts @@ -11,7 +11,6 @@ import type { Pipeable } from "effect/Pipeable" import type * as Predicate from "effect/Predicate" import type { Ref } from "effect/Ref" import type * as Schedule from "effect/Schedule" -import type * as Scope from "effect/Scope" import type { NoExcessProperties, NoInfer } from "effect/Types" import type { Cookies } from "./Cookies.js" import type * as Error from "./HttpClientError.js" @@ -35,7 +34,7 @@ export type TypeId = typeof TypeId * @since 1.0.0 * @category models */ -export interface HttpClient extends HttpClient.With {} +export interface HttpClient extends HttpClient.With {} /** * @since 1.0.0 @@ -45,7 +44,7 @@ export declare namespace HttpClient { * @since 1.0.0 * @category models */ - export interface With extends Pipeable, Inspectable { + export interface With extends Pipeable, Inspectable { readonly [TypeId]: TypeId readonly execute: ( request: ClientRequest.HttpClientRequest @@ -110,8 +109,7 @@ export const HttpClient: Context.Tag = internal.tag */ export const execute: ( request: ClientRequest.HttpClientRequest -) => Effect.Effect = - internal.execute +) => Effect.Effect = internal.execute /** * @since 1.0.0 @@ -123,7 +121,7 @@ export const get: ( ) => Effect.Effect< ClientResponse.HttpClientResponse, Error.HttpClientError, - Scope.Scope | HttpClient + HttpClient > = internal.get /** @@ -136,7 +134,7 @@ export const head: ( ) => Effect.Effect< ClientResponse.HttpClientResponse, Error.HttpClientError, - Scope.Scope | HttpClient + HttpClient > = internal.head /** @@ -149,7 +147,7 @@ export const post: ( ) => Effect.Effect< ClientResponse.HttpClientResponse, Error.HttpClientError, - Scope.Scope | HttpClient + HttpClient > = internal.post /** @@ -162,7 +160,7 @@ export const patch: ( ) => Effect.Effect< ClientResponse.HttpClientResponse, Error.HttpClientError, - Scope.Scope | HttpClient + HttpClient > = internal.patch /** @@ -175,7 +173,7 @@ export const put: ( ) => Effect.Effect< ClientResponse.HttpClientResponse, Error.HttpClientError, - Scope.Scope | HttpClient + HttpClient > = internal.put /** @@ -188,7 +186,7 @@ export const del: ( ) => Effect.Effect< ClientResponse.HttpClientResponse, Error.HttpClientError, - Scope.Scope | HttpClient + HttpClient > = internal.del /** @@ -201,7 +199,7 @@ export const options: ( ) => Effect.Effect< ClientResponse.HttpClientResponse, Error.HttpClientError, - Scope.Scope | HttpClient + HttpClient > = internal.options /** @@ -364,7 +362,7 @@ export const make: ( url: URL, signal: AbortSignal, fiber: RuntimeFiber - ) => Effect.Effect + ) => Effect.Effect ) => HttpClient = internal.make /** diff --git a/packages/platform/src/HttpClientResponse.ts b/packages/platform/src/HttpClientResponse.ts index 6aa820bc188..94c8f022864 100644 --- a/packages/platform/src/HttpClientResponse.ts +++ b/packages/platform/src/HttpClientResponse.ts @@ -5,7 +5,6 @@ import type * as Effect from "effect/Effect" import type * as ParseResult from "effect/ParseResult" import type * as Schema from "effect/Schema" import type { ParseOptions } from "effect/SchemaAST" -import type * as Scope from "effect/Scope" import type * as Stream from "effect/Stream" import type { Unify } from "effect/Unify" import type * as Cookies from "./Cookies.js" @@ -103,7 +102,7 @@ export const schemaNoBody: < */ export const stream: ( effect: Effect.Effect -) => Stream.Stream> = internal.stream +) => Stream.Stream = internal.stream /** * @since 1.0.0 diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index 78a636ceee7..b8faca34f8e 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -1,6 +1,7 @@ import * as Cause from "effect/Cause" import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" import type * as Fiber from "effect/Fiber" import * as FiberRef from "effect/FiberRef" import { constFalse, dual } from "effect/Function" @@ -11,7 +12,7 @@ import { pipeArguments } from "effect/Pipeable" import * as Predicate from "effect/Predicate" import * as Ref from "effect/Ref" import * as Schedule from "effect/Schedule" -import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" import type { NoExcessProperties, NoInfer } from "effect/Types" import * as Cookies from "../Cookies.js" import * as Headers from "../Headers.js" @@ -19,6 +20,7 @@ import type * as Client from "../HttpClient.js" import * as Error from "../HttpClientError.js" import type * as ClientRequest from "../HttpClientRequest.js" import type * as ClientResponse from "../HttpClientResponse.js" +import * as IncomingMessage from "../HttpIncomingMessage.js" import * as TraceContext from "../HttpTraceContext.js" import * as UrlParams from "../UrlParams.js" import * as internalRequest from "./httpClientRequest.js" @@ -123,6 +125,38 @@ export const makeWith = ( return self } +const responseRegistry = globalValue( + "@effect/platform/HttpClient/responseRegistry", + () => { + if ("FinalizationRegistry" in globalThis && globalThis.FinalizationRegistry) { + const registry = new FinalizationRegistry((controller: AbortController) => { + controller.abort() + }) + return { + register(response: ClientResponse.HttpClientResponse, controller: AbortController) { + registry.register(response, controller, response) + }, + unregister(response: ClientResponse.HttpClientResponse) { + registry.unregister(response) + } + } + } + + const timers = new Map() + return { + register(response: ClientResponse.HttpClientResponse, controller: AbortController) { + timers.set(response, setTimeout(() => controller.abort(), 5000)) + }, + unregister(response: ClientResponse.HttpClientResponse) { + const timer = timers.get(response) + if (timer === undefined) return + clearTimeout(timer) + timers.delete(response) + } + } + } +) + /** @internal */ export const make = ( f: ( @@ -130,14 +164,12 @@ export const make = ( url: URL, signal: AbortSignal, fiber: Fiber.RuntimeFiber - ) => Effect.Effect + ) => Effect.Effect ): Client.HttpClient => makeWith((effect) => Effect.flatMap(effect, (request) => Effect.withFiberRuntime((fiber) => { - const scope = Context.unsafeGet(fiber.getFiberRef(FiberRef.currentContext), Scope.Scope) const controller = new AbortController() - const addAbort = Scope.addFinalizer(scope, Effect.sync(() => controller.abort())) const urlResult = UrlParams.makeUrl(request.url, request.urlParams, request.hash) if (urlResult._tag === "Left") { return Effect.fail(new Error.RequestError({ request, reason: "InvalidUrl", cause: urlResult.left })) @@ -146,60 +178,139 @@ export const make = ( const tracerDisabled = !fiber.getFiberRef(FiberRef.currentTracerEnabled) || fiber.getFiberRef(currentTracerDisabledWhen)(request) if (tracerDisabled) { - return Effect.zipRight( - addAbort, - f(request, url, controller.signal, fiber) - ) + return f(request, url, controller.signal, fiber) } - return Effect.zipRight( - addAbort, - Effect.useSpan( - `http.client ${request.method}`, - { kind: "client", captureStackTrace: false }, - (span) => { - span.attribute("http.request.method", request.method) - span.attribute("server.address", url.origin) - if (url.port !== "") { - span.attribute("server.port", +url.port) - } - span.attribute("url.full", url.toString()) - span.attribute("url.path", url.pathname) - span.attribute("url.scheme", url.protocol.slice(0, -1)) - const query = url.search.slice(1) - if (query !== "") { - span.attribute("url.query", query) - } - const redactedHeaderNames = fiber.getFiberRef(Headers.currentRedactedNames) - const redactedHeaders = Headers.redact(request.headers, redactedHeaderNames) - for (const name in redactedHeaders) { - span.attribute(`http.request.header.${name}`, String(redactedHeaders[name])) - } - request = fiber.getFiberRef(currentTracerPropagation) - ? internalRequest.setHeaders(request, TraceContext.toHeaders(span)) - : request - return Effect.tap( - Effect.withParentSpan( - f( - request, - url, - controller.signal, - fiber - ), - span - ), - (response) => { - span.attribute("http.response.status_code", response.status) - const redactedHeaders = Headers.redact(response.headers, redactedHeaderNames) - for (const name in redactedHeaders) { - span.attribute(`http.response.header.${name}`, String(redactedHeaders[name])) + return Effect.useSpan( + `http.client ${request.method}`, + { kind: "client", captureStackTrace: false }, + (span) => { + span.attribute("http.request.method", request.method) + span.attribute("server.address", url.origin) + if (url.port !== "") { + span.attribute("server.port", +url.port) + } + span.attribute("url.full", url.toString()) + span.attribute("url.path", url.pathname) + span.attribute("url.scheme", url.protocol.slice(0, -1)) + const query = url.search.slice(1) + if (query !== "") { + span.attribute("url.query", query) + } + const redactedHeaderNames = fiber.getFiberRef(Headers.currentRedactedNames) + const redactedHeaders = Headers.redact(request.headers, redactedHeaderNames) + for (const name in redactedHeaders) { + span.attribute(`http.request.header.${name}`, String(redactedHeaders[name])) + } + request = fiber.getFiberRef(currentTracerPropagation) + ? internalRequest.setHeaders(request, TraceContext.toHeaders(span)) + : request + return Effect.uninterruptibleMask((restore) => + restore(f(request, url, controller.signal, fiber)).pipe( + Effect.withParentSpan(span), + Effect.matchCauseEffect({ + onSuccess: (response) => { + span.attribute("http.response.status_code", response.status) + const redactedHeaders = Headers.redact(response.headers, redactedHeaderNames) + for (const name in redactedHeaders) { + span.attribute(`http.response.header.${name}`, String(redactedHeaders[name])) + } + + responseRegistry.register(response, controller) + return Effect.succeed(new InterruptibleResponse(response, controller)) + }, + onFailure(cause) { + if (Cause.isInterrupted(cause)) { + controller.abort() + } + return Effect.failCause(cause) } - } + }) ) - } - ) + ) + } ) })), Effect.succeed as Client.HttpClient.Preprocess) +class InterruptibleResponse implements ClientResponse.HttpClientResponse { + constructor( + readonly original: ClientResponse.HttpClientResponse, + readonly controller: AbortController + ) {} + + readonly [internalResponse.TypeId]: ClientResponse.TypeId = internalResponse.TypeId + readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId = IncomingMessage.TypeId + + private applyInterrupt(effect: Effect.Effect) { + return Effect.suspend(() => { + responseRegistry.unregister(this.original) + return Effect.onInterrupt(effect, () => + Effect.sync(() => { + this.controller.abort() + })) + }) + } + + get request() { + return this.original.request + } + + get status() { + return this.original.status + } + + get headers() { + return this.original.headers + } + + get cookies() { + return this.original.cookies + } + + get remoteAddress() { + return this.original.remoteAddress + } + + get formData() { + return this.applyInterrupt(this.original.formData) + } + + get text() { + return this.applyInterrupt(this.original.text) + } + + get json() { + return this.applyInterrupt(this.original.json) + } + + get urlParamsBody() { + return this.applyInterrupt(this.original.urlParamsBody) + } + + get arrayBuffer() { + return this.applyInterrupt(this.original.arrayBuffer) + } + + get stream() { + return Stream.suspend(() => { + responseRegistry.unregister(this.original) + return Stream.ensuringWith(this.original.stream, (exit) => { + if (Exit.isInterrupted(exit)) { + this.controller.abort() + } + return Effect.void + }) + }) + } + + toJSON() { + return this.original.toJSON() + } + + [Inspectable.NodeInspectSymbol]() { + return this.original[Inspectable.NodeInspectSymbol]() + } +} + export const { /** @internal */ del, @@ -733,6 +844,6 @@ export const layerMergedContext = ( Effect.map(effect, (client) => transformResponse( client, - Effect.mapInputContext((input: Context.Context) => Context.merge(context, input)) + Effect.mapInputContext((input: Context.Context) => Context.merge(context, input)) ))) ) diff --git a/packages/platform/src/internal/httpClientResponse.ts b/packages/platform/src/internal/httpClientResponse.ts index d5a902a778b..9d5ea75918c 100644 --- a/packages/platform/src/internal/httpClientResponse.ts +++ b/packages/platform/src/internal/httpClientResponse.ts @@ -196,7 +196,7 @@ export const schemaNoBody = < /** @internal */ export const stream = (effect: Effect.Effect) => - Stream.unwrapScoped(Effect.map(effect, (_) => _.stream)) + Stream.unwrap(Effect.map(effect, (_) => _.stream)) /** @internal */ export const matchStatus = dual< diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index 2a3163e2647..6d7270bc3c0 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -48,8 +48,7 @@ const makeJsonPlaceholder = Effect.gen(function*() { HttpClientRequest.post("/todos").pipe( HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo), Effect.flatMap(client.execute), - Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) return { client, @@ -66,8 +65,7 @@ describe("HttpClient", () => { Effect.gen(function*() { const response = yield* pipe( HttpClient.get("https://www.google.com/"), - Effect.flatMap((_) => _.text), - Effect.scoped + Effect.flatMap((_) => _.text) ) assertInclude(response, "Google") }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) @@ -80,8 +78,7 @@ describe("HttpClient", () => { ) yield* pipe( HttpClientRequest.get("https://www.google.com/"), - client.execute, - Effect.scoped + client.execute ) const cookieHeader = yield* pipe(Ref.get(ref), Effect.map(Cookies.toCookieHeader)) yield* pipe( @@ -92,8 +89,7 @@ describe("HttpClient", () => { strictEqual(req.headers.cookie, cookieHeader) }) ) - ).execute, - Effect.scoped + ).execute ) }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) @@ -112,8 +108,7 @@ describe("HttpClient", () => { Effect.gen(function*() { const jp = yield* JsonPlaceholder const response = yield* jp.client.get("/todos/1").pipe( - Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) strictEqual(response.id, 1) }).pipe(Effect.provide(JsonPlaceholderLive), Effect.runPromise)) @@ -133,8 +128,7 @@ describe("HttpClient", () => { Effect.gen(function*() { const jp = yield* JsonPlaceholder const response = yield* jp.client.get("/todos/1").pipe( - Effect.flatMap(HttpClientResponse.schemaJson(OkTodo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaJson(OkTodo)) ) strictEqual(response.body.id, 1) }).pipe(Effect.provide(JsonPlaceholderLive), Effect.runPromise)) @@ -147,8 +141,7 @@ describe("HttpClient", () => { HttpClient.mapRequest(HttpClientRequest.prependUrl("https://")) ) const response = yield* client.get("/todos/1").pipe( - Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) strictEqual(response.id, 1) }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) @@ -168,8 +161,7 @@ describe("HttpClient", () => { yield* HttpClientRequest.post("https://jsonplaceholder.typicode.com").pipe( HttpClientRequest.bodyStream(requestStream), defaultClient.execute, - Effect.provide(Logger.replace(Logger.defaultLogger, logger)), - Effect.scoped + Effect.provide(Logger.replace(Logger.defaultLogger, logger)) ) deepStrictEqual(logs, [["hello"], ["world"]]) @@ -196,8 +188,7 @@ describe("HttpClient", () => { 404: () => Effect.fail("not found"), orElse: () => Effect.fail("boom") }) - ), - Effect.scoped + ) ) deepStrictEqual(response, { id: 1, userId: 1, title: "delectus aut autem", completed: false }) }).pipe(Effect.provide(JsonPlaceholderLive))) @@ -237,8 +228,7 @@ describe("HttpClient", () => { const client = defaultClient.pipe(HttpClient.followRedirects()) const response = yield* pipe( - client.get("https://google.com/"), - Effect.scoped + client.get("https://google.com/") ) strictEqual(response.request.url, "https://www.google.com/") }).pipe( From bfc1c60402e66b6613143fe221f4f3ad8a045f63 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 10 Mar 2025 13:13:34 +1300 Subject: [PATCH 2/5] handle non-tracing path --- packages/platform/src/internal/httpClient.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index b8faca34f8e..ba7e43ffd50 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -178,7 +178,20 @@ export const make = ( const tracerDisabled = !fiber.getFiberRef(FiberRef.currentTracerEnabled) || fiber.getFiberRef(currentTracerDisabledWhen)(request) if (tracerDisabled) { - return f(request, url, controller.signal, fiber) + return Effect.uninterruptibleMask((restore) => + Effect.matchCauseEffect(restore(f(request, url, controller.signal, fiber)), { + onSuccess(response) { + responseRegistry.register(response, controller) + return Effect.succeed(new InterruptibleResponse(response, controller)) + }, + onFailure(cause) { + if (Cause.isInterrupted(cause)) { + controller.abort() + } + return Effect.failCause(cause) + } + }) + ) } return Effect.useSpan( `http.client ${request.method}`, From f762e0004f0634e68f8e9fd6fc6745636488857d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 10 Mar 2025 14:35:48 +1300 Subject: [PATCH 3/5] fix timeout types --- packages/platform/src/internal/httpClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index ba7e43ffd50..2adc43694e4 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -142,7 +142,7 @@ const responseRegistry = globalValue( } } - const timers = new Map() + const timers = new Map() return { register(response: ClientResponse.HttpClientResponse, controller: AbortController) { timers.set(response, setTimeout(() => controller.abort(), 5000)) From bd6205cc2dac49f63af26e77bbaedfec1f73124c Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 10 Mar 2025 15:37:02 +1300 Subject: [PATCH 4/5] update README --- packages/platform/README.md | 66 +++++++++---------------------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/packages/platform/README.md b/packages/platform/README.md index 6ad732863b9..fc94125486a 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -2349,8 +2349,6 @@ const program = Effect.gen(function* () { console.log(json) }).pipe( - // Ensure request is aborted if the program is interrupted - Effect.scoped, // Provide the HttpClient Effect.provide(FetchHttpClient.layer) ) @@ -2387,7 +2385,6 @@ const program = HttpClient.get( "https://jsonplaceholder.typicode.com/posts/1" ).pipe( Effect.andThen((response) => response.json), - Effect.scoped, Effect.provide(FetchHttpClient.layer) ) @@ -2436,8 +2433,6 @@ const program = Effect.gen(function* () { console.log(json) }).pipe( - // Ensure request is aborted if the program is interrupted - Effect.scoped, // Provide the HttpClient Effect.provide(FetchHttpClient.layer) ) @@ -2457,30 +2452,6 @@ Output: */ ``` -## Understanding Scope - -When working with a request, note that there is a `Scope` requirement: - -```ts -import { FetchHttpClient, HttpClient } from "@effect/platform" -import { Effect } from "effect" - -// const program: Effect -const program = Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - const response = yield* client.get( - "https://jsonplaceholder.typicode.com/posts/1" - ) - const json = yield* response.json - console.log(json) -}).pipe( - // Provide the HttpClient implementation without scoping - Effect.provide(FetchHttpClient.layer) -) -``` - -A `Scope` is required because there is an open connection between the HTTP response and the body processing. For instance, if you have a streaming body, you receive the response before processing the body. This connection is managed within a scope, and using `Effect.scoped` controls when it is closed. - ## Customize a HttpClient The `HttpClient` module allows you to customize the client in various ways. For instance, you can log details of a request before execution using the `tapRequest` function. @@ -2504,7 +2475,7 @@ const program = Effect.gen(function* () { const json = yield* response.json console.log(json) -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) Effect.runPromise(program) /* @@ -2585,7 +2556,7 @@ const program = Effect.gen(function* () { const json = yield* response.json console.log(json) -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) Effect.runPromise(program) /* @@ -2627,7 +2598,7 @@ const program = Effect.gen(function* () { // Log the keys of the cookies stored in the reference console.log(Object.keys((yield* ref).cookies)) -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) Effect.runPromise(program) // Output: [ 'SOCS', 'AEC', '__Secure-ENID' ] @@ -2658,7 +2629,7 @@ const program = Effect.gen(function* () { ) const json = yield* response.json console.log(json) -}).pipe(Effect.scoped, Effect.provide(CustomFetchLive)) +}).pipe(Effect.provide(CustomFetchLive)) ``` ## Create a Custom HttpClient @@ -2694,7 +2665,6 @@ const program = Effect.gen(function* () { const json = yield* response.json console.log(json) }).pipe( - Effect.scoped, // Provide the HttpClient Effect.provide(Layer.succeed(HttpClient.HttpClient, myClient)) ) @@ -2849,7 +2819,7 @@ const getPostAsJson = Effect.gen(function* () { "https://jsonplaceholder.typicode.com/posts/1" ) return yield* response.json -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) getPostAsJson.pipe( Effect.andThen((post) => Console.log(typeof post, post)), @@ -2882,7 +2852,7 @@ const getPostAsText = Effect.gen(function* () { "https://jsonplaceholder.typicode.com/posts/1" ) return yield* response.text -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) getPostAsText.pipe( Effect.andThen((post) => Console.log(typeof post, post)), @@ -2937,7 +2907,7 @@ const getPostAndValidate = Effect.gen(function* () { "https://jsonplaceholder.typicode.com/posts/1" ) return yield* HttpClientResponse.schemaBodyJson(Post)(response) -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) getPostAndValidate.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* @@ -2951,8 +2921,6 @@ Output: In this example, we define a schema for a post object with properties `id` and `title`. Then, we fetch the data and validate it against this schema using `HttpClientResponse.schemaBodyJson`. Finally, we log the validated post object. -Note that we use `Effect.scoped` after consuming the response. This ensures that any resources associated with the HTTP request are properly cleaned up once we're done processing the response. - ### Filtering And Error Handling It's important to note that `HttpClient.get` doesn't consider non-`200` status codes as errors by default. This design choice allows for flexibility in handling different response scenarios. For instance, you might have a schema union where the status code serves as the discriminator, enabling you to define a schema that encompasses all possible response cases. @@ -2972,7 +2940,7 @@ const getText = Effect.gen(function* () { "https://jsonplaceholder.typicode.com/non-existing-page" ) return yield* response.text -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) getText.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* @@ -2994,7 +2962,7 @@ const getText = Effect.gen(function* () { "https://jsonplaceholder.typicode.com/non-existing-page" ) return yield* response.text -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) +}).pipe(Effect.provide(FetchHttpClient.layer)) getText.pipe(Effect.andThen(Console.log), NodeRuntime.runMain) /* @@ -3029,8 +2997,7 @@ const addPost = Effect.gen(function* () { userId: 1 }), Effect.flatMap(client.execute), - Effect.flatMap((res) => res.json), - Effect.scoped + Effect.flatMap((res) => res.json) ) }).pipe(Effect.provide(FetchHttpClient.layer)) @@ -3068,8 +3035,7 @@ const addPost = Effect.gen(function* () { "application/json; charset=UTF-8" ), client.execute, - Effect.flatMap((res) => res.json), - Effect.scoped + Effect.flatMap((res) => res.json) ) }).pipe(Effect.provide(FetchHttpClient.layer)) @@ -3113,8 +3079,7 @@ const addPost = Effect.gen(function* () { "application/json; charset=UTF-8" ), client.execute, - Effect.flatMap(HttpClientResponse.schemaBodyJson(Post)), - Effect.scoped + Effect.flatMap(HttpClientResponse.schemaBodyJson(Post)) ) }).pipe(Effect.provide(FetchHttpClient.layer)) @@ -3146,10 +3111,9 @@ const TestLayer = FetchHttpClient.layer.pipe(Layer.provide(FetchTest)) const program = Effect.gen(function* () { const client = yield* HttpClient.HttpClient - return yield* client.get("https://www.google.com/").pipe( - Effect.flatMap((res) => res.text), - Effect.scoped - ) + return yield* client + .get("https://www.google.com/") + .pipe(Effect.flatMap((res) => res.text)) }) // Test From 0f14926750ba20b2249306c13e83a591dafc9924 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Mar 2025 09:36:59 +1300 Subject: [PATCH 5/5] update changeset --- .changeset/lucky-shrimps-refuse.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.changeset/lucky-shrimps-refuse.md b/.changeset/lucky-shrimps-refuse.md index 4bf1f6977e8..32953abb016 100644 --- a/.changeset/lucky-shrimps-refuse.md +++ b/.changeset/lucky-shrimps-refuse.md @@ -5,3 +5,29 @@ --- remove Scope from HttpClient requirements + +Before: + +```ts +import { HttpClient } from "@effect/platform" +import { Effect } from "effect" + +Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json +}).pipe(Effect.scoped) +``` + +After: + +```ts +import { HttpClient } from "@effect/platform" +import { Effect } from "effect" + +Effect.gen(function* () { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("https://api.github.com/users/octocat") + return yield* response.json +}) // no need to add Effect.scoped +```