Skip to main content
Home
This release is 4 versions behind 0.2.0 — the latest version of @denyncrawford/print-tunnel. Jump to latest
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.1.6)
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(); // Reset backoff on activity currentPollInterval = minPollInterval; const url = new URL(path, localBaseUrl); if (query) { Object.entries(query).forEach(([key, value]) => { url.searchParams.set(key, value as string); }); } const response = await fetch(url.toString(), { method, headers, body: body ? JSON.stringify(body) : undefined, }); const responseData = { status: response.status, headers: Object.fromEntries(response.headers.entries()), body: await response.text(), }; await fetch(`${proxyUrl}/tunnel/${name}/response/${request.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(responseData), }); } catch (error) { console.error('Error processing request:', error); await fetch(`${proxyUrl}/tunnel/${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; // Exponential backoff up to maxPollInterval if (timeSinceLastActivity > 30000) { // 30 seconds of inactivity currentPollInterval = Math.min(currentPollInterval * 1.5, maxPollInterval); } } async function pollRequests() { while (isRunning) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 25000); // 25s timeout const response = await fetch(`${proxyUrl}/tunnel/${name}/requests`, { signal: controller.signal, headers: { 'Prefer': 'wait=20', // Ask server to wait up to 20 seconds }, }); 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 (error) { if (error.name === 'AbortError') { // Long polling timeout, continue normally await adjustPollInterval(); } else { console.error('Polling error:', error); setState('reconnecting'); await new Promise(resolve => setTimeout(resolve, 5000)); // Wait before retry } } await new Promise(resolve => setTimeout(resolve, currentPollInterval)); } } // Keep-alive heartbeat async function sendHeartbeat() { while (isRunning) { try { await fetch(`${proxyUrl}/tunnel/${name}/heartbeat`, { method: 'POST', }); } catch (error) { console.error('Heartbeat error:', error); setState('reconnecting'); } await new Promise(resolve => setTimeout(resolve, 30000)); // Every 30 seconds } } try { const response = await fetch(`${proxyUrl}/tunnel/${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}`); // Start polling and heartbeat pollPromise = pollRequests(); heartbeatPromise = sendHeartbeat(); // Return client interface return { disconnect: async () => { isRunning = false; setState('disconnected'); await Promise.all([pollPromise, heartbeatPromise]); try { await fetch(`${proxyUrl}/tunnel/${name}/register`, { method: 'DELETE' }); } catch (error) { console.error('Error during disconnect:', error); } }, getProxyUrl: () => `${proxyUrl}/tunnel/${name}`, getName: () => name, getState: () => connectionState, }; } catch (error) { console.error('Failed to create tunnel:', error); setState('disconnected'); throw error; } }