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.
This commit is contained in:
+210
-15
@@ -1,25 +1,69 @@
|
||||
/**
|
||||
* Pear-side Holesail client: bind a local port that proxies to a remote hs:// tunnel.
|
||||
* Pear-side Holesail client using the **real `holesail` package**.
|
||||
*
|
||||
* Avoids Node's `createRequire` (unsupported in Pear's ESM loader).
|
||||
* Uses dynamic import so CJS packages resolve in both Node and Pear/Bare.
|
||||
* Why a worker?
|
||||
* -------------
|
||||
* `holesail` → `holesail-client` → `@holesail/hyper-cmd-lib-net` → bare-net / bare-tcp.
|
||||
* bare-tcp ships `.bare` native addons loaded via `require.addon()`. That API exists in
|
||||
* the Bare runtime (pear-run workers, terminal apps, Node server with CJS require).
|
||||
* It is **not** available in the pear-electron UI renderer (script-linker require has
|
||||
* no `.addon` → `require.addon is not a function`).
|
||||
*
|
||||
* Holepunch guidance for pear-electron: put Bare-native P2P code in a worker and
|
||||
* talk over pear-pipe / pear-run (see hello-pear-electron “Workers”).
|
||||
*
|
||||
* - **Pear UI**: pear-run `./workers/holesail-local.cjs` (loads real holesail under Bare)
|
||||
* - **Node** (tests / non-Pear): in-process `require('holesail')` / dynamic import
|
||||
*/
|
||||
/** @type {Map<string, { instance: any, info: object, localPort: number }>} */
|
||||
/** @type {Map<string, { info: object, localPort: number, host: string, url: string }>} */
|
||||
const localClients = new Map()
|
||||
|
||||
let workerPipe = null
|
||||
let workerReady = null
|
||||
let workerBuf = ''
|
||||
/** @type {Map<number, { resolve: Function, reject: Function }>} */
|
||||
const pending = new Map()
|
||||
let nextId = 1
|
||||
|
||||
let HolesailCtor = null
|
||||
let holesailLoadError = null
|
||||
|
||||
/**
|
||||
* Load the holesail constructor via ESM dynamic import (CJS interop).
|
||||
* True when we must use the Bare worker (Pear Electron UI).
|
||||
* bare-tcp only loads via Bare's require.addon — not in the script-linker renderer.
|
||||
*/
|
||||
function needsBareWorker() {
|
||||
const Pear = globalThis.Pear
|
||||
if (!Pear) return false
|
||||
// Electron renderer / Pear GUI: never load bare-tcp in-process
|
||||
const rt = globalThis.process?.versions
|
||||
if (rt?.electron) return true
|
||||
if (typeof Pear.worker?.run === 'function') return true
|
||||
if (Pear.constructor?.UI) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the real holesail constructor in-process (Node / Bare with require.addon).
|
||||
* @returns {Promise<Function>}
|
||||
*/
|
||||
async function loadHolesail() {
|
||||
export async function loadHolesailCtor() {
|
||||
if (HolesailCtor) return HolesailCtor
|
||||
if (holesailLoadError) throw holesailLoadError
|
||||
try {
|
||||
// Prefer Node CJS require (matches server + p2ns)
|
||||
try {
|
||||
const { createRequire } = await import('module')
|
||||
const require = createRequire(import.meta.url)
|
||||
const Ctor = require('holesail')
|
||||
if (typeof Ctor === 'function') {
|
||||
HolesailCtor = Ctor
|
||||
return HolesailCtor
|
||||
}
|
||||
} catch {
|
||||
// Pear ESM: createRequire missing — try dynamic import
|
||||
}
|
||||
const mod = await import('holesail')
|
||||
// CJS default / named interop across Node, Pear, Bare
|
||||
const Ctor =
|
||||
(typeof mod === 'function' && mod) ||
|
||||
mod?.default ||
|
||||
@@ -27,9 +71,7 @@ async function loadHolesail() {
|
||||
(mod?.default && mod.default.default) ||
|
||||
null
|
||||
if (typeof Ctor !== 'function') {
|
||||
throw new Error(
|
||||
'holesail package loaded but no constructor export was found'
|
||||
)
|
||||
throw new Error('holesail package loaded but no constructor export was found')
|
||||
}
|
||||
HolesailCtor = Ctor
|
||||
return HolesailCtor
|
||||
@@ -44,7 +86,121 @@ async function loadHolesail() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a free TCP port on 127.0.0.1 (falls back to random high port if net unavailable).
|
||||
* Start (or reuse) the Bare worker that holds the real holesail instance.
|
||||
*/
|
||||
async function ensureWorker() {
|
||||
if (workerPipe && !workerPipe.destroyed) return workerPipe
|
||||
if (workerReady) return workerReady
|
||||
|
||||
workerReady = (async () => {
|
||||
let run
|
||||
try {
|
||||
const mod = await import('pear-run')
|
||||
run = mod.default || mod
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`pear-run unavailable (${err?.message || err}). Cannot start holesail Bare worker.`
|
||||
)
|
||||
}
|
||||
if (typeof run !== 'function') {
|
||||
throw new Error('pear-run did not export a run() function')
|
||||
}
|
||||
|
||||
const pipe = run('./workers/holesail-local.cjs')
|
||||
workerPipe = pipe
|
||||
workerBuf = ''
|
||||
|
||||
const onData = (chunk) => {
|
||||
workerBuf += typeof chunk === 'string' ? chunk : chunk.toString()
|
||||
let idx
|
||||
while ((idx = workerBuf.indexOf('\n')) !== -1) {
|
||||
const line = workerBuf.slice(0, idx).trim()
|
||||
workerBuf = workerBuf.slice(idx + 1)
|
||||
if (!line) continue
|
||||
let msg
|
||||
try {
|
||||
msg = JSON.parse(line)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
// id 0 = worker ready banner
|
||||
if (msg.id === 0) continue
|
||||
const p = pending.get(msg.id)
|
||||
if (!p) continue
|
||||
pending.delete(msg.id)
|
||||
if (msg.ok) p.resolve(msg.result)
|
||||
else p.reject(new Error(msg.error || 'Holesail worker error'))
|
||||
}
|
||||
}
|
||||
|
||||
pipe.on('data', onData)
|
||||
pipe.on('error', (err) => {
|
||||
for (const [, p] of pending) p.reject(err)
|
||||
pending.clear()
|
||||
workerPipe = null
|
||||
workerReady = null
|
||||
})
|
||||
pipe.on('close', () => {
|
||||
for (const [, p] of pending) p.reject(new Error('Holesail worker closed'))
|
||||
pending.clear()
|
||||
workerPipe = null
|
||||
workerReady = null
|
||||
})
|
||||
pipe.on('crash', ({ exitCode }) => {
|
||||
const err = new Error(`Holesail worker crashed (exit ${exitCode})`)
|
||||
for (const [, p] of pending) p.reject(err)
|
||||
pending.clear()
|
||||
workerPipe = null
|
||||
workerReady = null
|
||||
})
|
||||
|
||||
// Wait briefly for ready banner (non-fatal if missing)
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
return pipe
|
||||
})()
|
||||
|
||||
try {
|
||||
return await workerReady
|
||||
} catch (err) {
|
||||
workerReady = null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {object} [params]
|
||||
*/
|
||||
async function workerRpc(method, params = {}) {
|
||||
const pipe = await ensureWorker()
|
||||
const id = nextId++
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(id)
|
||||
reject(new Error(`Holesail worker timeout (${method})`))
|
||||
}, 60000)
|
||||
pending.set(id, {
|
||||
resolve: (v) => {
|
||||
clearTimeout(timer)
|
||||
resolve(v)
|
||||
},
|
||||
reject: (e) => {
|
||||
clearTimeout(timer)
|
||||
reject(e)
|
||||
},
|
||||
})
|
||||
try {
|
||||
pipe.write(JSON.stringify({ id, method, params }) + '\n')
|
||||
} catch (err) {
|
||||
clearTimeout(timer)
|
||||
pending.delete(id)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a free TCP port on 127.0.0.1 (Node / in-process path only).
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
export async function findFreePort() {
|
||||
@@ -62,13 +218,12 @@ export async function findFreePort() {
|
||||
s.on('error', reject)
|
||||
})
|
||||
} catch {
|
||||
// Pear / restricted environments: best-effort ephemeral range
|
||||
return 41000 + Math.floor(Math.random() * 10000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to an hs:// URL and listen locally.
|
||||
* Connect to an hs:// URL and listen locally (real holesail).
|
||||
* @param {string} urlOrKey
|
||||
* @param {{ localPort?: number, host?: string, openBrowser?: boolean }} [opts]
|
||||
*/
|
||||
@@ -80,7 +235,34 @@ export async function connectLocalHolesail(urlOrKey, opts = {}) {
|
||||
return localClients.get(url)
|
||||
}
|
||||
|
||||
const Holesail = await loadHolesail()
|
||||
if (needsBareWorker()) {
|
||||
const result = await workerRpc('connect', {
|
||||
url,
|
||||
localPort: opts.localPort,
|
||||
host: opts.host,
|
||||
})
|
||||
const entry = {
|
||||
instance: {
|
||||
async close() {
|
||||
await disconnectLocalHolesail(url)
|
||||
},
|
||||
info: result,
|
||||
},
|
||||
info: result,
|
||||
localPort: result.localPort,
|
||||
host: result.host || '127.0.0.1',
|
||||
url,
|
||||
via: 'worker',
|
||||
}
|
||||
localClients.set(url, entry)
|
||||
if (opts.openBrowser !== false) {
|
||||
tryOpenBrowser(`http://${entry.host}:${entry.localPort}`)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// Node / Bare in-process: real holesail package
|
||||
const Holesail = await loadHolesailCtor()
|
||||
const localPort = opts.localPort || (await findFreePort())
|
||||
const host = opts.host || '127.0.0.1'
|
||||
|
||||
@@ -99,7 +281,9 @@ export async function connectLocalHolesail(urlOrKey, opts = {}) {
|
||||
localPort: info.port || localPort,
|
||||
host: info.host || host,
|
||||
url,
|
||||
via: 'in-process',
|
||||
}
|
||||
if (entry.host === '0.0.0.0' || entry.host === '::') entry.host = '127.0.0.1'
|
||||
localClients.set(url, entry)
|
||||
|
||||
if (opts.openBrowser !== false) {
|
||||
@@ -116,6 +300,16 @@ export async function disconnectLocalHolesail(urlOrKey) {
|
||||
const entry = localClients.get(url)
|
||||
if (!entry) return false
|
||||
localClients.delete(url)
|
||||
|
||||
if (entry.via === 'worker') {
|
||||
try {
|
||||
await workerRpc('disconnect', { url })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await entry.instance.close()
|
||||
} catch {
|
||||
@@ -130,6 +324,7 @@ export function listLocalHolesail() {
|
||||
localPort: e.localPort,
|
||||
host: e.host,
|
||||
state: e.info?.state,
|
||||
via: e.via,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -142,7 +337,6 @@ function tryOpenBrowser(href) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
// Best-effort outside browser (Node only) — never use createRequire
|
||||
import('child_process')
|
||||
.then((cp) => {
|
||||
const exec = cp.exec || cp.default?.exec
|
||||
@@ -164,4 +358,5 @@ export default {
|
||||
disconnectLocalHolesail,
|
||||
listLocalHolesail,
|
||||
findFreePort,
|
||||
loadHolesailCtor,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user