/** * Bare worker: real `holesail` package (native bare-tcp / require.addon). * * Started from the Pear UI via pear-run (not imported in the Electron renderer). * Holepunch GUI apps put Bare-native code in a worker; the UI talks over pear-pipe. * * Protocol: newline-delimited JSON request/response * → { id, method: 'connect'|'disconnect'|'list'|'ping', params? } * ← { id, ok: true, result } | { id, ok: false, error } */ /* global Pear */ 'use strict' const pipeFactory = require('pear-pipe') const Holesail = require('holesail') const pipe = typeof pipeFactory === 'function' ? pipeFactory() : pipeFactory if (!pipe) { console.error('[holesail-local] not started as a pear-run child (no pear-pipe)') try { Pear.exit(1) } catch { process.exit(1) } } // Stay up for multiple connect/disconnect RPCs until parent closes the pipe. if ('autoexit' in pipe) pipe.autoexit = true /** @type {Map} */ const localClients = new Map() function send(msg) { try { pipe.write(JSON.stringify(msg) + '\n') } catch (err) { console.error('[holesail-local] write failed', err) } } /** * @param {string} [host] * @returns {Promise} */ function findFreePort(host = '127.0.0.1') { // Prefer Node/Bare builtin net (not the bare-net npm alias from hyper-cmd-lib-net's deps // when resolved incorrectly). Under Bare, require('net') is the runtime builtin. const net = require('net') return new Promise((resolve, reject) => { const s = net.createServer() s.listen(0, host, () => { const addr = s.address() const port = typeof addr === 'object' && addr ? addr.port : 0 s.close((err) => (err ? reject(err) : resolve(port))) }) s.on('error', reject) }) } /** * @param {string} urlOrKey * @param {{ localPort?: number, host?: string }} [opts] */ async function connect(urlOrKey, opts = {}) { const url = String(urlOrKey || '').trim() if (!url) throw new Error('Holesail URL required') if (localClients.has(url)) { const e = localClients.get(url) return { url: e.url, localPort: e.localPort, host: e.host, state: e.info?.state, secure: e.info?.secure, } } const host = opts.host || '127.0.0.1' const localPort = opts.localPort || (await findFreePort(host)) const instance = new Holesail({ client: true, key: url, port: localPort, host, log: false, }) await instance.ready() const info = instance.info || {} const entry = { instance, info, localPort: info.port || localPort, host: info.host || host, url, } // Prefer loopback for browser open when bound to all interfaces if (entry.host === '0.0.0.0' || entry.host === '::') entry.host = '127.0.0.1' localClients.set(url, entry) return { url: entry.url, localPort: entry.localPort, host: entry.host, state: info.state || 'listening', secure: info.secure, protocol: info.protocol, } } /** * @param {string} urlOrKey */ async function disconnect(urlOrKey) { const url = String(urlOrKey || '').trim() const entry = localClients.get(url) if (!entry) return false localClients.delete(url) try { await entry.instance.close() } catch { // ignore } return true } function list() { return [...localClients.values()].map((e) => ({ url: e.url, localPort: e.localPort, host: e.host, state: e.info?.state, })) } async function handle(msg) { const id = msg?.id const method = msg?.method const params = msg?.params || {} try { let result switch (method) { case 'ping': result = { pong: true, runtime: 'bare-holesail-worker' } break case 'connect': result = await connect(params.url, { localPort: params.localPort, host: params.host, }) break case 'disconnect': result = await disconnect(params.url) break case 'list': result = list() break default: throw new Error(`Unknown method: ${method}`) } send({ id, ok: true, result }) } catch (err) { send({ id, ok: false, error: err?.message || String(err), }) } } let buf = '' pipe.on('data', (chunk) => { buf += typeof chunk === 'string' ? chunk : chunk.toString() let idx while ((idx = buf.indexOf('\n')) !== -1) { const line = buf.slice(0, idx).trim() buf = buf.slice(idx + 1) if (!line) continue let msg try { msg = JSON.parse(line) } catch { send({ id: null, ok: false, error: 'Invalid JSON' }) continue } handle(msg) } }) pipe.on('error', (err) => { console.error('[holesail-local] pipe error', err) }) // Ready signal so the UI can wait before first RPC send({ id: 0, ok: true, result: { ready: true, holesail: true } })