capacitor-cors-bypass-enhanced
Version:
Enhanced Capacitor plugin for CORS bypass with HTTP/2, HTTP/3, gRPC, GraphQL, file operations, and advanced networking features. Modular TypeScript definitions for better maintainability.
223 lines (192 loc) • 6.63 kB
text/typescript
import type {
StreamRequestOptions,
StreamChunkEvent,
StreamStatusEvent,
} from '../definitions';
import { isCrossOrigin } from './utils';
/**
* Stream Manager
* Handles streaming HTTP requests with CORS bypass
*/
export class StreamManager {
private proxyServerUrl: string | null;
private streamControllers = new Map<string, AbortController>();
private streamCounter = 0;
private notifyListeners: (eventName: string, data: any) => void;
constructor(
proxyServerUrl: string | null,
notifyListeners: (eventName: string, data: any) => void
) {
this.proxyServerUrl = proxyServerUrl;
this.notifyListeners = notifyListeners;
}
/**
* Make a streaming HTTP request - supports AI model streaming output
*/
async streamRequest(options: StreamRequestOptions): Promise<{ streamId: string }> {
const streamId = `stream_${++this.streamCounter}`;
const {
url,
method = 'POST',
headers = {},
data,
params,
timeout = 60000,
followRedirects = true,
} = options;
// Build URL with query parameters
let requestUrl = url;
if (params) {
const urlParams = new URLSearchParams(params);
requestUrl += (url.includes('?') ? '&' : '?') + urlParams.toString();
}
// Use proxy server if available and URL is cross-origin
let finalUrl = requestUrl;
if (this.proxyServerUrl && isCrossOrigin(requestUrl)) {
console.log(`🔧 Using proxy server for streaming: ${requestUrl}`);
finalUrl = `${this.proxyServerUrl}/proxy/${encodeURIComponent(requestUrl)}`;
}
// Create AbortController for this stream
const controller = new AbortController();
this.streamControllers.set(streamId, controller);
// Set timeout
const timeoutId = setTimeout(() => {
controller.abort();
this.notifyListeners('streamStatus', {
streamId,
status: 'error',
error: 'Request timeout',
} as StreamStatusEvent);
}, timeout);
try {
// Prepare fetch options
const fetchOptions: RequestInit = {
method,
headers: {
...headers,
'Accept': 'text/event-stream, application/json, text/plain, */*',
},
signal: controller.signal,
redirect: followRedirects ? 'follow' : 'manual',
};
// Add body for methods that support it
if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
if (typeof data === 'string') {
fetchOptions.body = data;
} else {
fetchOptions.body = JSON.stringify(data);
if (!headers['Content-Type']) {
fetchOptions.headers = {
...fetchOptions.headers,
'Content-Type': 'application/json',
};
}
}
}
console.log(`🌊 Starting stream request: ${streamId} to ${finalUrl}`);
// Start the fetch request
fetch(finalUrl, fetchOptions)
.then(async (response) => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Convert headers to plain object
const responseHeaders: { [key: string]: string } = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
// Notify stream started
this.notifyListeners('streamStatus', {
streamId,
status: 'started',
statusCode: response.status,
headers: responseHeaders,
} as StreamStatusEvent);
// Read the stream
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('Response body is not readable');
}
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// Stream completed
this.notifyListeners('streamChunk', {
streamId,
data: '',
done: true,
} as StreamChunkEvent);
this.notifyListeners('streamStatus', {
streamId,
status: 'completed',
} as StreamStatusEvent);
this.streamControllers.delete(streamId);
break;
}
// Decode and send chunk
const chunk = decoder.decode(value, { stream: true });
this.notifyListeners('streamChunk', {
streamId,
data: chunk,
done: false,
} as StreamChunkEvent);
}
} catch (error: any) {
if (error.name === 'AbortError') {
this.notifyListeners('streamStatus', {
streamId,
status: 'cancelled',
} as StreamStatusEvent);
} else {
throw error;
}
}
})
.catch((error) => {
clearTimeout(timeoutId);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.notifyListeners('streamChunk', {
streamId,
data: '',
done: true,
error: errorMessage,
} as StreamChunkEvent);
this.notifyListeners('streamStatus', {
streamId,
status: 'error',
error: errorMessage,
} as StreamStatusEvent);
this.streamControllers.delete(streamId);
});
return { streamId };
} catch (error) {
clearTimeout(timeoutId);
this.streamControllers.delete(streamId);
throw error;
}
}
/**
* Cancel a streaming request
*/
async cancelStream(options: { streamId: string }): Promise<void> {
const { streamId } = options;
const controller = this.streamControllers.get(streamId);
if (controller) {
controller.abort();
this.streamControllers.delete(streamId);
this.notifyListeners('streamStatus', {
streamId,
status: 'cancelled',
} as StreamStatusEvent);
}
}
/**
* Get all active stream controllers
*/
getStreamControllers(): Map<string, AbortController> {
return this.streamControllers;
}
}