Files
peardock/workers/holesail-local.cjs
T
snxraven cc6df798e1 Run real holesail in a Bare worker for Pear UI
pear-electron cannot load bare-tcp (require.addon) in the renderer.
Use pear-run + workers/holesail-local.cjs so the actual holesail package
runs under Bare; Node still loads holesail in-process. Add connect
fallback modal with copy URL.
2026-07-10 23:13:30 -04:00

197 lines
4.8 KiB
JavaScript

/**
* 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<string, { instance: import('holesail'), localPort: number, host: string, url: string, info: object }>} */
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<number>}
*/
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 } })