From e918da64a764b04eedee7e8f7d3378212254bb14 Mon Sep 17 00:00:00 2001 From: Shigma Date: Mon, 12 Feb 2024 23:27:12 +0800 Subject: [PATCH] feat(http): re-impl based on FunctionalService --- packages/http/package.json | 4 +- packages/http/src/index.ts | 173 ++++++++++++++++-------------------- packages/socks/package.json | 4 +- 3 files changed, 83 insertions(+), 98 deletions(-) diff --git a/packages/http/package.json b/packages/http/package.json index aff4f7f..323bde6 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -50,11 +50,11 @@ "plugin" ], "devDependencies": { - "cordis": "^3.9.2", + "cordis": "^3.10.0", "undici": "^6.6.2" }, "peerDependencies": { - "cordis": "^3.9.2" + "cordis": "^3.10.0" }, "dependencies": { "cosmokit": "^1.5.2", diff --git a/packages/http/src/index.ts b/packages/http/src/index.ts index 23bacbf..0f84cb0 100644 --- a/packages/http/src/index.ts +++ b/packages/http/src/index.ts @@ -1,4 +1,4 @@ -import { Context } from 'cordis' +import { Context, FunctionalService } from 'cordis' import { base64ToArrayBuffer, defineProperty, Dict, trimSlash } from 'cosmokit' import { ClientOptions } from 'ws' import { loadFile, lookup, WebSocket } from '@cordisjs/plugin-http/adapter' @@ -115,7 +115,7 @@ export interface HTTP { put: HTTP.Request2 } -export class HTTP extends Function { +export class HTTP extends FunctionalService { static Error = HTTPError /** @deprecated use `HTTP.Error.is()` instead */ static isAxiosError = HTTPError.is @@ -138,95 +138,8 @@ export class HTTP extends Function { } } - constructor(ctx: Context, config: HTTP.Config = {}, isExtend?: boolean) { - super() - - function resolveDispatcher(href?: string) { - if (!href) return - const url = new URL(href) - const agent = ctx.bail('http/dispatcher', url) - if (agent) return agent - throw new Error(`Cannot resolve proxy agent ${url}`) - } - - const http = async function http(this: Context, ...args: any[]) { - let method: HTTP.Method | undefined - if (typeof args[1] === 'string' || args[1] instanceof URL) { - method = args.shift() - } - const caller = isExtend ? ctx : this - const config = (http as HTTP).resolveConfig(caller, args[1]) - const url = HTTP.resolveURL(caller, args[0], config) - - const controller = new AbortController() - let timer: NodeJS.Timeout | number | undefined - const dispose = caller.on('dispose', () => { - clearTimeout(timer) - controller.abort('context disposed') - }) - if (config.timeout) { - timer = setTimeout(() => { - controller.abort('timeout') - }, config.timeout) - } - - try { - const raw = await fetch(url, { - method, - body: config.data, - headers: config.headers, - keepalive: config.keepAlive, - signal: controller.signal, - ['dispatcher' as never]: resolveDispatcher(config?.proxyAgent), - }).catch((cause) => { - const error = new HTTP.Error(`fetch ${url} failed`) - error.cause = cause - throw error - }) - - const response: HTTP.Response = { - data: null, - url: raw.url, - status: raw.status, - statusText: raw.statusText, - headers: raw.headers, - } - - if (!raw.ok) { - const error = new HTTP.Error(raw.statusText) - error.response = response - try { - response.data = await this.http.decodeResponse(raw) - } catch {} - throw error - } - - if (config.responseType === 'arraybuffer') { - response.data = await raw.arrayBuffer() - } else if (config.responseType === 'stream') { - response.data = raw.body - } else { - response.data = await this.http.decodeResponse(raw) - } - return response - } finally { - dispose() - } - } as HTTP - - http.config = config - defineProperty(http, Context.current, ctx) - Object.setPrototypeOf(http, Object.getPrototypeOf(this)) - - if (!isExtend) { - ctx.provide('http') - ctx.http = http - ctx.on('dispose', () => { - ctx.http = null as never - }) - } - - return http + constructor(ctx: Context, public config: HTTP.Config = {}, standalone?: boolean) { + super(ctx, 'http', { immediate: true, standalone }) } static mergeConfig = (target: HTTP.Config, source?: HTTP.Config) => ({ @@ -242,9 +155,17 @@ export class HTTP extends Function { return new HTTP(this[Context.current], HTTP.mergeConfig(this.config, config), true) } - resolveConfig(caller: Context, init?: HTTP.RequestConfig): HTTP.RequestConfig { + resolveDispatcher(href?: string) { + if (!href) return + const url = new URL(href) + const agent = this[Context.current].bail('http/dispatcher', url) + if (agent) return agent + throw new Error(`Cannot resolve proxy agent ${url}`) + } + + resolveConfig(ctx: Context, init?: HTTP.RequestConfig): HTTP.RequestConfig { let result = { headers: {}, ...this.config } - let intercept = caller[Context.intercept] + let intercept = ctx[Context.intercept] while (intercept) { result = HTTP.mergeConfig(result, intercept.http) intercept = Object.getPrototypeOf(intercept) @@ -285,6 +206,70 @@ export class HTTP extends Function { } } + async call(caller: Context, ...args: any[]) { + let method: HTTP.Method | undefined + if (typeof args[1] === 'string' || args[1] instanceof URL) { + method = args.shift() + } + const config = this.resolveConfig(caller, args[1]) + const url = HTTP.resolveURL(caller, args[0], config) + + const controller = new AbortController() + let timer: NodeJS.Timeout | number | undefined + const dispose = caller.on('dispose', () => { + clearTimeout(timer) + controller.abort('context disposed') + }) + if (config.timeout) { + timer = setTimeout(() => { + controller.abort('timeout') + }, config.timeout) + } + + try { + const raw = await fetch(url, { + method, + body: config.data, + headers: config.headers, + keepalive: config.keepAlive, + signal: controller.signal, + ['dispatcher' as never]: this.resolveDispatcher(config?.proxyAgent), + }).catch((cause) => { + const error = new HTTP.Error(`fetch ${url} failed`) + error.cause = cause + throw error + }) + + const response: HTTP.Response = { + data: null, + url: raw.url, + status: raw.status, + statusText: raw.statusText, + headers: raw.headers, + } + + if (!raw.ok) { + const error = new HTTP.Error(raw.statusText) + error.response = response + try { + response.data = await this.decodeResponse(raw) + } catch {} + throw error + } + + if (config.responseType === 'arraybuffer') { + response.data = await raw.arrayBuffer() + } else if (config.responseType === 'stream') { + response.data = raw.body + } else { + response.data = await this.decodeResponse(raw) + } + return response + } finally { + dispose() + } + } + async head(url: string, config?: HTTP.Config) { const caller = this[Context.current] const response = await this.call(caller, 'HEAD', url, config) @@ -292,7 +277,7 @@ export class HTTP extends Function { } /** @deprecated use `ctx.http()` instead */ - axios(url: string, config?: HTTP.Config): HTTP.Response { + axios(url: string, config?: HTTP.Config): Promise> { const caller = this[Context.current] caller.emit('internal/warning', 'ctx.http.axios() is deprecated, use ctx.http() instead') return this.call(caller, url, config) diff --git a/packages/socks/package.json b/packages/socks/package.json index 05277a3..056d5c6 100644 --- a/packages/socks/package.json +++ b/packages/socks/package.json @@ -40,12 +40,12 @@ "plugin" ], "devDependencies": { - "cordis": "^3.6.1", + "cordis": "^3.10.0", "undici": "^6.6.2" }, "peerDependencies": { "@cordisjs/plugin-http": "^0.1.0", - "cordis": "^3.6.1" + "cordis": "^3.10.0" }, "dependencies": { "socks": "^2.7.1",