| /** | |
| * Represents responses returned by the Effect HTTP client. | |
| * | |
| * An `HttpClientResponse` keeps the original request together with the response | |
| * status, headers, cookies, and body accessors from the shared incoming-message | |
| * model. This module includes constructors, schema-based decoders, helpers for | |
| * streaming response bodies, and utilities for matching or filtering by HTTP | |
| * status. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Effect from "../../Effect.ts" | |
| import { dual } from "../../Function.ts" | |
| import * as Inspectable from "../../Inspectable.ts" | |
| import * as Option from "../../Option.ts" | |
| import { type Pipeable, pipeArguments } from "../../Pipeable.ts" | |
| import * as Schema from "../../Schema.ts" | |
| import type { ParseOptions } from "../../SchemaAST.ts" | |
| import * as Stream from "../../Stream.ts" | |
| import type { Unify } from "../../Unify.ts" | |
| import * as Cookies from "./Cookies.ts" | |
| import * as Headers from "./Headers.ts" | |
| import * as Error from "./HttpClientError.ts" | |
| import type * as HttpClientRequest from "./HttpClientRequest.ts" | |
| import * as HttpIncomingMessage from "./HttpIncomingMessage.ts" | |
| import * as UrlParams from "./UrlParams.ts" | |
| export { | |
| /** | |
| * Creates a decoder that reads a response JSON body and decodes it with the supplied schema. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| schemaBodyJson, | |
| /** | |
| * Creates a decoder that reads response URL-encoded body parameters and decodes them with the supplied schema. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| schemaBodyUrlParams, | |
| /** | |
| * Creates a decoder that validates and decodes response headers with the supplied schema. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| schemaHeaders | |
| } from "./HttpIncomingMessage.ts" | |
| /** | |
| * Type identifier for `HttpClientResponse` values. | |
| * | |
| * @category type IDs | |
| * @since 4.0.0 | |
| */ | |
| export const TypeId = "~effect/http/HttpClientResponse" | |
| /** | |
| * Model of an HTTP client response, including the original request, status, cookies, headers, and body accessors. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface HttpClientResponse extends HttpIncomingMessage.HttpIncomingMessage<Error.HttpClientError>, Pipeable { | |
| readonly [TypeId]: typeof TypeId | |
| readonly request: HttpClientRequest.HttpClientRequest | |
| readonly status: number | |
| readonly cookies: Cookies.Cookies | |
| readonly formData: Effect.Effect<FormData, Error.HttpClientError> | |
| } | |
| /** | |
| * Wraps a Web `Response` and its original `HttpClientRequest` as an `HttpClientResponse`. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const fromWeb = (request: HttpClientRequest.HttpClientRequest, source: Response): HttpClientResponse => | |
| new WebHttpClientResponse(request, source) | |
| /** | |
| * Creates a decoder for a response's status, headers, and JSON body using the supplied schema. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const schemaJson = < | |
| A, | |
| I extends { | |
| readonly status?: number | undefined | |
| readonly headers?: Readonly<Record<string, string | undefined>> | undefined | |
| readonly body?: unknown | |
| }, | |
| RD, | |
| RE | |
| >( | |
| schema: Schema.Codec<A, I, RD, RE>, | |
| options?: ParseOptions | undefined | |
| ) => { | |
| const decode = Schema.decodeEffect(Schema.toCodecJson(schema).annotate({ options })) | |
| return ( | |
| self: HttpClientResponse | |
| ): Effect.Effect<A, Schema.SchemaError | Error.HttpClientError, RD> => | |
| Effect.flatMap(self.json, (body) => | |
| decode({ | |
| status: self.status, | |
| headers: self.headers, | |
| body | |
| })) | |
| } | |
| /** | |
| * Creates a decoder for a response's status and headers without reading a response body. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const schemaNoBody = < | |
| A, | |
| I extends { | |
| readonly status?: number | undefined | |
| readonly headers?: Readonly<Record<string, string>> | undefined | |
| }, | |
| RD, | |
| RE | |
| >( | |
| schema: Schema.Codec<A, I, RD, RE>, | |
| options?: ParseOptions | undefined | |
| ) => { | |
| const decode = Schema.decodeEffect(schema.annotate({ options })) | |
| return (self: HttpClientResponse): Effect.Effect<A, Schema.SchemaError, RD> => | |
| decode({ | |
| status: self.status, | |
| headers: self.headers | |
| } as any as I) | |
| } | |
| /** | |
| * Converts an effect producing an `HttpClientResponse` into a stream of response body bytes. | |
| * | |
| * @category accessors | |
| * @since 4.0.0 | |
| */ | |
| export const stream = <E, R>( | |
| effect: Effect.Effect<HttpClientResponse, E, R> | |
| ): Stream.Stream<Uint8Array, Error.HttpClientError | E, R> => Stream.unwrap(Effect.map(effect, (self) => self.stream)) | |
| /** | |
| * Pattern matches on a response status, checking exact status handlers before status-class handlers and `orElse`. | |
| * | |
| * @category pattern matching | |
| * @since 4.0.0 | |
| */ | |
| export const matchStatus: { | |
| /** | |
| * Pattern matches on a response status, checking exact status handlers before status-class handlers and `orElse`. | |
| * | |
| * @category pattern matching | |
| * @since 4.0.0 | |
| */ | |
| < | |
| const Cases extends { | |
| readonly [status: number]: (_: HttpClientResponse) => any | |
| readonly "2xx"?: (_: HttpClientResponse) => any | |
| readonly "3xx"?: (_: HttpClientResponse) => any | |
| readonly "4xx"?: (_: HttpClientResponse) => any | |
| readonly "5xx"?: (_: HttpClientResponse) => any | |
| readonly orElse: (_: HttpClientResponse) => any | |
| } | |
| >(cases: Cases): (self: HttpClientResponse) => Cases[keyof Cases] extends (_: any) => infer R ? Unify<R> : never | |
| /** | |
| * Pattern matches on a response status, checking exact status handlers before status-class handlers and `orElse`. | |
| * | |
| * @category pattern matching | |
| * @since 4.0.0 | |
| */ | |
| < | |
| const Cases extends { | |
| readonly [status: number]: (_: HttpClientResponse) => any | |
| readonly "2xx"?: (_: HttpClientResponse) => any | |
| readonly "3xx"?: (_: HttpClientResponse) => any | |
| readonly "4xx"?: (_: HttpClientResponse) => any | |
| readonly "5xx"?: (_: HttpClientResponse) => any | |
| readonly orElse: (_: HttpClientResponse) => any | |
| } | |
| >(self: HttpClientResponse, cases: Cases): Cases[keyof Cases] extends (_: any) => infer R ? Unify<R> : never | |
| } = dual(2, < | |
| const Cases extends { | |
| readonly [status: number]: (_: HttpClientResponse) => any | |
| readonly "2xx"?: (_: HttpClientResponse) => any | |
| readonly "3xx"?: (_: HttpClientResponse) => any | |
| readonly "4xx"?: (_: HttpClientResponse) => any | |
| readonly "5xx"?: (_: HttpClientResponse) => any | |
| readonly orElse: (_: HttpClientResponse) => any | |
| } | |
| >(self: HttpClientResponse, cases: Cases) => { | |
| const status = self.status | |
| if (cases[status]) { | |
| return cases[status](self) | |
| } else if (status >= 200 && status < 300 && cases["2xx"]) { | |
| return cases["2xx"](self) | |
| } else if (status >= 300 && status < 400 && cases["3xx"]) { | |
| return cases["3xx"](self) | |
| } else if (status >= 400 && status < 500 && cases["4xx"]) { | |
| return cases["4xx"](self) | |
| } else if (status >= 500 && status < 600 && cases["5xx"]) { | |
| return cases["5xx"](self) | |
| } | |
| return cases.orElse(self) | |
| }) | |
| /** | |
| * Succeeds with the response when its status satisfies the predicate, otherwise fails with `HttpClientError`. | |
| * | |
| * @category filters | |
| * @since 4.0.0 | |
| */ | |
| export const filterStatus: { | |
| /** | |
| * Succeeds with the response when its status satisfies the predicate, otherwise fails with `HttpClientError`. | |
| * | |
| * @category filters | |
| * @since 4.0.0 | |
| */ | |
| (f: (status: number) => boolean): (self: HttpClientResponse) => Effect.Effect<HttpClientResponse, Error.HttpClientError> | |
| /** | |
| * Succeeds with the response when its status satisfies the predicate, otherwise fails with `HttpClientError`. | |
| * | |
| * @category filters | |
| * @since 4.0.0 | |
| */ | |
| (self: HttpClientResponse, f: (status: number) => boolean): Effect.Effect<HttpClientResponse, Error.HttpClientError> | |
| } = dual( | |
| 2, | |
| (self: HttpClientResponse, f: (status: number) => boolean) => | |
| Effect.suspend(() => | |
| f(self.status) ? Effect.succeed(self) : Effect.fail( | |
| new Error.HttpClientError({ | |
| reason: new Error.StatusCodeError({ | |
| response: self, | |
| request: self.request, | |
| description: "invalid status code" | |
| }) | |
| }) | |
| ) | |
| ) | |
| ) | |
| /** | |
| * Succeeds with the response only when its status is in the 2xx range, otherwise fails with `HttpClientError`. | |
| * | |
| * @category filters | |
| * @since 4.0.0 | |
| */ | |
| export const filterStatusOk = (self: HttpClientResponse): Effect.Effect<HttpClientResponse, Error.HttpClientError> => | |
| self.status >= 200 && self.status < 300 ? Effect.succeed(self) : Effect.fail( | |
| new Error.HttpClientError({ | |
| reason: new Error.StatusCodeError({ | |
| response: self, | |
| request: self.request, | |
| description: "non 2xx status code" | |
| }) | |
| }) | |
| ) | |
| // ----------------------------------------------------------------------------- | |
| // internal | |
| // ----------------------------------------------------------------------------- | |
| class WebHttpClientResponse extends Inspectable.Class implements HttpClientResponse, Pipeable { | |
| readonly [HttpIncomingMessage.TypeId]: typeof HttpIncomingMessage.TypeId | |
| readonly [TypeId]: typeof TypeId | |
| readonly request: HttpClientRequest.HttpClientRequest | |
| private readonly source: globalThis.Response | |
| constructor( | |
| request: HttpClientRequest.HttpClientRequest, | |
| source: globalThis.Response | |
| ) { | |
| super() | |
| this.request = request | |
| this.source = source | |
| this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId | |
| this[TypeId] = TypeId | |
| } | |
| toJSON(): unknown { | |
| return HttpIncomingMessage.inspect(this, { | |
| _id: "HttpClientResponse", | |
| request: this.request.toJSON(), | |
| status: this.status | |
| }) | |
| } | |
| get status(): number { | |
| return this.source.status | |
| } | |
| get headers(): Headers.Headers { | |
| return Headers.fromInput(this.source.headers) | |
| } | |
| cachedCookies?: Cookies.Cookies | |
| get cookies(): Cookies.Cookies { | |
| if (this.cachedCookies) { | |
| return this.cachedCookies | |
| } | |
| return this.cachedCookies = Cookies.fromSetCookie(this.source.headers.getSetCookie()) | |
| } | |
| get remoteAddress(): Option.Option<string> { | |
| return Option.none() | |
| } | |
| get stream(): Stream.Stream<Uint8Array, Error.HttpClientError> { | |
| return this.source.body | |
| ? Stream.fromReadableStream({ | |
| evaluate: () => this.source.body!, | |
| onError: (cause) => | |
| new Error.HttpClientError({ | |
| reason: new Error.DecodeError({ | |
| request: this.request, | |
| response: this, | |
| cause | |
| }) | |
| }) | |
| }) | |
| : Stream.fail( | |
| new Error.HttpClientError({ | |
| reason: new Error.EmptyBodyError({ | |
| request: this.request, | |
| response: this, | |
| description: "can not create stream from empty body" | |
| }) | |
| }) | |
| ) | |
| } | |
| get json(): Effect.Effect<Schema.Json, Error.HttpClientError> { | |
| return Effect.flatMap(this.text, (text) => | |
| Effect.try({ | |
| try: () => text === "" ? null : JSON.parse(text), | |
| catch: (cause) => | |
| new Error.HttpClientError({ | |
| reason: new Error.DecodeError({ | |
| request: this.request, | |
| response: this, | |
| cause | |
| }) | |
| }) | |
| })) | |
| } | |
| private textBody?: Effect.Effect<string, Error.HttpClientError> | |
| get text(): Effect.Effect<string, Error.HttpClientError> { | |
| if (this.textBody) { | |
| return this.textBody | |
| } | |
| this.textBody = Effect.tryPromise({ | |
| try: () => this.source.text(), | |
| catch: (cause) => | |
| new Error.HttpClientError({ | |
| reason: new Error.DecodeError({ | |
| request: this.request, | |
| response: this, | |
| cause | |
| }) | |
| }) | |
| }).pipe(Effect.cached, Effect.runSync) | |
| this.arrayBufferBody = Effect.map(this.textBody, (_) => new TextEncoder().encode(_).buffer) | |
| return this.textBody | |
| } | |
| get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, Error.HttpClientError> { | |
| return Effect.flatMap(this.text, (_) => | |
| Effect.try({ | |
| try: () => UrlParams.fromInput(new URLSearchParams(_)), | |
| catch: (cause) => | |
| new Error.HttpClientError({ | |
| reason: new Error.DecodeError({ | |
| request: this.request, | |
| response: this, | |
| cause | |
| }) | |
| }) | |
| })) | |
| } | |
| private formDataBody?: Effect.Effect<FormData, Error.HttpClientError> | |
| get formData(): Effect.Effect<FormData, Error.HttpClientError> { | |
| return this.formDataBody ??= Effect.tryPromise({ | |
| try: () => this.source.formData(), | |
| catch: (cause) => | |
| new Error.HttpClientError({ | |
| reason: new Error.DecodeError({ | |
| request: this.request, | |
| response: this, | |
| cause | |
| }) | |
| }) | |
| }).pipe(Effect.cached, Effect.runSync) | |
| } | |
| private arrayBufferBody?: Effect.Effect<ArrayBuffer, Error.HttpClientError> | |
| get arrayBuffer(): Effect.Effect<ArrayBuffer, Error.HttpClientError> { | |
| if (this.arrayBufferBody) { | |
| return this.arrayBufferBody | |
| } | |
| this.arrayBufferBody = Effect.tryPromise({ | |
| try: () => this.source.arrayBuffer(), | |
| catch: (cause) => | |
| new Error.HttpClientError({ | |
| reason: new Error.DecodeError({ | |
| request: this.request, | |
| response: this, | |
| cause | |
| }) | |
| }) | |
| }).pipe(Effect.cached, Effect.runSync) | |
| this.textBody = Effect.map(this.arrayBufferBody, (_) => new TextDecoder().decode(_)) | |
| return this.arrayBufferBody | |
| } | |
| pipe() { | |
| return pipeArguments(this, arguments) | |
| } | |
| } | |
Xet Storage Details
- Size:
- 13.4 kB
- Xet hash:
- 4e5c85245d1d5ddb203d631669e94d4b3e91eb464d93f019ec5d857c0545843a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.