// Connection choice: DIRECT (same network, a TCP stream through the PHP side: Tcp = plain socket on the desktop,// the JumPHP runtime in a browser) raced against the RELAY (the page's own WebSocket, ciphertext only).// Direct gets a head start (1.5 s); the first transport that opens wins, the other is closed.import { Channel } from './channel.js';const b64 = (bytes) => { let s = ''; for (const b of bytes) s += String.fromCharCode(b); return btoa(s); };const unb64 = (t) => Uint8Array.from(atob(t), (c) => c.charCodeAt(0));const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const never = new Promise(() => {});const handlers = new Map();let wired = false;function wire() { if (wired || !window.jumphp) return; wired = true; window.jumphp.on('direct', (ev) => handlers.get(ev.id)?.(ev));}export function splitHostPort(hint) { const m = /^\[([^\]]+)\]:(\d+)$/.exec(hint) || /^([^:]+):(\d+)$/.exec(hint); return m ? { host: m[1], port: Number(m[2]) } : null;}/** One direct attempt: {ready: Promise<{transport, close}>, cancel()} */function directAttempt(hint) { const target = splitHostPort(hint); const id = 'd' + Math.random().toString(36).slice(2, 10); let dataCb = null; let closeCb = null; const pending = []; const transport = Channel.streamTransport({ send: (bytes) => window.jumphp.invoke('direct.write', { id, data: b64(bytes) }), onData: (cb) => { dataCb = cb; for (const c of pending.splice(0)) cb(c); }, onClose: (cb) => { closeCb = cb; }, close: () => window.jumphp.invoke('direct.close', { id }), }); const cancel = () => { handlers.delete(id); window.jumphp.invoke('direct.close', { id }).catch(() => {}); }; const ready = new Promise((resolve, reject) => { if (!target) return reject(new Error('bad hint')); handlers.set(id, (ev) => { if (ev.type === 'open') resolve({ transport, cancel }); else if (ev.type === 'data') { const d = unb64(ev.data); dataCb ? dataCb(d) : pending.push(d); } else if (ev.type === 'error') reject(new Error(ev.message || 'direct failed')); else if (ev.type === 'close') { reject(new Error('closed')); closeCb?.(ev.message); } }); window.jumphp.invoke('direct.open', { id, ...target }).then((r) => { if (r && r.ok === false) reject(new Error(r.error)); }, reject); }); ready.catch(() => handlers.delete(id)); return { ready, cancel };}/** Direct is worth trying on the desktop, or in a browser when the JumPHP runtime is ready (net_mode auto). */export async function directPossible(settings, hints) { if (!hints.length || !window.jumphp) return false; wire(); if (settings.native) return true; if (settings.net_mode === 'server') return false; try { return (await window.jumphp.invoke('runtime.state', {})).state === 'ready'; } catch { return false; }}function openChannel(key, transport) { return Channel.open({ role: 'remote', key, transport });}function waitOpen(ch, ms) { return new Promise((resolve, reject) => { if (ch.isOpen) return resolve(ch); const t = setTimeout(() => reject(new Error('timeout')), ms); ch.on('open', () => { clearTimeout(t); resolve(ch); }); ch.on('close', () => { clearTimeout(t); reject(new Error('closed')); }); });}/** @returns {Promise<{channel, via:'direct'|'relay'}>} */export async function connect({ pairing, settings, headStart = (settings.direct_head_start || 1.5) * 1000, relayTimeout = 20000 }) { const attempts = (await directPossible(settings, pairing.direct)) ? pairing.direct.slice(0, 3).map(directAttempt) : []; const direct = attempts.length ? Promise.any(attempts.map((a) => a.ready)) : null; const dropAttempts = (keep) => attempts.forEach((a) => { if (a.cancel !== keep) a.cancel(); }); const viaDirect = async (won) => { dropAttempts(won.cancel); const channel = await openChannel(pairing.key, won.transport); return { channel, via: 'direct' }; }; if (direct) { const first = await Promise.race([direct.catch(() => null), sleep(headStart).then(() => 'late')]); if (first && first !== 'late') return viaDirect(first); } const relayUrl = settings.relay_url; const relayChannel = await openChannel(pairing.key, Channel.relayTransport({ url: relayUrl, room: pairing.room, role: 'remote' })); const relay = waitOpen(relayChannel, relayTimeout).then(() => ({ channel: relayChannel, via: 'relay' })); const late = direct ? direct.then(viaDirect, () => never) : never; const winner = await Promise.race([relay, late]); if (winner.via === 'direct') relayChannel.close(); else dropAttempts(null); return winner;}