reqly-cli 0.1.0 → 0.2.0
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/tunnel.mjs +54 -96
- package/package.json +1 -1
package/lib/tunnel.mjs
CHANGED
|
@@ -159,18 +159,16 @@ async function registerTunnel(serverUrl, token, name, port, binSlug) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
162
|
+
* Poll for new captures and forward them.
|
|
163
|
+
* More reliable than SSE through reverse proxies (Traefik, nginx).
|
|
164
164
|
*/
|
|
165
|
-
async function
|
|
166
|
-
let backoff = BACKOFF_INITIAL_MS;
|
|
165
|
+
async function pollForCaptures(serverUrl, token, tunnelId, localOrigin) {
|
|
167
166
|
let aborted = false;
|
|
168
|
-
let
|
|
167
|
+
let lastSeen = new Date().toISOString();
|
|
168
|
+
let consecutiveErrors = 0;
|
|
169
169
|
|
|
170
|
-
// Clean shutdown
|
|
171
170
|
function shutdown() {
|
|
172
171
|
aborted = true;
|
|
173
|
-
if (controller) controller.abort();
|
|
174
172
|
console.log();
|
|
175
173
|
console.log(`${DIM}[${timestamp()}]${RESET} Tunnel closed.`);
|
|
176
174
|
process.exit(0);
|
|
@@ -178,112 +176,72 @@ async function connectSSE(serverUrl, token, tunnelId, localOrigin) {
|
|
|
178
176
|
process.on("SIGINT", shutdown);
|
|
179
177
|
process.on("SIGTERM", shutdown);
|
|
180
178
|
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
console.log(
|
|
180
|
+
`${DIM}[${timestamp()}]${RESET} ${GREEN}Listening for requests...${RESET}`
|
|
181
|
+
);
|
|
183
182
|
|
|
183
|
+
while (!aborted) {
|
|
184
184
|
try {
|
|
185
|
-
const
|
|
185
|
+
const pollUrl = `${serverUrl}/api/tunnel/poll?token=${encodeURIComponent(token)}&tunnelId=${encodeURIComponent(tunnelId)}&since=${encodeURIComponent(lastSeen)}`;
|
|
186
186
|
|
|
187
|
-
const res = await fetch(
|
|
188
|
-
headers: {
|
|
189
|
-
signal: controller.signal,
|
|
187
|
+
const res = await fetch(pollUrl, {
|
|
188
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
190
189
|
});
|
|
191
190
|
|
|
192
191
|
if (!res.ok) {
|
|
193
|
-
throw new Error(`
|
|
192
|
+
throw new Error(`Poll failed (${res.status})`);
|
|
194
193
|
}
|
|
195
194
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
while (true) {
|
|
209
|
-
const { value, done } = await reader.read();
|
|
210
|
-
if (done) break;
|
|
211
|
-
|
|
212
|
-
buffer += decoder.decode(value, { stream: true });
|
|
213
|
-
const lines = buffer.split("\n");
|
|
214
|
-
buffer = lines.pop(); // keep incomplete line in buffer
|
|
215
|
-
|
|
216
|
-
for (const line of lines) {
|
|
217
|
-
if (line === "") {
|
|
218
|
-
// Empty line = end of event
|
|
219
|
-
if (current.data) {
|
|
220
|
-
handleEvent(current, serverUrl, token, localOrigin);
|
|
221
|
-
}
|
|
222
|
-
current = {};
|
|
223
|
-
} else {
|
|
224
|
-
parseSSELine(line, current);
|
|
225
|
-
}
|
|
195
|
+
consecutiveErrors = 0;
|
|
196
|
+
const data = await res.json();
|
|
197
|
+
|
|
198
|
+
for (const capture of data.captures || []) {
|
|
199
|
+
const { captureId, method, path } = capture;
|
|
200
|
+
if (!captureId) continue;
|
|
201
|
+
|
|
202
|
+
// Parse headers if they're a string
|
|
203
|
+
let headers = capture.headers;
|
|
204
|
+
if (typeof headers === "string") {
|
|
205
|
+
try { headers = JSON.parse(headers); } catch { headers = {}; }
|
|
226
206
|
}
|
|
227
|
-
}
|
|
228
207
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
208
|
+
const methodStr = (method || "POST").toUpperCase().padEnd(6);
|
|
209
|
+
console.log(
|
|
210
|
+
`${DIM}[${timestamp()}]${RESET} ${CYAN}\u2192 ${methodStr}${RESET} ${path || "/"}`
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Forward and report
|
|
214
|
+
const result = await forwardRequest(localOrigin, { ...capture, headers });
|
|
215
|
+
const color = result.status < 400 ? GREEN : RED;
|
|
216
|
+
console.log(
|
|
217
|
+
`${DIM}[${timestamp()}]${RESET} ${color}\u2190 ${result.status} ${result.statusText}${RESET} ${DIM}(${result.durationMs}ms)${RESET}`
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
reportResponse(serverUrl, token, captureId, result).catch(() => {});
|
|
221
|
+
|
|
222
|
+
// Update cursor
|
|
223
|
+
if (capture.createdAt && capture.createdAt > lastSeen) {
|
|
224
|
+
lastSeen = capture.createdAt;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
233
227
|
} catch (err) {
|
|
234
228
|
if (aborted) return;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
consecutiveErrors++;
|
|
230
|
+
if (consecutiveErrors <= 3) {
|
|
231
|
+
// Silent for first few errors
|
|
232
|
+
} else if (consecutiveErrors % 10 === 0) {
|
|
233
|
+
console.log(
|
|
234
|
+
`${DIM}[${timestamp()}]${RESET} ${RED}Poll error: ${err.message}${RESET}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
238
237
|
}
|
|
239
238
|
|
|
240
239
|
if (aborted) return;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const jitter = Math.random() * backoff * 0.3;
|
|
244
|
-
const delay = Math.min(backoff + jitter, BACKOFF_MAX_MS);
|
|
245
|
-
console.log(
|
|
246
|
-
`${DIM}[${timestamp()}]${RESET} ${YELLOW}Reconnecting in ${(delay / 1000).toFixed(1)}s...${RESET}`
|
|
247
|
-
);
|
|
248
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
249
|
-
backoff = Math.min(backoff * BACKOFF_FACTOR, BACKOFF_MAX_MS);
|
|
240
|
+
// Poll every 1 second
|
|
241
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
250
242
|
}
|
|
251
243
|
}
|
|
252
244
|
|
|
253
|
-
/**
|
|
254
|
-
* Handle a single SSE event: forward the request and report back.
|
|
255
|
-
*/
|
|
256
|
-
function handleEvent(event, serverUrl, token, localOrigin) {
|
|
257
|
-
// We only care about "request" events
|
|
258
|
-
if (event.event && event.event !== "request" && event.event !== "message") {
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
let payload;
|
|
263
|
-
try {
|
|
264
|
-
payload = JSON.parse(event.data);
|
|
265
|
-
} catch {
|
|
266
|
-
return; // ignore malformed events (e.g. heartbeat pings)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const { captureId, method, path } = payload;
|
|
270
|
-
if (!captureId) return;
|
|
271
|
-
|
|
272
|
-
const methodStr = (method || "POST").toUpperCase().padEnd(6);
|
|
273
|
-
console.log(
|
|
274
|
-
`${DIM}[${timestamp()}]${RESET} ${CYAN}\u2192 ${methodStr}${RESET} ${path || "/"}`
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
// Fire-and-forget: forward + report (don't block the event loop)
|
|
278
|
-
forwardRequest(localOrigin, payload).then((result) => {
|
|
279
|
-
const color = result.status < 400 ? GREEN : RED;
|
|
280
|
-
console.log(
|
|
281
|
-
`${DIM}[${timestamp()}]${RESET} ${color}\u2190 ${result.status} ${result.statusText}${RESET} ${DIM}(${result.durationMs}ms)${RESET}`
|
|
282
|
-
);
|
|
283
|
-
return reportResponse(serverUrl, token, captureId, result);
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
245
|
// ── Public API ──────────────────────────────────────────────────────
|
|
288
246
|
|
|
289
247
|
/**
|
|
@@ -332,5 +290,5 @@ export async function startTunnel({ port, bin, name }) {
|
|
|
332
290
|
printBanner(publicUrl, localOrigin, true);
|
|
333
291
|
|
|
334
292
|
// 3. Connect SSE and start forwarding
|
|
335
|
-
await
|
|
293
|
+
await pollForCaptures(serverUrl, token, tunnelId, localOrigin);
|
|
336
294
|
}
|