This release is 4 versions behind 0.2.0 — the latest version of @denyncrawford/print-tunnel. Jump to latest
@denyncrawford/print-tunnel@0.1.6
Works with
•JSR Score29%•It is unknown whether this package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




Publisheda year ago (0.1.6)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212import { 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; } }