CI / test (push) Successful in 9m59s
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.
197 lines
4.8 KiB
JavaScript
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 } })
|