fix(client): Fix CI configuration, prevent socket hangs with auto-drain, and apply various client/core TypeScript fixes and test updates

This commit is contained in:
2025-08-18 00:21:14 +00:00
parent 9b9c8fd618
commit ee750dea58
34 changed files with 2144 additions and 892 deletions

View File

@@ -1,6 +1,10 @@
import { type CoreResponse } from '../../core/index.js';
import type { ICoreResponse } from '../../core_base/types.js';
import { type TPaginationConfig, PaginationStrategy, type TPaginatedResponse } from '../types/pagination.js';
import {
type TPaginationConfig,
PaginationStrategy,
type TPaginatedResponse,
} from '../types/pagination.js';
/**
* Creates a paginated response from a regular response
@@ -9,15 +13,17 @@ export async function createPaginatedResponse<T>(
response: ICoreResponse<any>,
paginationConfig: TPaginationConfig,
queryParams: Record<string, string>,
fetchNextPage: (params: Record<string, string>) => Promise<TPaginatedResponse<T>>
fetchNextPage: (
params: Record<string, string>,
) => Promise<TPaginatedResponse<T>>,
): Promise<TPaginatedResponse<T>> {
// Parse response body first
const body = await response.json() as any;
const body = (await response.json()) as any;
// Default to response.body for items if response is JSON
let items: T[] = Array.isArray(body)
? body
: (body?.items || body?.data || body?.results || []);
: body?.items || body?.data || body?.results || [];
let hasNextPage = false;
let nextPageParams: Record<string, string> = {};
@@ -26,8 +32,14 @@ export async function createPaginatedResponse<T>(
switch (paginationConfig.strategy) {
case PaginationStrategy.OFFSET: {
const config = paginationConfig;
const currentPage = parseInt(queryParams[config.pageParam || 'page'] || String(config.startPage || 1));
const limit = parseInt(queryParams[config.limitParam || 'limit'] || String(config.pageSize || 20));
const currentPage = parseInt(
queryParams[config.pageParam || 'page'] ||
String(config.startPage || 1),
);
const limit = parseInt(
queryParams[config.limitParam || 'limit'] ||
String(config.pageSize || 20),
);
const total = getValueByPath(body, config.totalPath || 'total') || 0;
hasNextPage = currentPage * limit < total;
@@ -35,7 +47,7 @@ export async function createPaginatedResponse<T>(
if (hasNextPage) {
nextPageParams = {
...queryParams,
[config.pageParam || 'page']: String(currentPage + 1)
[config.pageParam || 'page']: String(currentPage + 1),
};
}
break;
@@ -43,7 +55,10 @@ export async function createPaginatedResponse<T>(
case PaginationStrategy.CURSOR: {
const config = paginationConfig;
const nextCursor = getValueByPath(body, config.cursorPath || 'nextCursor');
const nextCursor = getValueByPath(
body,
config.cursorPath || 'nextCursor',
);
const hasMore = getValueByPath(body, config.hasMorePath || 'hasMore');
hasNextPage = !!nextCursor || !!hasMore;
@@ -51,7 +66,7 @@ export async function createPaginatedResponse<T>(
if (hasNextPage && nextCursor) {
nextPageParams = {
...queryParams,
[config.cursorParam || 'cursor']: nextCursor
[config.cursorParam || 'cursor']: nextCursor,
};
}
break;
@@ -60,7 +75,9 @@ export async function createPaginatedResponse<T>(
case PaginationStrategy.LINK_HEADER: {
const linkHeader = response.headers['link'] || '';
// Handle both string and string[] types for the link header
const headerValue = Array.isArray(linkHeader) ? linkHeader[0] : linkHeader;
const headerValue = Array.isArray(linkHeader)
? linkHeader[0]
: linkHeader;
const links = parseLinkHeader(headerValue);
hasNextPage = !!links.next;
@@ -100,7 +117,13 @@ export async function createPaginatedResponse<T>(
// Create a function to fetch all remaining pages
const getAllPages = async (): Promise<T[]> => {
const allItems = [...items];
let currentPage: TPaginatedResponse<T> = { items, hasNextPage, getNextPage, getAllPages, response };
let currentPage: TPaginatedResponse<T> = {
items,
hasNextPage,
getNextPage,
getAllPages,
response,
};
while (currentPage.hasNextPage) {
try {
@@ -119,7 +142,7 @@ export async function createPaginatedResponse<T>(
hasNextPage,
getNextPage,
getAllPages,
response
response,
};
}
@@ -166,11 +189,15 @@ export function getValueByPath(obj: any, path?: string): any {
let current = obj;
for (const key of keys) {
if (current === null || current === undefined || typeof current !== 'object') {
if (
current === null ||
current === undefined ||
typeof current !== 'object'
) {
return undefined;
}
current = current[key];
}
return current;
}
}

View File

@@ -5,15 +5,22 @@ export { SmartRequest } from './smartrequest.js';
export { CoreResponse } from '../core/index.js';
// Export types
export type { HttpMethod, ResponseType, FormField, RetryConfig, TimeoutConfig, RateLimitConfig } from './types/common.js';
export {
export type {
HttpMethod,
ResponseType,
FormField,
RetryConfig,
TimeoutConfig,
RateLimitConfig,
} from './types/common.js';
export {
PaginationStrategy,
type TPaginationConfig as PaginationConfig,
type OffsetPaginationConfig,
type CursorPaginationConfig,
type LinkPaginationConfig,
type CustomPaginationConfig,
type TPaginatedResponse as PaginatedResponse
type TPaginatedResponse as PaginatedResponse,
} from './types/pagination.js';
// Convenience factory functions
@@ -45,4 +52,4 @@ export function createBinaryClient<T = any>() {
*/
export function createStreamClient() {
return SmartRequest.create().accept('stream');
}
}

