Skip to main content
Home
Works with
It is unknown whether this package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
It is unknown whether this package works with Cloudflare Workers
It is unknown whether this package works with Node.js
It is unknown whether this package works with Deno
It is unknown whether this package works with Bun
It is unknown whether this package works with Browsers
JSR Score29%
Publisheda year ago (0.2.0)
import { adjectives, animals, colors, type Config, uniqueNamesGenerator, } from "npm:unique-names-generator@4.7.1"; export type ConnectionState = "connecting" | "connected" | "disconnected" | "reconnecting"; export interface ProxyClientOptions { name?: string; port?: number; maxPollInterval?: number; minPollInterval?: number; onStateChange?: (state: ConnectionState) => void; onNameChange?: (newName: string, oldName?: string) => void; } export interface ProxyClient { disconnect: () => Promise<void>; getProxyUrl: () => string; getName: () => string; getState: () => ConnectionState; } const customNameConfig: Config = { dictionaries: [adjectives, colors, animals], separator: "-", length: 2, }; export async function createProxyClient( proxyUrl = "http://localhost:3000", options: ProxyClientOptions = {}, ): Promise<ProxyClient> { let name = options.name || uniqueNamesGenerator(customNameConfig); const port = options.port || 3000; const minPollInterval = options.minPollInterval || 1000; const maxPollInterval = options.maxPollInterval || 10000; const localBaseUrl = `http://localhost:${port}`; const onStateChange = options.onStateChange; const onNameChange = options.onNameChange; let isRunning = true; let currentPollInterval = minPollInterval; let lastActivityTimestamp = Date.now(); let connectionState: ConnectionState = "connecting"; let pollPromise: Promise<void>; let heartbeatPromise: Promise<void>; function setState(newState: ConnectionState) { connectionState = newState; onStateChange?.(newState); } async function processRequest(request: any) { try { const { method, path, headers, body, query } = request; lastActivityTimestamp = Date.now(); currentPollInterval = minPollInterval; const url = new URL(path, localBaseUrl); if (query) { Object.entries(query).forEach(([key, value]) => { url.searchParams.set(key, value as string); }); } let requestBody: BodyInit | null = null; const requestHeaders = new Headers(headers); if (body) { if (body.type === "multipart" || body.type === "binary") { // Reconstruct binary data from chunks const stream = new ReadableStream({ start(controller) { for (const chunk of body.chunks) { controller.enqueue(new Uint8Array(chunk)); } controller.close(); } }); requestBody = stream; requestHeaders.set("content-type", body.contentType); } else if (body.type === "text") { requestBody = body.content; } else if (typeof body === "string") { requestBody = body; } else { requestBody = JSON.stringify(body); } } const response = await fetch(url.toString(), { method, headers: requestHeaders, body: requestBody, // @ts-ignore - Deno types are incorrect duplex: "half" // Required for streaming requests }); // Stream the response const responseStream = response.body; const chunks: number[][] = []; if (responseStream) { const reader = responseStream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(Array.from(value)); } } finally { reader.releaseLock(); } } await fetch(`${proxyUrl}/${name}/response/${request.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: response.status, headers: Object.fromEntries(response.headers.entries()), body: { type: "stream", chunks } }), }); } catch (e) { const error = e as Error & { status?: number }; console.error('Error processing request:', error); await fetch(`${proxyUrl}/${name}/response/${request.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: error.message }), }), }); } } async function adjustPollInterval() { const timeSinceLastActivity = Date.now() - lastActivityTimestamp; if (timeSinceLastActivity > 30000) { currentPollInterval = Math.min(currentPollInterval * 1.5, maxPollInterval); } } async function pollRequests() { while (isRunning) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 25000); const response = await fetch(`${proxyUrl}/${name}/requests`, { signal: controller.signal, headers: { 'Prefer': 'wait=20', }, }); clearTimeout(timeout); if (response.ok) { const requests = await response.json(); if (requests.length > 0) { for (const request of requests) { await processRequest(request); } } else { await adjustPollInterval(); } } } catch (e) { const error = e as Error; if (error.name === 'AbortError') { await adjustPollInterval(); } else { console.error('Polling error:', error); setState('reconnecting'); await new Promise(resolve => setTimeout(resolve, 5000)); } } await new Promise(resolve => setTimeout(resolve, currentPollInterval)); } } async function sendHeartbeat() { while (isRunning) { try { await fetch(`${proxyUrl}/${name}/heartbeat`, { method: 'POST', }); } catch (error) { console.error('Heartbeat error:', error); setState('reconnecting'); } await new Promise(resolve => setTimeout(resolve, 30000)); } } try { const response = await fetch(`${proxyUrl}/${name}/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port }), }); if (!response.ok) { throw new Error(`Registration failed: ${response.statusText}`); } const result = await response.json(); if (result.name && result.name !== name) { const oldName = name; name = result.name; onNameChange?.(name, oldName); } setState('connected'); console.log(`Tunnel registered as: ${name}`); pollPromise = pollRequests(); heartbeatPromise = sendHeartbeat(); return { disconnect: async () => { isRunning = false; setState('disconnected'); await Promise.all([pollPromise, heartbeatPromise]); try { await fetch(`${proxyUrl}/${name}/register`, { method: 'DELETE' }); } catch (error) { console.error('Error during disconnect:', error); } }, getProxyUrl: () => `${proxyUrl}/${name}`, getName: () => name, getState: () => connectionState, }; } catch (error) { console.error('Failed to create tunnel:', error); setState('disconnected'); throw error; } }