| … | … | @@ -19,10 +19,9 @@ |
| 19 | 19 | * pear-request client (everything else streams from the worker) |
| 20 | 20 | * - a bare-ws server bridges each tab's WebSocket <-> the worker's duplex pipe |
| 21 | 21 | * |
| 22 | | * Worker source is pluggable: |
| 23 | | * - 'demo' -> an in-process pear-request router (rock-solid, no network) |
| 24 | | * - pear://|file:// -> a real headless worker via pear-run (the production path, |
| 25 | | * the in-tab sibling of CMD_LAUNCH_PEAR_LINK's window spawn) |
| 22 | * The only worker is the in-process demo router. This runtime is a local UI |
| 23 | * proof surface, never an application loader: native app code must arrive in a |
| 24 | * verified package outside the browser process. |
| 26 | 25 | */ |
| 27 | 26 | const http = require('bare-http1') |
| 28 | 27 | const ws = require('bare-ws') |
| … | … | @@ -74,7 +73,6 @@ function listenWsServer (port, onSocket) { |
| 74 | 73 | |
| 75 | 74 | class TabRuntime { |
| 76 | 75 | constructor (opts = {}) { |
| 77 | | this.pearRun = opts.pearRun || null // (link) => duplex worker pipe |
| 78 | 76 | this.tabs = new Map() // tabId -> { source } |
| 79 | 77 | this._seq = 0 |
| 80 | 78 | this.httpPort = 0 |
| … | … | @@ -108,29 +106,14 @@ class TabRuntime { |
| 108 | 106 | // A stable demo tab so the headless run-in-tab path is reachable without UI |
| 109 | 107 | // (GET /tab/demo) — the in-process router; the "Headless Demo" card opens it. |
| 110 | 108 | this.tabs.set('demo', { source: 'demo' }) |
| 111 | | // Optional: a stable tab that exercises the real pear-run worker path |
| 112 | | // (/tab/demo-worker). Enabled by PEAR_TAB_DEMO_WORKER, or by the local demo |
| 113 | | // app at ~/Desktop/pear-request-demo if present (statSync-guarded, so it's a |
| 114 | | // no-op on machines without it). This is the same app the "Headless Demo |
| 115 | | // (worker)" card launches. |
| 116 | | const env = (globalThis.Bare && Bare.env) || (globalThis.process && process.env) || {} |
| 117 | | let demoWorker = env.PEAR_TAB_DEMO_WORKER || null |
| 118 | | if (!demoWorker && env.HOME) { |
| 119 | | const p = env.HOME + '/Desktop/pear-request-demo' |
| 120 | | try { require('bare-fs').statSync(p); demoWorker = 'file://' + p } catch {} |
| 121 | | } |
| 122 | | if (demoWorker) { |
| 123 | | this.tabs.set('demo-worker', { source: demoWorker }) |
| 124 | | console.log('[tab-runtime] demo-worker (pear-run) -> ' + demoWorker) |
| 125 | | } |
| 126 | 109 | console.log(`[tab-runtime] http :${this.httpPort} ws :${this.wsPort} (demo: /tab/demo)`) |
| 127 | 110 | return { httpPort: this.httpPort, wsPort: this.wsPort } |
| 128 | 111 | } |
| 129 | 112 | |
| 130 | 113 | // Register a tab and return the wrapper URL the UI should load in an iframe. |
| 131 | | open (link) { |
| 114 | open (source = 'demo') { |
| 115 | if (source !== 'demo') throw new Error('Only the in-process demo is available in a browser tab') |
| 132 | 116 | const tabId = 'tab' + (++this._seq) |
| 133 | | const source = (!link || link === 'demo') ? 'demo' : String(link) |
| 134 | 117 | const tab = { source, contextToken: this._newContextToken() } |
| 135 | 118 | this.tabs.set(tabId, tab) |
| 136 | 119 | return { |
| … | … | @@ -188,7 +171,7 @@ class TabRuntime { |
| 188 | 171 | const tab = this.tabs.get(tabId) |
| 189 | 172 | if (!tab) { try { sock.end() } catch {} ; return } |
| 190 | 173 | try { |
| 191 | | worker = tab.source === 'demo' ? this._spawnInProc(sock) : this._spawnWorker(tab.source, sock) |
| 174 | worker = this._spawnInProc(sock) |
| 192 | 175 | } catch (err) { |
| 193 | 176 | console.error('[tab-runtime] spawn failed:', err && err.message) |
| 194 | 177 | try { sock.end() } catch {} |
| … | … | @@ -216,19 +199,6 @@ class TabRuntime { |
| 216 | 199 | } |
| 217 | 200 | } |
| 218 | 201 | |
| 219 | | // Production: spawn the app as a real headless worker; bridge its pipe <-> WS. |
| 220 | | _spawnWorker (link, sock) { |
| 221 | | if (!this.pearRun) throw new Error('pear-run not available') |
| 222 | | const pipe = this.pearRun(link) |
| 223 | | pipe.on('data', (d) => { try { sock.write(d) } catch {} }) // worker -> browser |
| 224 | | try { pipe.on('error', (e) => console.error('[tab-runtime] worker error:', e && e.message)) } catch {} |
| 225 | | try { pipe.on('crash', (i) => console.error('[tab-runtime] worker crash:', i)) } catch {} |
| 226 | | return { |
| 227 | | toWorker: (data) => { try { pipe.write(data) } catch (e) { console.error('[tab-runtime] write:', e && e.message) } }, |
| 228 | | close: () => { try { pipe.end ? pipe.end() : pipe.destroy && pipe.destroy() } catch {} } |
| 229 | | } |
| 230 | | } |
| 231 | | |
| 232 | 202 | async stop () { |
| 233 | 203 | try { this._ws && this._ws.close() } catch {} |
| 234 | 204 | try { this._http && this._http.close() } catch {} |