openzoo 0.21.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/setup.js CHANGED
@@ -190,7 +190,7 @@ export async function setupEditor(which, target) {
190
190
  // an unbounded wait once left the editor never launching at all.
191
191
  if (!publicUrl) {
192
192
  process.stdout.write('waiting for the public tunnel (the editor\'s server cannot reach localhost)');
193
- for (let i = 0; i < 120 && !started?.publicUrl; i++) { // up to 60s
193
+ for (let i = 0; i < 240 && !started?.publicUrl; i++) { // up to 120s (rungs race; first to SERVE wins)
194
194
  await new Promise((r) => setTimeout(r, 500));
195
195
  if (i % 4 === 3) process.stdout.write('.');
196
196
  }
package/lib/tunnel.js CHANGED
@@ -132,22 +132,33 @@ export async function probeTunnel(url, { tries = 8, gapMs = 2500 } = {}) {
132
132
  * Returns { url, proc, rung } or throws when every rung fails.
133
133
  */
134
134
  export async function startHealthyTunnel(bin, port, log = () => {}) {
135
- let lastErr = null;
136
- for (const rung of TUNNEL_RUNGS) {
137
- let started = null;
138
- try {
139
- started = await startCloudflared(bin, port, log, { args: rung.args });
140
- } catch (e) { lastErr = e; log(`tunnel: ${rung.why} — ${e.message}`); continue; }
141
- if (await probeTunnel(started.url)) {
142
- log(`tunnel: up via ${rung.why}`);
143
- return { ...started, rung: rung.why };
144
- }
145
- // Printed a URL but never served — kill it before trying the next rung, or
146
- // the dead process keeps holding the port's tunnel slot.
147
- log(`tunnel: ${rung.why} printed ${started.url} but the edge could not reach it — next`);
148
- try { started.proc.kill(); } catch { /* already gone */ }
135
+ // RACE, DO NOT WALK. Tried serially, four rungs cost up to 60s waiting for a
136
+ // URL plus ~20s of probing EACH — minutes of "waiting for the public tunnel"
137
+ // before the good one is even attempted, and the caller times out first.
138
+ // Quick tunnels are free and independent, so all rungs are started at once and
139
+ // the first one that actually SERVES wins; the losers are killed. Worst case
140
+ // becomes one rung's latency instead of four.
141
+ const losers = [];
142
+ let settled = false;
143
+ const attempts = TUNNEL_RUNGS.map(async (rung) => {
144
+ const started = await startCloudflared(bin, port, log, { args: rung.args });
145
+ losers.push(started.proc);
146
+ const ok = await probeTunnel(started.url, { tries: 6, gapMs: 2000 });
147
+ if (!ok) throw new Error(`${rung.why}: printed ${started.url} but the edge could not reach it`);
148
+ if (settled) throw new Error('another rung won');
149
+ settled = true;
150
+ return { ...started, rung: rung.why };
151
+ });
152
+ try {
153
+ const winner = await Promise.any(attempts);
154
+ log(`tunnel: up via ${winner.rung}`);
155
+ for (const p of losers) { if (p !== winner.proc) { try { p.kill(); } catch { /* gone */ } } }
156
+ return winner;
157
+ } catch (agg) {
158
+ for (const p of losers) { try { p.kill(); } catch { /* gone */ } }
159
+ const why = agg?.errors?.map((e) => e.message).join(' | ') || agg?.message || 'unknown';
160
+ throw new Error(`no cloudflared configuration produced a working tunnel (${why})`);
149
161
  }
150
- throw lastErr || new Error('no cloudflared configuration produced a working tunnel');
151
162
  }
152
163
 
153
164
  /**
@@ -195,7 +206,8 @@ export function startCloudflared(bin, port, log, { args = [] } = {}) {
195
206
  proc.stderr.on('data', scan); // cloudflared prints the URL on stderr
196
207
  proc.on('error', reject);
197
208
  proc.on('exit', (c) => { if (!settled) reject(new Error(`cloudflared exited ${c} before printing a URL`)); });
198
- setTimeout(() => { if (!settled) reject(new Error('cloudflared did not produce a URL in 60s')); }, 60000);
209
+ const urlWaitMs = Number(process.env.OPENZOO_TUNNEL_URL_WAIT_MS || 25000);
210
+ setTimeout(() => { if (!settled) { try { proc.kill(); } catch { /* gone */ } reject(new Error(`cloudflared did not produce a URL in ${urlWaitMs}ms`)); } }, urlWaitMs);
199
211
  });
200
212
  }
201
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",