feat(core): Add Bun and Deno runtime support, unify core loader, unix-socket support and cross-runtime streaming/tests

This commit is contained in:
2025-11-16 22:50:19 +00:00
parent 32332309dc
commit 6211acd60b
22 changed files with 1447 additions and 92 deletions

View 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
View 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
View 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
View 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
View 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;
}