From eb2ccd8d9fe3e9573395a136d32ea661b0d1419b Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 28 Jul 2025 22:37:36 +0000 Subject: [PATCH] update --- package.json | 1 + pnpm-lock.yaml | 8 ++ test/test.browser.ts | 102 ++++++++++++++++++++++++++ test/{test.modern.ts => test.node.ts} | 16 ++-- ts/client/features/pagination.ts | 7 +- ts/client/index.ts | 2 +- ts/client/plugins.ts | 6 ++ ts/client/smartrequestclient.ts | 24 +++--- ts/client/types/pagination.ts | 9 ++- ts/core/index.ts | 38 ++++++---- ts/core/plugins.ts | 4 + ts/core_base/request.ts | 2 +- ts/core_base/response.ts | 4 +- ts/core_base/types.ts | 32 ++++++-- ts/core_fetch/request.ts | 5 ++ ts/core_fetch/response.ts | 4 +- ts/core_fetch/types.ts | 26 ++----- ts/core_node/request.ts | 6 ++ ts/core_node/response.ts | 2 +- ts/core_node/types.ts | 27 +------ ts/index.ts | 2 +- 21 files changed, 228 insertions(+), 99 deletions(-) create mode 100644 test/test.browser.ts rename test/{test.modern.ts => test.node.ts} (83%) create mode 100644 ts/client/plugins.ts create mode 100644 ts/core/plugins.ts diff --git a/package.json b/package.json index 1a84549..f2f972a 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "homepage": "https://code.foss.global/push.rocks/smartrequest", "dependencies": { "@push.rocks/smartenv": "^5.0.13", + "@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpromise": "^4.0.4", "@push.rocks/smarturl": "^3.1.0", "agentkeepalive": "^4.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af9c372..f4bae2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@push.rocks/smartenv': specifier: ^5.0.13 version: 5.0.13 + '@push.rocks/smartpath': + specifier: ^6.0.0 + version: 6.0.0 '@push.rocks/smartpromise': specifier: ^4.0.4 version: 4.2.3 @@ -830,6 +833,9 @@ packages: '@push.rocks/smartpath@5.0.18': resolution: {integrity: sha512-kIyRTlOoeEth5b4Qp8KPUxNOGNdvhb2aD0hbHfF3oGTQ0xnDdgB1l03/4bIoapHG48OrTgh4uQ5tUorykgdOzw==} + '@push.rocks/smartpath@6.0.0': + resolution: {integrity: sha512-r94u1MbBaIOSy+517PZp2P7SuZPSe9LkwJ8l3dXQKHeIOri/zDxk/RQPiFM+j4N9301ztkRyhvRj7xgUDroOsg==} + '@push.rocks/smartpdf@3.2.2': resolution: {integrity: sha512-SKGNHz7HsgU6uVSVrRCL13kIeAFMvd4oQBLI3VmPcMkxXfWNPJkb6jKknqP8bhobWA/ryJS+3Dj///UELUvVKQ==} @@ -5725,6 +5731,8 @@ snapshots: '@push.rocks/smartpath@5.0.18': {} + '@push.rocks/smartpath@6.0.0': {} + '@push.rocks/smartpdf@3.2.2(typescript@5.7.3)': dependencies: '@push.rocks/smartbuffer': 3.0.4 diff --git a/test/test.browser.ts b/test/test.browser.ts new file mode 100644 index 0000000..99e7a5b --- /dev/null +++ b/test/test.browser.ts @@ -0,0 +1,102 @@ +import { tap, expect } from '@pushrocks/tapbundle'; + +// For browser tests, we need to import from a browser-safe path +// that doesn't trigger Node.js module imports +import { CoreRequest, CoreResponse } from '../ts/core/index.js'; +import type { ICoreRequestOptions } from '../ts/core_base/types.js'; + +tap.test('browser: should request a JSON document over https', async () => { + const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts/1'); + const response = await request.fire(); + + expect(response).not.toBeNull(); + expect(response).toHaveProperty('status'); + expect(response.status).toEqual(200); + + const data = await response.json(); + expect(data).toHaveProperty('id'); + expect(data.id).toEqual(1); + expect(data).toHaveProperty('title'); +}); + +tap.test('browser: should handle CORS requests', async () => { + const options: ICoreRequestOptions = { + headers: { + 'Accept': 'application/vnd.github.v3+json' + } + }; + + const request = new CoreRequest('https://api.github.com/users/github', options); + const response = await request.fire(); + + expect(response).not.toBeNull(); + expect(response.status).toEqual(200); + + const data = await response.json(); + expect(data).toHaveProperty('login'); + expect(data.login).toEqual('github'); +}); + +tap.test('browser: should handle request timeouts', async () => { + let timedOut = false; + + const options: ICoreRequestOptions = { + timeout: 1000 + }; + + try { + const request = new CoreRequest('https://httpbin.org/delay/10', options); + await request.fire(); + } catch (error) { + timedOut = true; + expect(error.message).toContain('timed out'); + } + + expect(timedOut).toEqual(true); +}); + +tap.test('browser: should handle POST requests with JSON', async () => { + const testData = { + title: 'foo', + body: 'bar', + userId: 1 + }; + + const options: ICoreRequestOptions = { + method: 'POST', + requestBody: testData + }; + + const request = new CoreRequest('https://jsonplaceholder.typicode.com/posts', options); + const response = await request.fire(); + + expect(response.status).toEqual(201); + + const responseData = await response.json(); + expect(responseData).toHaveProperty('id'); + expect(responseData.title).toEqual(testData.title); + expect(responseData.body).toEqual(testData.body); + expect(responseData.userId).toEqual(testData.userId); +}); + +tap.test('browser: should handle query parameters', async () => { + const options: ICoreRequestOptions = { + queryParams: { + foo: 'bar', + baz: 'qux' + } + }; + + const request = new CoreRequest('https://httpbin.org/get', options); + const response = await request.fire(); + + expect(response.status).toEqual(200); + + const data = await response.json(); + expect(data.args).toHaveProperty('foo'); + expect(data.args.foo).toEqual('bar'); + expect(data.args).toHaveProperty('baz'); + expect(data.args.baz).toEqual('qux'); +}); + +export default tap.start(); \ No newline at end of file diff --git a/test/test.modern.ts b/test/test.node.ts similarity index 83% rename from test/test.modern.ts rename to test/test.node.ts index e929a13..582d6f5 100644 --- a/test/test.modern.ts +++ b/test/test.node.ts @@ -1,8 +1,8 @@ import { tap, expect } from '@pushrocks/tapbundle'; -import { SmartRequestClient } from '../ts/modern/index.js'; +import { SmartRequestClient } from '../ts/client/index.js'; -tap.test('modern: should request a html document over https', async () => { +tap.test('client: should request a html document over https', async () => { const response = await SmartRequestClient.create() .url('https://encrypted.google.com/') .get(); @@ -14,7 +14,7 @@ tap.test('modern: should request a html document over https', async () => { expect(text.length).toBeGreaterThan(0); }); -tap.test('modern: should request a JSON document over https', async () => { +tap.test('client: should request a JSON document over https', async () => { const response = await SmartRequestClient.create() .url('https://jsonplaceholder.typicode.com/posts/1') .get(); @@ -24,7 +24,7 @@ tap.test('modern: should request a JSON document over https', async () => { expect(body.id).toEqual(1); }); -tap.test('modern: should post a JSON document over http', async () => { +tap.test('client: should post a JSON document over http', async () => { const testData = { text: 'example_text' }; const response = await SmartRequestClient.create() .url('https://httpbin.org/post') @@ -37,7 +37,7 @@ tap.test('modern: should post a JSON document over http', async () => { expect(body.json.text).toEqual('example_text'); }); -tap.test('modern: should set headers correctly', async () => { +tap.test('client: should set headers correctly', async () => { const customHeader = 'X-Custom-Header'; const headerValue = 'test-value'; @@ -54,7 +54,7 @@ tap.test('modern: should set headers correctly', async () => { expect(body.headers[customHeader]).toEqual(headerValue); }); -tap.test('modern: should handle query parameters', async () => { +tap.test('client: should handle query parameters', async () => { const params = { param1: 'value1', param2: 'value2' }; const response = await SmartRequestClient.create() @@ -70,7 +70,7 @@ tap.test('modern: should handle query parameters', async () => { expect(body.args.param2).toEqual('value2'); }); -tap.test('modern: should handle timeout configuration', async () => { +tap.test('client: should handle timeout configuration', async () => { // This test just verifies that the timeout method doesn't throw const client = SmartRequestClient.create() .url('https://httpbin.org/get') @@ -81,7 +81,7 @@ tap.test('modern: should handle timeout configuration', async () => { expect(response.ok).toBeTrue(); }); -tap.test('modern: should handle retry configuration', async () => { +tap.test('client: should handle retry configuration', async () => { // This test just verifies that the retry method doesn't throw const client = SmartRequestClient.create() .url('https://httpbin.org/get') diff --git a/ts/client/features/pagination.ts b/ts/client/features/pagination.ts index 16ce099..379d5db 100644 --- a/ts/client/features/pagination.ts +++ b/ts/client/features/pagination.ts @@ -1,17 +1,18 @@ -import { type CoreResponse } from '../../core_node/index.js'; +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'; /** * Creates a paginated response from a regular response */ export async function createPaginatedResponse( - response: CoreResponse, + response: ICoreResponse, paginationConfig: TPaginationConfig, queryParams: Record, fetchNextPage: (params: Record) => Promise> ): Promise> { // Parse response body first - const body = await response.json(); + const body = await response.json() as any; // Default to response.body for items if response is JSON let items: T[] = Array.isArray(body) diff --git a/ts/client/index.ts b/ts/client/index.ts index 988e790..b823d3a 100644 --- a/ts/client/index.ts +++ b/ts/client/index.ts @@ -2,7 +2,7 @@ export { SmartRequestClient } from './smartrequestclient.js'; // Export response type from core -export { CoreResponse } from '../core_node/index.js'; +export { CoreResponse } from '../core/index.js'; // Export types export type { HttpMethod, ResponseType, FormField, RetryConfig, TimeoutConfig } from './types/common.js'; diff --git a/ts/client/plugins.ts b/ts/client/plugins.ts new file mode 100644 index 0000000..4570542 --- /dev/null +++ b/ts/client/plugins.ts @@ -0,0 +1,6 @@ +// plugins for client module +import FormData from 'form-data'; + +export { + FormData as formData +}; \ No newline at end of file diff --git a/ts/client/smartrequestclient.ts b/ts/client/smartrequestclient.ts index 4af085f..fd03b22 100644 --- a/ts/client/smartrequestclient.ts +++ b/ts/client/smartrequestclient.ts @@ -1,6 +1,7 @@ import { CoreRequest, CoreResponse } from '../core/index.js'; -import * as plugins from '../core_node/plugins.js'; -import type { IAbstractRequestOptions } from '../core_base/types.js'; +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 } from './types/common.js'; import { @@ -18,7 +19,7 @@ import { createPaginatedResponse } from './features/pagination.js'; */ export class SmartRequestClient { private _url: string; - private _options: IAbstractRequestOptions = {}; + private _options: ICoreRequestOptions = {}; private _retries: number = 0; private _queryParams: Record = {}; private _paginationConfig?: TPaginationConfig; @@ -224,35 +225,35 @@ export class SmartRequestClient { /** * Make a GET request */ - async get(): Promise> { + async get(): Promise> { return this.execute('GET'); } /** * Make a POST request */ - async post(): Promise> { + async post(): Promise> { return this.execute('POST'); } /** * Make a PUT request */ - async put(): Promise> { + async put(): Promise> { return this.execute('PUT'); } /** * Make a DELETE request */ - async delete(): Promise> { + async delete(): Promise> { return this.execute('DELETE'); } /** * Make a PATCH request */ - async patch(): Promise> { + async patch(): Promise> { return this.execute('PATCH'); } @@ -297,7 +298,7 @@ export class SmartRequestClient { /** * Execute the HTTP request */ - private async execute(method?: HttpMethod): Promise> { + private async execute(method?: HttpMethod): Promise> { if (method) { this._options.method = method; } @@ -309,8 +310,9 @@ export class SmartRequestClient { for (let attempt = 0; attempt <= this._retries; attempt++) { try { - const response = await CoreRequest.create(this._url, this._options); - return response as CoreResponse; + const request = new CoreRequest(this._url, this._options as any); + const response = await request.fire(); + return response as ICoreResponse; } catch (error) { lastError = error as Error; diff --git a/ts/client/types/pagination.ts b/ts/client/types/pagination.ts index 5c38d73..e6e7b42 100644 --- a/ts/client/types/pagination.ts +++ b/ts/client/types/pagination.ts @@ -1,4 +1,5 @@ -import { type CoreResponse } from '../../core_node/index.js'; +import { type CoreResponse } from '../../core/index.js'; +import type { ICoreResponse } from '../../core_base/types.js'; /** * Pagination strategy options @@ -45,8 +46,8 @@ export interface LinkPaginationConfig { */ export interface CustomPaginationConfig { strategy: PaginationStrategy.CUSTOM; - hasNextPage: (response: CoreResponse) => boolean; - getNextPageParams: (response: CoreResponse, currentParams: Record) => Record; + hasNextPage: (response: ICoreResponse) => boolean; + getNextPageParams: (response: ICoreResponse, currentParams: Record) => Record; } /** @@ -62,5 +63,5 @@ export interface TPaginatedResponse { hasNextPage: boolean; // Whether there are more pages getNextPage: () => Promise>; // Function to get the next page getAllPages: () => Promise; // Function to get all remaining pages and combine - response: CoreResponse; // Original response + response: ICoreResponse; // Original response } \ No newline at end of file diff --git a/ts/core/index.ts b/ts/core/index.ts index 1549ca1..7e177b7 100644 --- a/ts/core/index.ts +++ b/ts/core/index.ts @@ -1,22 +1,30 @@ -import * as smartenv from '@push.rocks/smartenv'; +import * as plugins from './plugins.js'; // Export all base types - these are the public API export * from '../core_base/types.js'; -const smartenvInstance = new smartenv.Smartenv(); +const smartenvInstance = new plugins.smartenv.Smartenv(); -// Load the appropriate implementation based on environment -const implementation = await (async () => { - if (smartenvInstance.isNode) { - return smartenvInstance.getSafeNodeModule('../core_node/index.js'); - } else { - return import('../core_fetch/index.js'); - } -})(); +// Dynamically load the appropriate implementation +let CoreRequest: any; +let CoreResponse: any; -// Export the implementation classes -export const CoreRequest = implementation.CoreRequest; -export const CoreResponse = implementation.CoreResponse; +if (smartenvInstance.isNode) { + // In Node.js, load the node implementation + const modulePath = plugins.smartpath.join( + plugins.smartpath.dirname(import.meta.url), + '../core_node/index.js' + ) + console.log(modulePath); + const impl = await smartenvInstance.getSafeNodeModule(modulePath); + CoreRequest = impl.CoreRequest; + CoreResponse = impl.CoreResponse; +} else { + // In browser, load the fetch implementation + const impl = await import('../core_fetch/index.js'); + CoreRequest = impl.CoreRequest; + CoreResponse = impl.CoreResponse; +} -// Export CoreResponse as a type for type annotations -export type CoreResponse = InstanceType; +// Export the loaded implementations +export { CoreRequest, CoreResponse }; diff --git a/ts/core/plugins.ts b/ts/core/plugins.ts new file mode 100644 index 0000000..6449cee --- /dev/null +++ b/ts/core/plugins.ts @@ -0,0 +1,4 @@ +import * as smartenv from '@push.rocks/smartenv'; +import * as smartpath from '@push.rocks/smartpath/iso'; + +export { smartenv, smartpath }; diff --git a/ts/core_base/request.ts b/ts/core_base/request.ts index 3df6526..acfa284 100644 --- a/ts/core_base/request.ts +++ b/ts/core_base/request.ts @@ -3,7 +3,7 @@ import * as types from './types.js'; /** * Abstract Core Request class that defines the interface for all HTTP/HTTPS requests */ -export abstract class CoreRequest { +export abstract class CoreRequest { /** * Tests if a URL is a unix socket */ diff --git a/ts/core_base/response.ts b/ts/core_base/response.ts index d946b56..5b7d22b 100644 --- a/ts/core_base/response.ts +++ b/ts/core_base/response.ts @@ -3,14 +3,14 @@ import * as types from './types.js'; /** * Abstract Core Response class that provides a fetch-like API */ -export abstract class CoreResponse implements types.IAbstractResponse { +export abstract class CoreResponse implements types.ICoreResponse { protected consumed = false; // Public properties public abstract readonly ok: boolean; public abstract readonly status: number; public abstract readonly statusText: string; - public abstract readonly headers: types.AbstractHeaders; + public abstract readonly headers: types.Headers; public abstract readonly url: string; /** diff --git a/ts/core_base/types.ts b/ts/core_base/types.ts index a23e02a..f00bc9e 100644 --- a/ts/core_base/types.ts +++ b/ts/core_base/types.ts @@ -27,9 +27,10 @@ export interface IUrlEncodedField { } /** - * Abstract request options - platform agnostic + * Core request options - unified interface for all implementations */ -export interface IAbstractRequestOptions { +export interface ICoreRequestOptions { + // Common options method?: THttpMethod | string; // Allow string for compatibility headers?: any; // Allow any for platform compatibility keepAlive?: boolean; @@ -37,22 +38,39 @@ export interface IAbstractRequestOptions { queryParams?: { [key: string]: string }; timeout?: number; hardDataCuttingTimeout?: number; + + // Node.js specific options (ignored in fetch implementation) + agent?: any; + socketPath?: string; + hostname?: string; + port?: number; + path?: string; + + // Fetch API specific options (ignored in Node.js implementation) + credentials?: RequestCredentials; + mode?: RequestMode; + cache?: RequestCache; + redirect?: RequestRedirect; + referrer?: string; + referrerPolicy?: ReferrerPolicy; + integrity?: string; + signal?: AbortSignal; } /** - * Abstract response headers - platform agnostic + * Response headers - platform agnostic */ -export type AbstractHeaders = Record; +export type Headers = Record; /** - * Abstract response interface - platform agnostic + * Core response interface - platform agnostic */ -export interface IAbstractResponse { +export interface ICoreResponse { // Properties ok: boolean; status: number; statusText: string; - headers: AbstractHeaders; + headers: Headers; url: string; // Methods diff --git a/ts/core_fetch/request.ts b/ts/core_fetch/request.ts index 9f34e43..c8cf4c1 100644 --- a/ts/core_fetch/request.ts +++ b/ts/core_fetch/request.ts @@ -8,6 +8,11 @@ import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js'; export class CoreRequest extends AbstractCoreRequest { constructor(url: string, options: types.ICoreRequestOptions = {}) { super(url, options); + + // Check for unsupported Node.js-specific options + if (options.agent || options.socketPath) { + throw new Error('Node.js specific options (agent, socketPath) are not supported in browser/fetch implementation'); + } } /** diff --git a/ts/core_fetch/response.ts b/ts/core_fetch/response.ts index ab5ea9c..d770978 100644 --- a/ts/core_fetch/response.ts +++ b/ts/core_fetch/response.ts @@ -4,7 +4,7 @@ import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js'; /** * Fetch-based implementation of Core Response class */ -export class CoreResponse extends AbstractCoreResponse implements types.ICoreResponse { +export class CoreResponse extends AbstractCoreResponse implements types.IFetchResponse { private response: Response; private responseClone: Response; @@ -12,7 +12,7 @@ export class CoreResponse extends AbstractCoreResponse implements ty public readonly ok: boolean; public readonly status: number; public readonly statusText: string; - public readonly headers: types.AbstractHeaders; + public readonly headers: types.Headers; public readonly url: string; constructor(response: Response) { diff --git a/ts/core_fetch/types.ts b/ts/core_fetch/types.ts index f9e0203..212a213 100644 --- a/ts/core_fetch/types.ts +++ b/ts/core_fetch/types.ts @@ -4,24 +4,12 @@ import * as baseTypes from '../core_base/types.js'; export * from '../core_base/types.js'; /** - * Core request options for fetch-based implementation - * Extends RequestInit from the Fetch API + * Fetch-specific response extensions */ -export interface ICoreRequestOptions extends RequestInit { - // Override method to be more specific - method?: baseTypes.THttpMethod; - // Additional options not in RequestInit - requestBody?: any; - queryParams?: { [key: string]: string }; - timeout?: number; - hardDataCuttingTimeout?: number; - // keepAlive maps to keepalive in RequestInit - keepAlive?: boolean; -} - -/** - * Core response object for fetch implementation - */ -export interface ICoreResponse extends baseTypes.IAbstractResponse { - // Fetch-specific properties (all from base interface) +export interface IFetchResponse extends baseTypes.ICoreResponse { + // Fetch-specific methods + stream(): ReadableStream | null; + + // Access to raw Response object + raw(): Response; } \ No newline at end of file diff --git a/ts/core_node/request.ts b/ts/core_node/request.ts index c6a5bd0..650c497 100644 --- a/ts/core_node/request.ts +++ b/ts/core_node/request.ts @@ -39,6 +39,12 @@ export class CoreRequest extends AbstractCoreRequest extends AbstractCoreResponse implements types.ICoreResponse { +export class CoreResponse extends AbstractCoreResponse implements types.INodeResponse { private incomingMessage: plugins.http.IncomingMessage; private bodyBufferPromise: Promise | null = null; diff --git a/ts/core_node/types.ts b/ts/core_node/types.ts index cc88a69..d66b4ce 100644 --- a/ts/core_node/types.ts +++ b/ts/core_node/types.ts @@ -4,17 +4,6 @@ import * as baseTypes from '../core_base/types.js'; // Re-export base types export * from '../core_base/types.js'; -/** - * Core request options extending Node.js RequestOptions - * Node.js RequestOptions already includes method and headers - */ -export interface ICoreRequestOptions extends plugins.https.RequestOptions { - keepAlive?: boolean; - requestBody?: any; - queryParams?: { [key: string]: string }; - hardDataCuttingTimeout?: number; -} - /** * Extended IncomingMessage with body property (legacy compatibility) */ @@ -23,20 +12,10 @@ export interface IExtendedIncomingMessage extends plugins.http.Incoming } /** - * Core response object that provides fetch-like API with Node.js specific methods + * Node.js specific response extensions */ -export interface ICoreResponse extends baseTypes.IAbstractResponse { - // Properties - ok: boolean; - status: number; - statusText: string; - headers: plugins.http.IncomingHttpHeaders; - url: string; - - // Methods - json(): Promise; - text(): Promise; - arrayBuffer(): Promise; +export interface INodeResponse extends baseTypes.ICoreResponse { + // Node.js specific methods stream(): NodeJS.ReadableStream; // Legacy compatibility diff --git a/ts/index.ts b/ts/index.ts index 8702738..b2afaa3 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -3,7 +3,7 @@ export * from './client/index.js'; // Core exports for advanced usage export { CoreResponse } from './core/index.js'; -export type { IAbstractRequestOptions, IAbstractResponse } from './core_base/types.js'; +export type { ICoreRequestOptions, ICoreResponse } from './core_base/types.js'; // Default export for easier importing import { SmartRequestClient } from './client/smartrequestclient.js';