From dff86e74b37756f79f57d7d22ab42f7281effad8 Mon Sep 17 00:00:00 2001 From: Alex Layton Date: Fri, 26 Apr 2024 19:36:08 -0400 Subject: [PATCH] feat(timeouts): allow changing http timeouts --- lib/auto.ts | 7 +++++-- lib/client.ts | 17 ++++++++++------- lib/http.ts | 14 +++++++++++++- lib/index.ts | 21 ++++++++++++++------- package.json | 2 +- 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/lib/auto.ts b/lib/auto.ts index a54bba2..4d2ff62 100644 --- a/lib/auto.ts +++ b/lib/auto.ts @@ -20,6 +20,7 @@ import resolveALPN from 'resolve-alpn'; import { HttpClient } from './http.js'; import { WebSocketClient } from './websocket.js'; +import { HTTPTimeouts } from './http.js'; const error = debug('@oada/client:auto:error'); @@ -70,11 +71,13 @@ export async function autoConnection({ token, concurrency, userAgent, + timeouts, }: { domain: string; token: string; concurrency: number; userAgent: string; + timeouts: HTTPTimeouts; }) { try { const { hostname, port, protocols } = parseDomain(domain); @@ -89,7 +92,7 @@ export async function autoConnection({ switch (alpnProtocol) { // Prefer HTTP/2 case 'h2': { - return new HttpClient(domain, token, { concurrency, userAgent }); + return new HttpClient(domain, token, { concurrency, userAgent, timeouts }); } // If no HTTP/2, use a WebSocket @@ -105,6 +108,6 @@ export async function autoConnection({ } catch (cError: unknown) { // Fallback to HTTP on error error(cError, 'Failed to auto pick connection type, falling back to HTTP'); - return new HttpClient(domain, token, { concurrency, userAgent }); + return new HttpClient(domain, token, { concurrency, userAgent, timeouts }); } } diff --git a/lib/client.ts b/lib/client.ts index 987dd0c..de477c8 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -35,7 +35,7 @@ import { toStringPath, } from './utils.js'; import { AbortController } from '#fetch'; -import { HttpClient } from './http.js'; +import { HttpClient, type HTTPTimeouts } from './http.js'; import { WebSocketClient } from './websocket.js'; import type { Change, Json, JsonObject } from './index.js'; @@ -66,9 +66,9 @@ export type ConnectionRequest = { headers: Record; data?: Body; } & ( - | { watch?: false } - | { watch: true; method: 'head' | 'get' | 'put' | 'post' | 'delete' } -); + | { watch?: false } + | { watch: true; method: 'head' | 'get' | 'put' | 'post' | 'delete' } + ); export interface ConnectionResponse { requestId: string | string[]; @@ -106,6 +106,7 @@ export interface Config { /** @default 'auto' */ connection?: 'auto' | 'ws' | 'http' | Connection; userAgent?: string; + timeouts?: number | Partial } export type Response = ConnectionResponse; @@ -271,7 +272,8 @@ export class OADAClient { concurrency = 1, userAgent = `${process.env.npm_package_name}/${process.env.npm_package_version}`, connection = 'http', - }: Config) { + timeouts = {}, + }: Config & { timeouts?: HTTPTimeouts }) { // Help for those who can't remember if https should be there this.#domain = domain; this.#token = token; @@ -294,6 +296,7 @@ export class OADAClient { this.#connection = new HttpClient(this.#domain, this.#token, { concurrency: this.#concurrency, userAgent, + timeouts, }); break; } @@ -434,8 +437,8 @@ export class OADAClient { }); const rev = typeof data === 'object' && - !(data instanceof Uint8Array) && - !Array.isArray(data) + !(data instanceof Uint8Array) && + !Array.isArray(data) ? Number(data?._rev) : undefined; diff --git a/lib/http.ts b/lib/http.ts index 356f994..41b097a 100644 --- a/lib/http.ts +++ b/lib/http.ts @@ -45,6 +45,14 @@ const enum ConnectionStatus { Connected, } +export interface HTTPTimeouts { + /** @default 60e3 */ + connect?: number + body?: number; + headers?: number; + keepAlive?: number; +} + function isJson(contentType: string) { const media = fromString(contentType); return [media.subtype, media.suffix].includes('json'); @@ -75,7 +83,7 @@ export class HttpClient extends EventEmitter implements Connection { constructor( domain: string, token: string, - { concurrency = 10, userAgent }: { concurrency: number; userAgent: string }, + { concurrency = 10, userAgent, timeouts }: { concurrency: number; userAgent: string; timeouts: HTTPTimeouts }, ) { super(); @@ -94,7 +102,11 @@ export class HttpClient extends EventEmitter implements Connection { this.#agent = Agent && new Agent({ + keepAliveTimeout: timeouts.keepAlive, + bodyTimeout: timeouts.body, + headersTimeout: timeouts.headers, connect: { + timeout: timeouts.connect, rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0', }, }); diff --git a/lib/index.ts b/lib/index.ts index f53c764..92aa173 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -38,11 +38,18 @@ export async function connect({ connection: proto = 'auto', concurrency = 1, userAgent = `${process.env.npm_package_name}/${process.env.npm_package_version}`, + timeouts: t = {}, ...config }: Config & { token: string }): Promise { + const timeouts = typeof t === 'number' ? { + connect: t, + keepAlive: t, + headers: t, + body: t, + } : t; const connection = proto === 'auto' - ? await autoConnection({ concurrency, userAgent, ...config }) + ? await autoConnection({ concurrency, userAgent, timeouts, ...config }) : proto; // Create an instance of client and start connection const client = new OADAClient({ @@ -78,12 +85,12 @@ export type Json = JsonPrimitive | JsonObject | JsonArray; export type JsonCompatible = { [P in keyof T]: T[P] extends Json - ? T[P] - : Pick extends Required> - ? never - : T[P] extends (() => unknown) | undefined - ? never - : JsonCompatible; + ? T[P] + : Pick extends Required> + ? never + : T[P] extends (() => unknown) | undefined + ? never + : JsonCompatible; }; declare global { diff --git a/package.json b/package.json index d6881e4..0f8c49a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oada/client", - "version": "5.0.1", + "version": "5.1.0", "description": "A lightweight client tool to interact with an OADA-compliant server", "repository": "https://github.com/OADA/client", "main": "./dist/index.js",