View File

@@ -1,6 +1,4 @@
// plugins for client module
import FormData from 'form-data';
export {
FormData as formData
};
export { FormData as formData };

View File

@@ -3,14 +3,19 @@ import type { ICoreResponse } from '../core_base/types.js';
import * as plugins from './plugins.js';
import type { ICoreRequestOptions } from '../core_base/types.js';
import type { HttpMethod, ResponseType, FormField, RateLimitConfig } from './types/common.js';
import type {
HttpMethod,
ResponseType,
FormField,
RateLimitConfig,
} from './types/common.js';
import {
type TPaginationConfig,
PaginationStrategy,
type OffsetPaginationConfig,
type CursorPaginationConfig,
type CustomPaginationConfig,
type TPaginatedResponse
type TPaginatedResponse,
} from './types/pagination.js';
import { createPaginatedResponse } from './features/pagination.js';
@@ -22,21 +27,21 @@ import { createPaginatedResponse } from './features/pagination.js';
function parseRetryAfter(retryAfter: string | string[]): number {
// Handle array of values (take first)
const value = Array.isArray(retryAfter) ? retryAfter[0] : retryAfter;
if (!value) return 0;
// Try to parse as seconds (number)
const seconds = parseInt(value, 10);
if (!isNaN(seconds)) {
return seconds * 1000;
}
// Try to parse as HTTP date
const retryDate = new Date(value);
if (!isNaN(retryDate.getTime())) {
return Math.max(0, retryDate.getTime() - Date.now());
}
return 0;
}
@@ -96,7 +101,7 @@ export class SmartRequest<T = any> {
if (Buffer.isBuffer(item.value)) {
form.append(item.name, item.value, {
filename: item.filename || 'file',
contentType: item.contentType || 'application/octet-stream'
contentType: item.contentType || 'application/octet-stream',
});
} else {
form.append(item.name, item.value);
@@ -109,7 +114,7 @@ export class SmartRequest<T = any> {
this._options.headers = {
...this._options.headers,
...form.getHeaders()
...form.getHeaders(),
};
this._options.requestBody = form;
@@ -143,7 +148,7 @@ export class SmartRequest<T = any> {
maxWaitTime: config?.maxWaitTime ?? 60000,
fallbackDelay: config?.fallbackDelay ?? 1000,
backoffFactor: config?.backoffFactor ?? 2,
onRateLimit: config?.onRateLimit
onRateLimit: config?.onRateLimit,
};
return this;
}
@@ -157,7 +162,7 @@ export class SmartRequest<T = any> {
}
this._options.headers = {
...this._options.headers,
...headers
...headers,
};
return this;
}
@@ -179,7 +184,7 @@ export class SmartRequest<T = any> {
query(params: Record<string, string>): this {
this._queryParams = {
...this._queryParams,
...params
...params,
};
return this;
}
@@ -190,7 +195,7 @@ export class SmartRequest<T = any> {
options(options: Partial<ICoreRequestOptions>): this {
this._options = {
...this._options,
...options
...options,
};
return this;
}
@@ -210,12 +215,12 @@ export class SmartRequest<T = any> {
accept(type: ResponseType): this {
// Map response types to Accept header values
const acceptHeaders: Record<ResponseType, string> = {
'json': 'application/json',
'text': 'text/plain',
'binary': 'application/octet-stream',
'stream': '*/*'
json: 'application/json',
text: 'text/plain',
binary: 'application/octet-stream',
stream: '*/*',
};
return this.header('Accept', acceptHeaders[type]);
}
@@ -230,20 +235,26 @@ export class SmartRequest<T = any> {
/**
* Configure offset-based pagination (page & limit)
*/
withOffsetPagination(config: Omit<OffsetPaginationConfig, 'strategy'> = {}): this {
withOffsetPagination(
config: Omit<OffsetPaginationConfig, 'strategy'> = {},
): this {
this._paginationConfig = {
strategy: PaginationStrategy.OFFSET,
pageParam: config.pageParam || 'page',
limitParam: config.limitParam || 'limit',
startPage: config.startPage || 1,
pageSize: config.pageSize || 20,
totalPath: config.totalPath || 'total'
totalPath: config.totalPath || 'total',
};
// Add initial pagination parameters
this.query({
[this._paginationConfig.pageParam]: String(this._paginationConfig.startPage),
[this._paginationConfig.limitParam]: String(this._paginationConfig.pageSize)
[this._paginationConfig.pageParam]: String(
this._paginationConfig.startPage,
),
[this._paginationConfig.limitParam]: String(
this._paginationConfig.pageSize,
),
});
return this;
@@ -252,12 +263,14 @@ export class SmartRequest<T = any> {
/**
* Configure cursor-based pagination
*/
withCursorPagination(config: Omit<CursorPaginationConfig, 'strategy'> = {}): this {
withCursorPagination(
config: Omit<CursorPaginationConfig, 'strategy'> = {},
): this {
this._paginationConfig = {
strategy: PaginationStrategy.CURSOR,
cursorParam: config.cursorParam || 'cursor',
cursorPath: config.cursorPath || 'nextCursor',
hasMorePath: config.hasMorePath || 'hasMore'
hasMorePath: config.hasMorePath || 'hasMore',
};
return this;
}
@@ -267,7 +280,7 @@ export class SmartRequest<T = any> {
*/
withLinkPagination(): this {
this._paginationConfig = {
strategy: PaginationStrategy.LINK_HEADER
strategy: PaginationStrategy.LINK_HEADER,
};
return this;
}
@@ -279,7 +292,7 @@ export class SmartRequest<T = any> {
this._paginationConfig = {
strategy: PaginationStrategy.CUSTOM,
hasNextPage: config.hasNextPage,
getNextPageParams: config.getNextPageParams
getNextPageParams: config.getNextPageParams,
};
return this;
}
@@ -324,7 +337,9 @@ export class SmartRequest<T = any> {
*/
async getPaginated<ItemType = T>(): Promise<TPaginatedResponse<ItemType>> {
if (!this._paginationConfig) {
throw new Error('Pagination not configured. Call one of the pagination methods first.');
throw new Error(
'Pagination not configured. Call one of the pagination methods first.',
);
}
// Default to GET if no method specified
@@ -345,7 +360,7 @@ export class SmartRequest<T = any> {
nextClient._queryParams = nextPageParams;
return nextClient.getPaginated<ItemType>();
}
},
);
}
@@ -375,8 +390,8 @@ export class SmartRequest<T = any> {
for (let attempt = 0; attempt <= this._retries; attempt++) {
try {
const request = new CoreRequest(this._url, this._options as any);
const response = await request.fire() as ICoreResponse<R>;
const response = (await request.fire()) as ICoreResponse<R>;
// Check for 429 status if rate limit handling is enabled
if (this._rateLimitConfig && response.status === 429) {
if (rateLimitAttempt >= this._rateLimitConfig.maxRetries) {
@@ -385,18 +400,22 @@ export class SmartRequest<T = any> {
}
let waitTime: number;
if (this._rateLimitConfig.respectRetryAfter && response.headers['retry-after']) {
if (
this._rateLimitConfig.respectRetryAfter &&
response.headers['retry-after']
) {
// Parse Retry-After header
waitTime = parseRetryAfter(response.headers['retry-after']);
// Cap wait time to maxWaitTime
waitTime = Math.min(waitTime, this._rateLimitConfig.maxWaitTime);
} else {
// Use exponential backoff
waitTime = Math.min(
this._rateLimitConfig.fallbackDelay * Math.pow(this._rateLimitConfig.backoffFactor, rateLimitAttempt),
this._rateLimitConfig.maxWaitTime
this._rateLimitConfig.fallbackDelay *
Math.pow(this._rateLimitConfig.backoffFactor, rateLimitAttempt),
this._rateLimitConfig.maxWaitTime,
);
}
@@ -406,14 +425,14 @@ export class SmartRequest<T = any> {
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise((resolve) => setTimeout(resolve, waitTime));
rateLimitAttempt++;
// Decrement attempt to retry this attempt
attempt--;
continue;
}
// Success or non-429 error response
return response;
} catch (error) {
@@ -425,11 +444,11 @@ export class SmartRequest<T = any> {
}
// Otherwise, wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
// This should never be reached due to the throw in the loop above
throw lastError;
}
}
}

View File

@@ -1,7 +1,14 @@
/**
* HTTP Methods supported by the client
*/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
export type HttpMethod =
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH'
| 'HEAD'
| 'OPTIONS';
/**
* Response types supported by the client
@@ -30,11 +37,11 @@ export interface UrlEncodedField {
* Retry configuration
*/
export interface RetryConfig {
attempts: number; // Number of retry attempts
initialDelay?: number; // Initial delay in ms
maxDelay?: number; // Maximum delay in ms
factor?: number; // Backoff factor
statusCodes?: number[]; // Status codes to retry on
attempts: number; // Number of retry attempts
initialDelay?: number; // Initial delay in ms
maxDelay?: number; // Maximum delay in ms
factor?: number; // Backoff factor
statusCodes?: number[]; // Status codes to retry on
shouldRetry?: (error: Error, attemptCount: number) => boolean;
}
@@ -42,20 +49,20 @@ export interface RetryConfig {
* Timeout configuration
*/
export interface TimeoutConfig {
request?: number; // Overall request timeout in ms
connection?: number; // Connection timeout in ms
socket?: number; // Socket idle timeout in ms
response?: number; // Response timeout in ms
request?: number; // Overall request timeout in ms
connection?: number; // Connection timeout in ms
socket?: number; // Socket idle timeout in ms
response?: number; // Response timeout in ms
}
/**
* Rate limit configuration for handling 429 responses
*/
export interface RateLimitConfig {
maxRetries?: number; // Maximum number of retries (default: 3)
respectRetryAfter?: boolean; // Respect Retry-After header (default: true)
maxWaitTime?: number; // Max wait time in ms (default: 60000)
fallbackDelay?: number; // Delay when no Retry-After header (default: 1000)
backoffFactor?: number; // Exponential backoff factor (default: 2)
maxRetries?: number; // Maximum number of retries (default: 3)
respectRetryAfter?: boolean; // Respect Retry-After header (default: true)
maxWaitTime?: number; // Max wait time in ms (default: 60000)
fallbackDelay?: number; // Delay when no Retry-After header (default: 1000)
backoffFactor?: number; // Exponential backoff factor (default: 2)
onRateLimit?: (attempt: number, waitTime: number) => void; // Callback for rate limit events
}
}

View File

@@ -5,10 +5,10 @@ import type { ICoreResponse } from '../../core_base/types.js';
* Pagination strategy options
*/
export enum PaginationStrategy {
OFFSET = 'offset', // Uses page & limit parameters
CURSOR = 'cursor', // Uses a cursor/token for next page
LINK_HEADER = 'link', // Uses Link headers
CUSTOM = 'custom' // Uses a custom pagination handler
OFFSET = 'offset', // Uses page & limit parameters
CURSOR = 'cursor', // Uses a cursor/token for next page
LINK_HEADER = 'link', // Uses Link headers
CUSTOM = 'custom', // Uses a custom pagination handler
}
/**
@@ -16,11 +16,11 @@ export enum PaginationStrategy {
*/
export interface OffsetPaginationConfig {
strategy: PaginationStrategy.OFFSET;
pageParam?: string; // Parameter name for page number (default: "page")
limitParam?: string; // Parameter name for page size (default: "limit")
startPage?: number; // Starting page number (default: 1)
pageSize?: number; // Number of items per page (default: 20)
totalPath?: string; // JSON path to total item count (default: "total")
pageParam?: string; // Parameter name for page number (default: "page")
limitParam?: string; // Parameter name for page size (default: "limit")
startPage?: number; // Starting page number (default: 1)
pageSize?: number; // Number of items per page (default: 20)
totalPath?: string; // JSON path to total item count (default: "total")
}
/**
@@ -28,9 +28,9 @@ export interface OffsetPaginationConfig {
*/
export interface CursorPaginationConfig {
strategy: PaginationStrategy.CURSOR;
cursorParam?: string; // Parameter name for cursor (default: "cursor")
cursorPath?: string; // JSON path to next cursor (default: "nextCursor")
hasMorePath?: string; // JSON path to check if more items exist (default: "hasMore")
cursorParam?: string; // Parameter name for cursor (default: "cursor")
cursorPath?: string; // JSON path to next cursor (default: "nextCursor")
hasMorePath?: string; // JSON path to check if more items exist (default: "hasMore")
}
/**
@@ -47,21 +47,28 @@ export interface LinkPaginationConfig {
export interface CustomPaginationConfig {
strategy: PaginationStrategy.CUSTOM;
hasNextPage: (response: ICoreResponse<any>) => boolean;
getNextPageParams: (response: ICoreResponse<any>, currentParams: Record<string, string>) => Record<string, string>;
getNextPageParams: (
response: ICoreResponse<any>,
currentParams: Record<string, string>,
) => Record<string, string>;
}
/**
* Union type of all pagination configurations
*/
export type TPaginationConfig = OffsetPaginationConfig | CursorPaginationConfig | LinkPaginationConfig | CustomPaginationConfig;
export type TPaginationConfig =
| OffsetPaginationConfig
| CursorPaginationConfig
| LinkPaginationConfig
| CustomPaginationConfig;
/**
* Interface for a paginated response
*/
export interface TPaginatedResponse<T> {
items: T[]; // Current page items
hasNextPage: boolean; // Whether there are more pages
getNextPage: () => Promise<TPaginatedResponse<T>>; // Function to get the next page
getAllPages: () => Promise<T[]>; // Function to get all remaining pages and combine
response: ICoreResponse<any>; // Original response
}
items: T[]; // Current page items
hasNextPage: boolean; // Whether there are more pages
getNextPage: () => Promise<TPaginatedResponse<T>>; // Function to get the next page
getAllPages: () => Promise<T[]>; // Function to get all remaining pages and combine
response: ICoreResponse<any>; // Original response
}