feat(core): Add Bun and Deno runtime support, unify core loader, unix-socket support and cross-runtime streaming/tests
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartrequest',
|
||||
version: '4.3.8',
|
||||
version: '4.4.0',
|
||||
description: 'A module for modern HTTP/HTTPS requests with support for form data, file uploads, JSON, binary data, streams, and more.'
|
||||
}
|
||||
|
||||
@@ -447,15 +447,18 @@ export class SmartRequest<T = any> {
|
||||
requestDataFunc = (req: any) => {
|
||||
nodeStream.pipe(req);
|
||||
};
|
||||
// Remove the temporary stream reference
|
||||
delete (this._options as any).__nodeStream;
|
||||
// Don't delete __nodeStream yet - let CoreRequest implementations handle it
|
||||
// Node.js will use requestDataFunc, Bun/Deno will convert the stream
|
||||
} else if ((this._options as any).__rawStreamFunc) {
|
||||
requestDataFunc = (this._options as any).__rawStreamFunc;
|
||||
// Remove the temporary function reference
|
||||
delete (this._options as any).__rawStreamFunc;
|
||||
// Don't delete __rawStreamFunc yet - let CoreRequest implementations handle it
|
||||
}
|
||||
|
||||
|
||||
const request = new CoreRequest(this._url, this._options as any, requestDataFunc);
|
||||
|
||||
// Clean up temporary properties after CoreRequest has been created
|
||||
delete (this._options as any).__nodeStream;
|
||||
delete (this._options as any).__rawStreamFunc;
|
||||
const response = (await request.fire()) as ICoreResponse<R>;
|
||||
|
||||
// Check for 429 status if rate limit handling is enabled
|
||||
|
||||
@@ -5,12 +5,22 @@ export * from '../core_base/types.js';
|
||||
|
||||
const smartenvInstance = new plugins.smartenv.Smartenv();
|
||||
|
||||
// Dynamically load the appropriate implementation
|
||||
// Dynamically load the appropriate implementation based on runtime
|
||||
let CoreRequest: any;
|
||||
let CoreResponse: any;
|
||||
|
||||
if (smartenvInstance.isNode) {
|
||||
// In Node.js, load the node implementation
|
||||
if (smartenvInstance.isDeno) {
|
||||
// In Deno, load the Deno implementation with HttpClient-based unix socket support
|
||||
const impl = await import('../core_deno/index.js');
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
} else if (smartenvInstance.isBun) {
|
||||
// In Bun, load the Bun implementation with native fetch unix socket support
|
||||
const impl = await import('../core_bun/index.js');
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
} else if (smartenvInstance.isNode) {
|
||||
// In Node.js, load the Node.js implementation with native http/https unix socket support
|
||||
const modulePath = plugins.smartpath.join(
|
||||
plugins.smartpath.dirname(import.meta.url),
|
||||
'../core_node/index.js',
|
||||
@@ -19,7 +29,7 @@ if (smartenvInstance.isNode) {
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
} else {
|
||||
// In browser, load the fetch implementation
|
||||
// In browser, load the fetch implementation (no unix socket support)
|
||||
const impl = await import('../core_fetch/index.js');
|
||||
CoreRequest = impl.CoreRequest;
|
||||
CoreResponse = impl.CoreResponse;
|
||||
|
||||
3
ts/core_bun/index.ts
Normal file
3
ts/core_bun/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Core Bun exports - Bun's native fetch implementation with unix socket support
|
||||
export * from './response.js';
|
||||
export { CoreRequest } from './request.js';
|
||||
249
ts/core_bun/request.ts
Normal file
249
ts/core_bun/request.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse } from './response.js';
|
||||
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
|
||||
|
||||
/**
|
||||
* Bun implementation of Core Request class using native fetch with unix socket support
|
||||
*/
|
||||
export class CoreRequest extends AbstractCoreRequest<
|
||||
types.IBunRequestOptions,
|
||||
CoreResponse
|
||||
> {
|
||||
private timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
private abortController: AbortController | null = null;
|
||||
private requestDataFunc: ((req: any) => void) | null;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
options: types.IBunRequestOptions = {},
|
||||
requestDataFunc: ((req: any) => void) | null = null,
|
||||
) {
|
||||
super(url, options);
|
||||
this.requestDataFunc = requestDataFunc;
|
||||
|
||||
// Check for unsupported Node.js-specific options
|
||||
if (options.agent) {
|
||||
throw new Error(
|
||||
'Node.js specific option (agent) is not supported in Bun implementation',
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Node.js stream conversion if requestDataFunc is provided
|
||||
if (requestDataFunc && (options as any).__nodeStream) {
|
||||
// Convert Node.js stream to web ReadableStream for Bun
|
||||
const nodeStream = (options as any).__nodeStream;
|
||||
|
||||
// Bun can handle Node.js streams via Readable.toWeb if available
|
||||
// Or we can create a web stream that reads from the Node stream
|
||||
if (typeof (nodeStream as any).toWeb === 'function') {
|
||||
this.options.requestBody = (nodeStream as any).toWeb();
|
||||
} else {
|
||||
// Create web ReadableStream from Node.js stream
|
||||
this.options.requestBody = new ReadableStream({
|
||||
async start(controller) {
|
||||
nodeStream.on('data', (chunk: any) => {
|
||||
controller.enqueue(new Uint8Array(chunk));
|
||||
});
|
||||
nodeStream.on('end', () => {
|
||||
controller.close();
|
||||
});
|
||||
nodeStream.on('error', (err: any) => {
|
||||
controller.error(err);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if raw streaming function is provided (not supported in Bun)
|
||||
if (requestDataFunc && (options as any).__rawStreamFunc) {
|
||||
throw new Error(
|
||||
'Raw streaming with .raw() is not supported in Bun. Use .stream() with web ReadableStream instead.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full URL with query parameters
|
||||
*/
|
||||
private buildUrl(): string {
|
||||
// For unix sockets, we need to extract the HTTP path part
|
||||
if (CoreRequest.isUnixSocket(this.url)) {
|
||||
const { path } = CoreRequest.parseUnixSocketUrl(this.url);
|
||||
|
||||
// Build URL for the HTTP request (the hostname doesn't matter for unix sockets)
|
||||
if (
|
||||
!this.options.queryParams ||
|
||||
Object.keys(this.options.queryParams).length === 0
|
||||
) {
|
||||
return `http://localhost${path}`;
|
||||
}
|
||||
|
||||
const url = new URL(`http://localhost${path}`);
|
||||
Object.entries(this.options.queryParams).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Regular HTTP/HTTPS URL
|
||||
if (
|
||||
!this.options.queryParams ||
|
||||
Object.keys(this.options.queryParams).length === 0
|
||||
) {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
const url = new URL(this.url);
|
||||
Object.entries(this.options.queryParams).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our options to fetch RequestInit with Bun-specific extensions
|
||||
*/
|
||||
private buildFetchOptions(): RequestInit & { unix?: string } {
|
||||
const fetchOptions: RequestInit & { unix?: string } = {
|
||||
method: this.options.method,
|
||||
headers: this.options.headers,
|
||||
credentials: this.options.credentials,
|
||||
mode: this.options.mode,
|
||||
cache: this.options.cache,
|
||||
redirect: this.options.redirect,
|
||||
referrer: this.options.referrer,
|
||||
referrerPolicy: this.options.referrerPolicy,
|
||||
integrity: this.options.integrity,
|
||||
keepalive: this.options.keepAlive,
|
||||
signal: this.options.signal,
|
||||
};
|
||||
|
||||
// Handle unix socket
|
||||
if (CoreRequest.isUnixSocket(this.url)) {
|
||||
const { socketPath } = CoreRequest.parseUnixSocketUrl(this.url);
|
||||
fetchOptions.unix = socketPath;
|
||||
} else if (this.options.unix) {
|
||||
// Direct unix option was provided
|
||||
fetchOptions.unix = this.options.unix;
|
||||
} else if (this.options.socketPath) {
|
||||
// Legacy Node.js socketPath option - convert to Bun's unix option
|
||||
fetchOptions.unix = this.options.socketPath;
|
||||
}
|
||||
|
||||
// Handle request body
|
||||
if (this.options.requestBody !== undefined) {
|
||||
if (
|
||||
typeof this.options.requestBody === 'string' ||
|
||||
this.options.requestBody instanceof ArrayBuffer ||
|
||||
this.options.requestBody instanceof Uint8Array ||
|
||||
this.options.requestBody instanceof FormData ||
|
||||
this.options.requestBody instanceof URLSearchParams ||
|
||||
this.options.requestBody instanceof ReadableStream ||
|
||||
// Check for Buffer (Bun supports Node.js Buffer)
|
||||
(typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
|
||||
) {
|
||||
fetchOptions.body = this.options.requestBody as BodyInit;
|
||||
|
||||
// If streaming, we need to set duplex mode
|
||||
if (this.options.requestBody instanceof ReadableStream) {
|
||||
(fetchOptions as any).duplex = 'half';
|
||||
}
|
||||
} else {
|
||||
// Convert objects to JSON
|
||||
fetchOptions.body = JSON.stringify(this.options.requestBody);
|
||||
// Set content-type if not already set
|
||||
if (!fetchOptions.headers) {
|
||||
fetchOptions.headers = { 'Content-Type': 'application/json' };
|
||||
} else if (fetchOptions.headers instanceof Headers) {
|
||||
if (!fetchOptions.headers.has('Content-Type')) {
|
||||
fetchOptions.headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
} else if (
|
||||
typeof fetchOptions.headers === 'object' &&
|
||||
!Array.isArray(fetchOptions.headers)
|
||||
) {
|
||||
const headersObj = fetchOptions.headers as Record<string, string>;
|
||||
if (!headersObj['Content-Type']) {
|
||||
headersObj['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
|
||||
const timeout =
|
||||
this.options.hardDataCuttingTimeout || this.options.timeout;
|
||||
this.abortController = new AbortController();
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
}, timeout);
|
||||
fetchOptions.signal = this.abortController.signal;
|
||||
}
|
||||
|
||||
return fetchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a CoreResponse
|
||||
*/
|
||||
async fire(): Promise<CoreResponse> {
|
||||
const response = await this.fireCore();
|
||||
return new CoreResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw Response
|
||||
*/
|
||||
async fireCore(): Promise<Response> {
|
||||
const url = this.buildUrl();
|
||||
const options = this.buildFetchOptions();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
// Clear timeout on successful response
|
||||
this.clearTimeout();
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Clear timeout on error
|
||||
this.clearTimeout();
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timed out');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the timeout and abort controller
|
||||
*/
|
||||
private clearTimeout(): void {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
if (this.abortController) {
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create and fire a request
|
||||
*/
|
||||
static async create(
|
||||
url: string,
|
||||
options: types.IBunRequestOptions = {},
|
||||
): Promise<CoreResponse> {
|
||||
const request = new CoreRequest(url, options);
|
||||
return request.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience exports for backward compatibility
|
||||
*/
|
||||
export const isUnixSocket = CoreRequest.isUnixSocket;
|
||||
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;
|
||||
95
ts/core_bun/response.ts
Normal file
95
ts/core_bun/response.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
|
||||
|
||||
/**
|
||||
* Bun implementation of Core Response class that wraps native fetch Response
|
||||
*/
|
||||
export class CoreResponse<T = any>
|
||||
extends AbstractCoreResponse<T>
|
||||
implements types.IBunResponse<T>
|
||||
{
|
||||
private response: Response;
|
||||
private responseClone: Response;
|
||||
|
||||
// Public properties
|
||||
public readonly ok: boolean;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly headers: types.Headers;
|
||||
public readonly url: string;
|
||||
|
||||
constructor(response: Response) {
|
||||
super();
|
||||
// Clone the response so we can read the body multiple times if needed
|
||||
this.response = response;
|
||||
this.responseClone = response.clone();
|
||||
|
||||
this.ok = response.ok;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.url = response.url;
|
||||
|
||||
// Convert Headers to plain object
|
||||
this.headers = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
this.headers[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
async json(): Promise<T> {
|
||||
this.ensureNotConsumed();
|
||||
try {
|
||||
return await this.response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.arrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a readable stream (Web Streams API)
|
||||
*/
|
||||
stream(): ReadableStream<Uint8Array> | null {
|
||||
this.ensureNotConsumed();
|
||||
return this.response.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a Node.js-style stream
|
||||
* Bun supports Node.js streams, so we can provide this functionality
|
||||
*
|
||||
* Note: In Bun, you may also be able to use the web stream directly with stream() method
|
||||
*/
|
||||
streamNode(): never {
|
||||
// Bun primarily uses web streams and has excellent compatibility
|
||||
// For most use cases, use stream() which returns a standard ReadableStream
|
||||
throw new Error(
|
||||
'streamNode() is not available in Bun environment. Use stream() for web-style ReadableStream, which Bun fully supports.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw Response object
|
||||
*/
|
||||
raw(): Response {
|
||||
return this.responseClone;
|
||||
}
|
||||
}
|
||||
23
ts/core_bun/types.ts
Normal file
23
ts/core_bun/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as baseTypes from '../core_base/types.js';
|
||||
|
||||
// Re-export base types
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Bun-specific request options
|
||||
*/
|
||||
export interface IBunRequestOptions extends baseTypes.ICoreRequestOptions {
|
||||
/**
|
||||
* Unix domain socket path for Bun's fetch
|
||||
* When provided, the request will be sent over the unix socket instead of TCP
|
||||
*/
|
||||
unix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bun-specific response extensions
|
||||
*/
|
||||
export interface IBunResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Access to raw Response object
|
||||
raw(): Response;
|
||||
}
|
||||
23
ts/core_deno/deno.types.ts
Normal file
23
ts/core_deno/deno.types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Minimal Deno type definitions for compilation in Node.js environment
|
||||
* These types are only used during build-time type checking
|
||||
* At runtime, actual Deno APIs will be available in Deno environment
|
||||
*/
|
||||
|
||||
declare global {
|
||||
namespace Deno {
|
||||
interface HttpClient {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
interface CreateHttpClientOptions {
|
||||
proxy?: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
function createHttpClient(options: CreateHttpClientOptions): HttpClient;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
3
ts/core_deno/index.ts
Normal file
3
ts/core_deno/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Core Deno exports - Deno's native fetch implementation with unix socket support via HttpClient
|
||||
export * from './response.js';
|
||||
export { CoreRequest } from './request.js';
|
||||
295
ts/core_deno/request.ts
Normal file
295
ts/core_deno/request.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/// <reference path="./deno.types.ts" />
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse } from './response.js';
|
||||
import { CoreRequest as AbstractCoreRequest } from '../core_base/request.js';
|
||||
|
||||
/**
|
||||
* Cache for HttpClient instances keyed by socket path
|
||||
* This prevents creating multiple clients for the same socket
|
||||
*/
|
||||
const httpClientCache = new Map<string, Deno.HttpClient>();
|
||||
|
||||
/**
|
||||
* Deno implementation of Core Request class using native fetch with unix socket support via HttpClient
|
||||
*/
|
||||
export class CoreRequest extends AbstractCoreRequest<
|
||||
types.IDenoRequestOptions,
|
||||
CoreResponse
|
||||
> {
|
||||
private timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
private abortController: AbortController | null = null;
|
||||
private createdClient: Deno.HttpClient | null = null;
|
||||
private requestDataFunc: ((req: any) => void) | null;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
options: types.IDenoRequestOptions = {},
|
||||
requestDataFunc: ((req: any) => void) | null = null,
|
||||
) {
|
||||
super(url, options);
|
||||
this.requestDataFunc = requestDataFunc;
|
||||
|
||||
// Check for unsupported Node.js-specific options
|
||||
if (options.agent) {
|
||||
throw new Error(
|
||||
'Node.js specific option (agent) is not supported in Deno implementation',
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Node.js stream conversion if requestDataFunc is provided
|
||||
if (requestDataFunc && (options as any).__nodeStream) {
|
||||
// Convert Node.js stream to web ReadableStream for Deno
|
||||
const nodeStream = (options as any).__nodeStream;
|
||||
|
||||
// Create web ReadableStream from Node.js stream
|
||||
this.options.requestBody = new ReadableStream({
|
||||
async start(controller) {
|
||||
nodeStream.on('data', (chunk: any) => {
|
||||
controller.enqueue(new Uint8Array(chunk));
|
||||
});
|
||||
nodeStream.on('end', () => {
|
||||
controller.close();
|
||||
});
|
||||
nodeStream.on('error', (err: any) => {
|
||||
controller.error(err);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Throw error if raw streaming function is provided (not supported in Deno)
|
||||
if (requestDataFunc && (options as any).__rawStreamFunc) {
|
||||
throw new Error(
|
||||
'Raw streaming with .raw() is not supported in Deno. Use .stream() with web ReadableStream instead.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create an HttpClient for unix socket communication
|
||||
*/
|
||||
private getHttpClient(): Deno.HttpClient | undefined {
|
||||
// If client was explicitly provided, use it
|
||||
if (this.options.client) {
|
||||
return this.options.client;
|
||||
}
|
||||
|
||||
// Check if we need a unix socket client
|
||||
const socketPath = this.options.socketPath ||
|
||||
(CoreRequest.isUnixSocket(this.url)
|
||||
? CoreRequest.parseUnixSocketUrl(this.url).socketPath
|
||||
: null);
|
||||
|
||||
if (!socketPath) {
|
||||
return undefined; // Use default client
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (httpClientCache.has(socketPath)) {
|
||||
return httpClientCache.get(socketPath);
|
||||
}
|
||||
|
||||
// Create new HttpClient for this socket
|
||||
const client = Deno.createHttpClient({
|
||||
proxy: {
|
||||
url: `unix://${socketPath}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Cache it
|
||||
httpClientCache.set(socketPath, client);
|
||||
this.createdClient = client;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full URL with query parameters
|
||||
*/
|
||||
private buildUrl(): string {
|
||||
// For unix sockets, we need to extract the HTTP path part
|
||||
if (CoreRequest.isUnixSocket(this.url)) {
|
||||
const { path } = CoreRequest.parseUnixSocketUrl(this.url);
|
||||
|
||||
// Build URL for the HTTP request (the hostname doesn't matter for unix sockets)
|
||||
if (
|
||||
!this.options.queryParams ||
|
||||
Object.keys(this.options.queryParams).length === 0
|
||||
) {
|
||||
return `http://localhost${path}`;
|
||||
}
|
||||
|
||||
const url = new URL(`http://localhost${path}`);
|
||||
Object.entries(this.options.queryParams).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Regular HTTP/HTTPS URL
|
||||
if (
|
||||
!this.options.queryParams ||
|
||||
Object.keys(this.options.queryParams).length === 0
|
||||
) {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
const url = new URL(this.url);
|
||||
Object.entries(this.options.queryParams).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our options to fetch RequestInit
|
||||
*/
|
||||
private buildFetchOptions(): RequestInit & { client?: Deno.HttpClient } {
|
||||
const fetchOptions: RequestInit & { client?: Deno.HttpClient } = {
|
||||
method: this.options.method,
|
||||
headers: this.options.headers,
|
||||
credentials: this.options.credentials,
|
||||
mode: this.options.mode,
|
||||
cache: this.options.cache,
|
||||
redirect: this.options.redirect,
|
||||
referrer: this.options.referrer,
|
||||
referrerPolicy: this.options.referrerPolicy,
|
||||
integrity: this.options.integrity,
|
||||
keepalive: this.options.keepAlive,
|
||||
signal: this.options.signal,
|
||||
};
|
||||
|
||||
// Set the HttpClient (for unix sockets or custom configurations)
|
||||
const client = this.getHttpClient();
|
||||
if (client) {
|
||||
fetchOptions.client = client;
|
||||
}
|
||||
|
||||
// Handle request body
|
||||
if (this.options.requestBody !== undefined) {
|
||||
if (
|
||||
typeof this.options.requestBody === 'string' ||
|
||||
this.options.requestBody instanceof ArrayBuffer ||
|
||||
this.options.requestBody instanceof Uint8Array ||
|
||||
this.options.requestBody instanceof FormData ||
|
||||
this.options.requestBody instanceof URLSearchParams ||
|
||||
this.options.requestBody instanceof ReadableStream ||
|
||||
// Check for Buffer (Deno provides Buffer via Node.js compatibility)
|
||||
(typeof Buffer !== 'undefined' && this.options.requestBody instanceof Buffer)
|
||||
) {
|
||||
fetchOptions.body = this.options.requestBody as BodyInit;
|
||||
|
||||
// If streaming, we need to set duplex mode
|
||||
if (this.options.requestBody instanceof ReadableStream) {
|
||||
(fetchOptions as any).duplex = 'half';
|
||||
}
|
||||
} else {
|
||||
// Convert objects to JSON
|
||||
fetchOptions.body = JSON.stringify(this.options.requestBody);
|
||||
// Set content-type if not already set
|
||||
if (!fetchOptions.headers) {
|
||||
fetchOptions.headers = { 'Content-Type': 'application/json' };
|
||||
} else if (fetchOptions.headers instanceof Headers) {
|
||||
if (!fetchOptions.headers.has('Content-Type')) {
|
||||
fetchOptions.headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
} else if (
|
||||
typeof fetchOptions.headers === 'object' &&
|
||||
!Array.isArray(fetchOptions.headers)
|
||||
) {
|
||||
const headersObj = fetchOptions.headers as Record<string, string>;
|
||||
if (!headersObj['Content-Type']) {
|
||||
headersObj['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if (this.options.timeout || this.options.hardDataCuttingTimeout) {
|
||||
const timeout =
|
||||
this.options.hardDataCuttingTimeout || this.options.timeout;
|
||||
this.abortController = new AbortController();
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
}, timeout);
|
||||
fetchOptions.signal = this.abortController.signal;
|
||||
}
|
||||
|
||||
return fetchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return a CoreResponse
|
||||
*/
|
||||
async fire(): Promise<CoreResponse> {
|
||||
const response = await this.fireCore();
|
||||
return new CoreResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the request and return the raw Response
|
||||
*/
|
||||
async fireCore(): Promise<Response> {
|
||||
const url = this.buildUrl();
|
||||
const options = this.buildFetchOptions();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
// Clear timeout on successful response
|
||||
this.clearTimeout();
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Clear timeout on error
|
||||
this.clearTimeout();
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timed out');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the timeout and abort controller
|
||||
* Note: We don't close the HttpClient here as it's cached for reuse
|
||||
*/
|
||||
private clearTimeout(): void {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
if (this.abortController) {
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create and fire a request
|
||||
*/
|
||||
static async create(
|
||||
url: string,
|
||||
options: types.IDenoRequestOptions = {},
|
||||
): Promise<CoreResponse> {
|
||||
const request = new CoreRequest(url, options);
|
||||
return request.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to clear the HttpClient cache
|
||||
* Call this when you want to force new clients to be created
|
||||
*/
|
||||
static clearClientCache(): void {
|
||||
httpClientCache.forEach((client) => {
|
||||
client.close();
|
||||
});
|
||||
httpClientCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience exports for backward compatibility
|
||||
*/
|
||||
export const isUnixSocket = CoreRequest.isUnixSocket;
|
||||
export const parseUnixSocketUrl = CoreRequest.parseUnixSocketUrl;
|
||||
91
ts/core_deno/response.ts
Normal file
91
ts/core_deno/response.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as types from './types.js';
|
||||
import { CoreResponse as AbstractCoreResponse } from '../core_base/response.js';
|
||||
|
||||
/**
|
||||
* Deno implementation of Core Response class that wraps native fetch Response
|
||||
*/
|
||||
export class CoreResponse<T = any>
|
||||
extends AbstractCoreResponse<T>
|
||||
implements types.IDenoResponse<T>
|
||||
{
|
||||
private response: Response;
|
||||
private responseClone: Response;
|
||||
|
||||
// Public properties
|
||||
public readonly ok: boolean;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly headers: types.Headers;
|
||||
public readonly url: string;
|
||||
|
||||
constructor(response: Response) {
|
||||
super();
|
||||
// Clone the response so we can read the body multiple times if needed
|
||||
this.response = response;
|
||||
this.responseClone = response.clone();
|
||||
|
||||
this.ok = response.ok;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.url = response.url;
|
||||
|
||||
// Convert Headers to plain object
|
||||
this.headers = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
this.headers[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse response as JSON
|
||||
*/
|
||||
async json(): Promise<T> {
|
||||
this.ensureNotConsumed();
|
||||
try {
|
||||
return await this.response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as text
|
||||
*/
|
||||
async text(): Promise<string> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as ArrayBuffer
|
||||
*/
|
||||
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||
this.ensureNotConsumed();
|
||||
return await this.response.arrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response as a readable stream (Web Streams API)
|
||||
*/
|
||||
stream(): ReadableStream<Uint8Array> | null {
|
||||
this.ensureNotConsumed();
|
||||
return this.response.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js stream method - not available in Deno's standard mode
|
||||
* Throws an error as Deno uses web-standard ReadableStream
|
||||
*/
|
||||
streamNode(): never {
|
||||
throw new Error(
|
||||
'streamNode() is not available in Deno environment. Use stream() for web-style ReadableStream.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw Response object
|
||||
*/
|
||||
raw(): Response {
|
||||
return this.responseClone;
|
||||
}
|
||||
}
|
||||
24
ts/core_deno/types.ts
Normal file
24
ts/core_deno/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/// <reference path="./deno.types.ts" />
|
||||
import * as baseTypes from '../core_base/types.js';
|
||||
|
||||
// Re-export base types
|
||||
export * from '../core_base/types.js';
|
||||
|
||||
/**
|
||||
* Deno-specific request options
|
||||
*/
|
||||
export interface IDenoRequestOptions extends baseTypes.ICoreRequestOptions {
|
||||
/**
|
||||
* Deno HttpClient instance for custom configurations including unix sockets
|
||||
* If not provided and socketPath is specified, a client will be created automatically
|
||||
*/
|
||||
client?: Deno.HttpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deno-specific response extensions
|
||||
*/
|
||||
export interface IDenoResponse<T = any> extends baseTypes.ICoreResponse<T> {
|
||||
// Access to raw Response object
|
||||
raw(): Response;
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as path from 'path';
|
||||
import * as stream from 'stream';
|
||||
|
||||
export { http, https, fs, path };
|
||||
export { http, https, fs, path, stream };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
@@ -147,6 +147,12 @@ export class CoreRequest extends AbstractCoreRequest<
|
||||
this.options.requestBody.pipe(request).on('finish', () => {
|
||||
request.end();
|
||||
});
|
||||
} else if (this.options.requestBody instanceof ReadableStream) {
|
||||
// Convert web ReadableStream to Node.js Readable stream
|
||||
const nodeStream = plugins.stream.Readable.fromWeb(this.options.requestBody as any);
|
||||
nodeStream.pipe(request).on('finish', () => {
|
||||
request.end();
|
||||
});
|
||||
} else {
|
||||
// Write body as-is - caller is responsible for serialization
|
||||
const bodyData =
|
||||
|
||||
Reference in New Issue
Block a user