@zimic/fetch
Version:
Next-gen TypeScript-first Fetch client
233 lines (200 loc) • 10 kB
text/typescript
import {
HttpHeaders,
HttpHeadersSchema,
HttpResponse,
HttpResponseBodySchema,
HttpResponseHeadersSchema,
HttpSchema,
HttpSchemaMethod,
HttpSchemaPath,
HttpStatusCode,
} from '@zimic/http';
import { Default, PossiblePromise } from '@zimic/utils/types';
import { FetchRequest } from '../request/FetchRequest';
import { getOrSetBoundBodyMethod, isBodyMethod, withIncludedBodyIfAvailable } from '../utils/objects';
import FetchResponseError from './error/FetchResponseError';
import {
FetchResponseBodySchema,
FetchResponseInit,
FetchResponseObject,
FetchResponseObjectOptions,
FetchResponseStatusCode,
} from './types';
/** @see {@link https://zimic.dev/docs/fetch/api/fetch-response `FetchResponse` API reference} */
export interface FetchResponsePerStatusCode<
Schema extends HttpSchema,
Method extends HttpSchemaMethod<Schema>,
Path extends HttpSchemaPath.Literal<Schema, Method>,
StatusCode extends HttpStatusCode = HttpStatusCode,
> extends HttpResponse<
HttpResponseBodySchema<Default<Schema[Path][Method]>, StatusCode>,
Default<HttpResponseHeadersSchema<Default<Schema[Path][Method]>, StatusCode>>,
StatusCode
> {
raw: Response;
request: FetchRequest<Schema, Method, Path>;
error: FetchResponseError<Schema, Method, Path>;
clone: () => FetchResponsePerStatusCode<Schema, Method, Path, StatusCode>;
/** @see {@link https://zimic.dev/docs/fetch/api/fetch-response#responsetoobject `response.toObject()` API reference} */
toObject: ((options: FetchResponseObjectOptions.WithBody) => Promise<FetchResponseObject>) &
((options?: FetchResponseObjectOptions.WithoutBody) => FetchResponseObject) &
((options?: FetchResponseObjectOptions) => PossiblePromise<FetchResponseObject>);
}
/** @see {@link https://zimic.dev/docs/fetch/api/fetch-response `FetchResponse` API reference} */
export type FetchResponse<
Schema extends HttpSchema,
Method extends HttpSchemaMethod<Schema>,
Path extends HttpSchemaPath.Literal<Schema, Method>,
/** @deprecated The type parameter `ErrorOnly` will be removed in the next major version. */
ErrorOnly extends boolean = false,
Redirect extends RequestRedirect = 'follow',
StatusCode extends FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect> =
FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect>,
> = StatusCode extends StatusCode ? FetchResponsePerStatusCode<Schema, Method, Path, StatusCode> : never;
export namespace FetchResponse {
/** A loosely typed version of a {@link FetchResponse}. */
export interface Loose extends Response {
raw: Response;
request: FetchRequest.Loose;
error: FetchResponseError<any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
clone: () => Loose;
/** @see {@link https://zimic.dev/docs/fetch/api/fetch-response#responsetoobject `response.toObject()` API reference} */
toObject: ((options: FetchResponseObjectOptions.WithBody) => Promise<FetchResponseObject>) &
((options?: FetchResponseObjectOptions.WithoutBody) => FetchResponseObject) &
((options?: FetchResponseObjectOptions) => PossiblePromise<FetchResponseObject>);
}
}
interface FetchResponseClass {
new <
Schema extends HttpSchema,
Method extends HttpSchemaMethod<Schema>,
Path extends HttpSchemaPath.Literal<Schema, Method>,
/** @deprecated The type parameter `ErrorOnly` will be removed in the next major version. */
ErrorOnly extends boolean = false,
Redirect extends RequestRedirect = 'follow',
StatusCode extends FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect> =
FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect>,
>(
fetchRequest: FetchRequest<Schema, Method, Path>,
response?: Response,
): FetchResponse<Schema, Method, Path, ErrorOnly, Redirect, StatusCode>;
new <
Schema extends HttpSchema,
Method extends HttpSchemaMethod<Schema>,
Path extends HttpSchemaPath.Literal<Schema, Method>,
/** @deprecated The type parameter `ErrorOnly` will be removed in the next major version. */
ErrorOnly extends boolean = false,
Redirect extends RequestRedirect = 'follow',
StatusCode extends FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect> =
FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect>,
>(
fetchRequest: FetchRequest<Schema, Method, Path>,
body?: FetchResponseBodySchema<Default<Default<Default<Schema[Path][Method]>['response']>[StatusCode]>>,
init?: FetchResponseInit<Schema, Method, Path, ErrorOnly, Redirect, StatusCode>,
): FetchResponse<Schema, Method, Path, ErrorOnly, Redirect, StatusCode>;
prototype: Response;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Symbol.hasInstance]: (instance: unknown) => instance is FetchResponse<any, any, any, any, any, any>;
}
const FETCH_RESPONSE_BRAND = Symbol.for('FetchResponse');
const FETCH_RESPONSE_EXTRA_PROPERTIES = [FETCH_RESPONSE_BRAND, 'raw', 'request', 'error', 'toObject'] as const;
type FetchResponseExtraProperty = (typeof FETCH_RESPONSE_EXTRA_PROPERTIES)[number];
function createFetchResponseClass() {
const FetchResponseClass = function FetchResponse<
Schema extends HttpSchema,
Method extends HttpSchemaMethod<Schema>,
Path extends HttpSchemaPath.Literal<Schema, Method>,
/** @deprecated The type parameter `ErrorOnly` will be removed in the next major version. */
ErrorOnly extends boolean = false,
Redirect extends RequestRedirect = 'follow',
StatusCode extends FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect> =
FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect>,
>(
fetchRequest: FetchRequest<Schema, Method, Path>,
responseOrBody?:
| Response
| FetchResponseBodySchema<Default<Default<Default<Schema[Path][Method]>['response']>[StatusCode]>>,
init?: FetchResponseInit<Schema, Method, Path, ErrorOnly, Redirect, StatusCode>,
): FetchResponse<Schema, Method, Path, ErrorOnly, Redirect, StatusCode> {
const response =
responseOrBody instanceof Response
? responseOrBody
: new Response(responseOrBody as BodyInit | null, init as ResponseInit);
let error: FetchResponseError<Schema, Method, Path> | null = null;
function clone() {
return new FetchResponseClass(fetchRequest, response.clone());
}
function toObject(options: { includeBody: true }): Promise<FetchResponseObject>;
function toObject(options?: { includeBody?: false }): FetchResponseObject;
function toObject(options?: { includeBody?: boolean }): PossiblePromise<FetchResponseObject>;
function toObject(options?: { includeBody?: boolean }): PossiblePromise<FetchResponseObject> {
const responseObject: FetchResponseObject = {
url: response.url,
type: response.type,
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: HttpHeaders.prototype.toObject.call(response.headers) as HttpHeadersSchema,
redirected: response.redirected,
};
if (!options?.includeBody) {
return responseObject;
}
return withIncludedBodyIfAvailable(response, responseObject);
}
type FetchResponseInstance = FetchResponse<Schema, Method, Path, ErrorOnly, Redirect, StatusCode>;
const fetchResponse = new Proxy(response, {
get(target, property, receiver) {
if (property === FETCH_RESPONSE_BRAND) {
return true;
}
if (property === ('raw' satisfies keyof FetchResponseInstance)) {
return response satisfies FetchResponseInstance['raw'];
}
if (property === ('request' satisfies keyof FetchResponseInstance)) {
return fetchRequest satisfies FetchResponseInstance['request'];
}
if (property === ('error' satisfies keyof FetchResponseInstance)) {
// We create the error lazily to preserve the stack trace from the point where it was first accessed.
error ??= new FetchResponseError(fetchRequest, receiver as FetchResponse<Schema, Method, Path>);
return error satisfies FetchResponseInstance['error'];
}
if (property === ('clone' satisfies keyof FetchResponseInstance)) {
// The `clone` method is not compatible with the mapping we do in `FetchResponse`(i.e.
// `StatusCode extends StatusCode ? ... : ...`), so we need a type assertion here.
return clone as FetchResponseInstance['clone'];
}
if (property === ('toObject' satisfies keyof FetchResponseInstance)) {
return toObject satisfies FetchResponseInstance['toObject'];
}
// Fallback other properties to the original `Response` instance.
const value = Reflect.get(target, property, target) as unknown;
if (isBodyMethod(property, value)) {
return getOrSetBoundBodyMethod(response, property, value);
}
return value;
},
has(target, property) {
return (
FETCH_RESPONSE_EXTRA_PROPERTIES.includes(property as FetchResponseExtraProperty) ||
Reflect.has(target, property)
);
},
}) as unknown as FetchResponse<Schema, Method, Path, ErrorOnly, Redirect, StatusCode>;
return fetchResponse;
} as unknown as FetchResponseClass;
Object.defineProperty(FetchResponseClass, Symbol.hasInstance, {
value(instance: unknown): boolean {
return (
instance instanceof Response && FETCH_RESPONSE_BRAND in instance && instance[FETCH_RESPONSE_BRAND] === true
);
},
writable: false,
enumerable: false,
configurable: false,
});
Object.setPrototypeOf(FetchResponseClass.prototype, Response.prototype);
return FetchResponseClass;
}
export const FetchResponse = createFetchResponseClass();
Object.setPrototypeOf(FetchResponse.prototype, Response.prototype);