Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 41 additions & 68 deletions packages/httio/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,64 @@
import request from "~/http/request";
import connection from "~/middleware/connection";
import pipeline from "~/middleware/pipeline";
import type { HttioClient, HttioClientOptions, HttioMethodOptions } from "~/types/client";
import type { Json } from "~/types/data";
import pipeline from "~/http/pipeline";
import type { HttioClient, HttioClientMethods, HttioClientOptions, HttioMethodOptions } from "~/types/client";
import type { Middleware } from "~/types/pipeline";
import type { HttioRequestInit } from "~/types/request";
import type { HttioResponse } from "~/types/response";
import assign from "~/utils/assign";
import merge from "~/utils/merge";
import url from "~/utils/url";
import { isPlaneObject } from "~/utils/validate";

export default function client(options?: HttioClientOptions): HttioClient {
const { fetch: $fetch = fetch, query, url: base = "", ...init } = options || {};

const middleware = pipeline(connection($fetch));
const METHODS_WITH_BODY: (keyof HttioClientMethods)[] = ["delete", "options", "patch", "post", "put"];

const open = (url: URL, options: HttioRequestInit): HttioResponse => {
return middleware.handle(request(url, options));
};
const METHODS: (keyof HttioClientMethods)[] = ["get", "head", ...METHODS_WITH_BODY];

return {
delete(path: string, options?: HttioMethodOptions): HttioResponse {
const { query, ...$options } = merge(init, options || {});
export default function client(options?: HttioClientOptions): HttioClient {
const { fetch: $fetch = fetch, url: base, ...init } = options || {};

return open(url(base || path, path, query), { ...$options, method: "DELETE" });
},
const middleware = pipeline($fetch);

extends(_options: HttioClientOptions): HttioClient {
const { query: _query, url: _base = "", ..._init } = _options;
const methods = {} as HttioClientMethods;

if (!_init.fetch) {
_init.fetch = $fetch;
}
for (const method of METHODS) {
// @ts-expect-error ---
methods[method] = (path, body, options) => {
if (!METHODS_WITH_BODY.includes(method)) {
if (isPlaneObject(body)) {
options = body as HttioMethodOptions;
}

if (base && _base) {
// @ts-expect-error ---
_init.url = url(base, _base, merge(query, _query));
} else if (base) {
// @ts-expect-error ---
_init.url = url(base, "/", merge(query, _query));
} else if (_base) {
// @ts-expect-error ---
_init.url = url(_base, "/", merge(query, _query));
body = void 0;
}

return client(merge(init, _init)).use(...middleware.pipes);
},

get(path: string, options?: HttioMethodOptions): HttioResponse {
const { query, ...$options } = merge(init, options || {});

return open(url(base || path, path, query), { ...$options, method: "GET" });
},

head(path: string, options?: HttioMethodOptions): HttioResponse {
const { query, ...$options } = merge(init, options || {});

return open(url(base || path, path, query), { ...$options, method: "HEAD" });
},

options(path: string, options?: HttioMethodOptions): HttioResponse {
const { query, ...$options } = merge(init, options || {});

return open(url(base || path, path, query), { ...$options, method: "OPTIONS" });
},

patch(path: string, payload?: BodyInit | Json, options?: HttioMethodOptions): HttioResponse {
const { query, ...$options } = merge(init, options || {});

return open(url(base || path, path, query), { ...$options, body: payload, method: "PATCH" });
},

post(path: string, payload?: BodyInit | Json, options?: HttioMethodOptions): HttioResponse {
const { query, ...$options } = merge(init, options || {});

return open(url(base || path, path, query), { ...$options, body: payload, method: "POST" });
},
return middleware.handle(
url(base || path, path, query),
assign($options, {
body,
method: method.toUpperCase(),
})
);
};
}

return assign(methods, {
extends(_options: HttioClientOptions): HttioClient {
if (!_options.fetch) {
_options.fetch = $fetch;
}

put(path: string, payload?: BodyInit | Json, options?: HttioMethodOptions): HttioResponse {
const { query, ...$options } = merge(init, options || {});
if (!_options.url) {
_options.url = base;
} else if (base) {
_options.url = url(base, _options.url instanceof URL ? _options.url.toString() : _options.url);
}

return open(url(base || path, path, query), { ...$options, body: payload, method: "PUT" });
return client(merge(init, _options)).use(...middleware.pipes);
},

use(...middlewares: Middleware[]): HttioClient {
use(this: HttioClient, ...middlewares: Middleware[]): HttioClient {
middleware.use(...middlewares);

return this;
},
};
});
}
14 changes: 14 additions & 0 deletions packages/httio/src/errors/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { HttioRequest } from "~/types/request";
import type { HttioResponse } from "~/types/response";

export default class HttpError extends Error {
public request: HttioRequest;
public response: HttioResponse;

constructor(message: string | null | undefined, request: HttioRequest, response: HttioResponse) {
super(message || response.toString());

this.request = request;
this.response = response;
}
}
3 changes: 3 additions & 0 deletions packages/httio/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import HttpError from "./http";

export { HttpError };
27 changes: 27 additions & 0 deletions packages/httio/src/http/body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { HttioBody } from "~/types/response";
import { isFunction } from "~/utils/validate";

type Failure = readonly [never, unknown];
type Success = readonly [Response, never?];

function bind<T>(promise: Promise<Failure | Success>, key: keyof Response) {
return async function handle(): Promise<T> {
const [data, error] = await promise;

if (error) {
throw error;
}

return isFunction(data[key]) ? (data[key]() as T) : (data[key] as T);
};
}

export default function body(promise: Promise<Failure | Success>): HttioBody {
const data = { stream: bind(promise, "body") } as HttioBody;

for (const property of ["arrayBuffer", "blob", "bytes", "json", "text"] satisfies (keyof HttioBody)[]) {
data[property] = bind(promise, property) as never;
}

return data;
}
40 changes: 0 additions & 40 deletions packages/httio/src/http/error.ts

This file was deleted.

55 changes: 55 additions & 0 deletions packages/httio/src/http/pipeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import request from "~/http/request";
import response from "~/http/response";
import normalize from "~/middleware/normalize";
import type { Fetcher } from "~/types/fetch";
import type { Middleware, NextMiddleware, Pipeline } from "~/types/pipeline";
import type { HttioRequest, HttioRequestInit } from "~/types/request";
import type { ResponseInstance } from "~/types/response";
import { isHttioResponse } from "~/utils/validate";

export default function pipeline(fetch: Fetcher): Pipeline {
const pipes: Middleware[] = [];

const open: NextMiddleware = (request) => {
const { url, ...init } = request;

return response(url, request.method, async () => fetch(new Request(url, init as RequestInit)));
};

const reducer = (next: NextMiddleware, middleware: Middleware) => {
return function handle(req: HttioRequest): ResponseInstance {
return response(req.url, req.method, async () => {
const data = await middleware(req, next);

if (isHttioResponse(data)) {
return new Response(await data.stream(), {
headers: data.headers,
status: data.status,
});
}

return data instanceof Response ? data : new Response(data);
});
};
};

return {
get handle() {
const next = pipes.reduceRight(reducer, open);

return function handle(url: URL | string, options: HttioRequestInit): ResponseInstance {
return normalize(request(url, options), next) as never;
};
},

get pipes() {
return pipes;
},

use(...middleware: Middleware[]) {
pipes.push(...middleware);

return this;
},
};
}
30 changes: 10 additions & 20 deletions packages/httio/src/http/request.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
import type { HttioRequest, HttioRequestInit } from "~/types/request";
import assign from "~/utils/assign";
import { REQUEST } from "~/utils/consts";
import { isPlaneObject } from "~/utils/validate";
import { isString } from "~/utils/validate";

export default function request(url: URL | string, options: HttioRequestInit): HttioRequest {
const { headers, ...init } = options;
const { headers, method, ...init } = options;

if (!init.method) {
throw new Error("Invalid method");
if (isString(url)) {
url = new URL(url);
}

if (headers === null || (headers && !(headers instanceof Headers || isPlaneObject(headers)))) {
throw new Error("Invalid headers");
}

return Object.assign({ [REQUEST]: REQUEST }, init, {
return assign({ [REQUEST]: REQUEST }, init, {
headers: new Headers(headers),
url: new URL(url),

clone(this: HttioRequest): HttioRequest {
const { url, ...init } = this;

return request(url, init);
},

toRequest(): Request {
const { url, ...init } = this;
method: method.toUpperCase(),
url,

return new Request(url, init);
toString() {
return `[${options.method.toUpperCase()}] ${url.toString()}`;
},
});
}
Loading
Loading