@inkbox/sdk 0.5.13 → 0.5.15
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/README.md +77 -7
- package/dist/_http.d.ts +3 -1
- package/dist/_http.d.ts.map +1 -1
- package/dist/_http.js +6 -2
- package/dist/_http.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -0
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invitations.d.ts +79 -0
- package/dist/a2a/invitations.d.ts.map +1 -0
- package/dist/a2a/invitations.js +165 -0
- package/dist/a2a/invitations.js.map +1 -0
- package/dist/agent_signup/types.d.ts +21 -0
- package/dist/agent_signup/types.d.ts.map +1 -1
- package/dist/agent_signup/types.js +14 -0
- package/dist/agent_signup/types.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inkbox.d.ts +9 -2
- package/dist/inkbox.d.ts.map +1 -1
- package/dist/inkbox.js +44 -10
- package/dist/inkbox.js.map +1 -1
- package/dist/tunnels/client/_listener.d.ts +12 -3
- package/dist/tunnels/client/_listener.d.ts.map +1 -1
- package/dist/tunnels/client/_listener.js +13 -23
- package/dist/tunnels/client/_listener.js.map +1 -1
- package/dist/tunnels/client/_runtime.d.ts +34 -2
- package/dist/tunnels/client/_runtime.d.ts.map +1 -1
- package/dist/tunnels/client/_runtime.js +335 -157
- package/dist/tunnels/client/_runtime.js.map +1 -1
- package/dist/tunnels/client/_ws.d.ts +6 -0
- package/dist/tunnels/client/_ws.d.ts.map +1 -1
- package/dist/tunnels/client/_ws.js +9 -0
- package/dist/tunnels/client/_ws.js.map +1 -1
- package/dist/tunnels/client/_ws_url_edge_bridge.d.ts.map +1 -1
- package/dist/tunnels/client/_ws_url_edge_bridge.js +6 -7
- package/dist/tunnels/client/_ws_url_edge_bridge.js.map +1 -1
- package/dist/tunnels/client/index.d.ts +2 -2
- package/dist/tunnels/client/index.d.ts.map +1 -1
- package/dist/tunnels/client/index.js +1 -1
- package/dist/tunnels/client/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* stack against a real flow-control sequence.
|
|
18
18
|
*/
|
|
19
19
|
import * as http2 from "node:http2";
|
|
20
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
21
|
import { setTimeout as setTimeoutPromise } from "node:timers/promises";
|
|
21
22
|
import { BRIDGE_CLEANUP_SEND_TIMEOUT_MS, BRIDGE_CLOSE_CODE, BRIDGE_HALF_CLOSE_GRACE_MS, BRIDGE_STATUS_TIMEOUT_MS, BridgeProtocolError, BridgeStreamReset, makeBridgeStats, } from "./_bridge.js";
|
|
22
23
|
import { filterResponseHeaders, parseEnvelope, } from "./_envelope.js";
|
|
@@ -25,7 +26,7 @@ import { ControlHeaders, ControlPaths, HOP_BY_HOP_RESPONSE, INKBOX_FORWARDED_HEA
|
|
|
25
26
|
import { validateEnvelopePath } from "./_validation.js";
|
|
26
27
|
import { createUndiciAgentCache, forwardEnvelopeToUrl, } from "./_url_forward.js";
|
|
27
28
|
import { WS_OPCODE_BINARY, WS_OPCODE_CLOSE, WS_OPCODE_CONTINUATION, WS_OPCODE_PING, WS_OPCODE_PONG, WS_OPCODE_TEXT, WsFrameDecoder, encodeWsEnvelope, encodeWsFrame, } from "./_wsframe.js";
|
|
28
|
-
import { dispatchWsUpgradeInProcess, WsServerDraining, } from "./_ws.js";
|
|
29
|
+
import { dispatchWsUpgradeInProcess, WsConnectionLost, WsServerDraining, } from "./_ws.js";
|
|
29
30
|
const HTTP2_HEADER_METHOD = http2.constants.HTTP2_HEADER_METHOD;
|
|
30
31
|
const HTTP2_HEADER_PATH = http2.constants.HTTP2_HEADER_PATH;
|
|
31
32
|
const HTTP2_HEADER_SCHEME = http2.constants.HTTP2_HEADER_SCHEME;
|
|
@@ -38,6 +39,9 @@ export const PING_INTERVAL_MS = 20_000;
|
|
|
38
39
|
* a dead TCP doesn't strand the runtime past the next intake.
|
|
39
40
|
*/
|
|
40
41
|
export const PING_ACK_TIMEOUT_MS = 10_000;
|
|
42
|
+
export const CONNECT_TIMEOUT_MS = 10_000;
|
|
43
|
+
export const HELLO_TIMEOUT_MS = 15_000;
|
|
44
|
+
export const INITIAL_BACKOFF_MS = 1_000;
|
|
41
45
|
export const BACKOFF_CAP_SEC = 30.0;
|
|
42
46
|
export const BACKOFF_JITTER = 0.25;
|
|
43
47
|
// On drain, keep a post-GOAWAY connection alive for its in-flight bridges
|
|
@@ -77,6 +81,14 @@ export class TunnelSupersededError extends Error {
|
|
|
77
81
|
this.name = "TunnelSupersededError";
|
|
78
82
|
}
|
|
79
83
|
}
|
|
84
|
+
class TunnelPhaseTimeoutError extends Error {
|
|
85
|
+
phase;
|
|
86
|
+
constructor(phase, timeoutMs) {
|
|
87
|
+
super(`tunnel ${phase} timed out after ${timeoutMs}ms`);
|
|
88
|
+
this.phase = phase;
|
|
89
|
+
this.name = "TunnelPhaseTimeoutError";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
80
92
|
class OwnerTokenInvalidError extends Error {
|
|
81
93
|
constructor(message) {
|
|
82
94
|
super(message);
|
|
@@ -145,9 +157,17 @@ class Connection {
|
|
|
145
157
|
streams = new Map();
|
|
146
158
|
bridgeStreamIds = new Set();
|
|
147
159
|
pingHandle = null;
|
|
148
|
-
|
|
160
|
+
pingAckHandles = new Set();
|
|
161
|
+
closed;
|
|
162
|
+
resolveClosed;
|
|
163
|
+
closePublished = false;
|
|
164
|
+
detachTransportListeners = null;
|
|
165
|
+
cancelConnectWait = null;
|
|
149
166
|
constructor(id) {
|
|
150
167
|
this.id = id;
|
|
168
|
+
this.closed = new Promise((resolve) => {
|
|
169
|
+
this.resolveClosed = resolve;
|
|
170
|
+
});
|
|
151
171
|
}
|
|
152
172
|
/** Live WS/TCP bridge count — drives the drain-quiescent check. */
|
|
153
173
|
get liveBridges() {
|
|
@@ -174,6 +194,12 @@ export class TunnelRuntime {
|
|
|
174
194
|
onStatus;
|
|
175
195
|
rng;
|
|
176
196
|
http2Connect;
|
|
197
|
+
connectTimeoutMs;
|
|
198
|
+
helloTimeoutMs;
|
|
199
|
+
pingIntervalMs;
|
|
200
|
+
pingAckTimeoutMs;
|
|
201
|
+
handoffRedialBudgetMs;
|
|
202
|
+
sleep;
|
|
177
203
|
// The pool that parks new intakes. Swapped atomically on handoff.
|
|
178
204
|
active = null;
|
|
179
205
|
// Post-GOAWAY connections finishing in-flight work before close.
|
|
@@ -203,6 +229,14 @@ export class TunnelRuntime {
|
|
|
203
229
|
// in aclose().
|
|
204
230
|
undiciAgentCache = createUndiciAgentCache();
|
|
205
231
|
shutdownAbort = new AbortController();
|
|
232
|
+
runtimeStatus = "idle";
|
|
233
|
+
lastConnectedAtMs = null;
|
|
234
|
+
hasConnected = false;
|
|
235
|
+
coldAttempt = 0;
|
|
236
|
+
terminalError = null;
|
|
237
|
+
pendingConnection = null;
|
|
238
|
+
coldGeneration = 0;
|
|
239
|
+
dispatchGeneration = new AsyncLocalStorage();
|
|
206
240
|
constructor(opts) {
|
|
207
241
|
this.tunnelId = opts.tunnelId;
|
|
208
242
|
this.apiKey = opts.apiKey;
|
|
@@ -218,21 +252,44 @@ export class TunnelRuntime {
|
|
|
218
252
|
this.onStatus = opts.onStatus;
|
|
219
253
|
this.rng = opts.rng ?? Math.random;
|
|
220
254
|
this.http2Connect = opts.http2Connect ?? http2.connect.bind(http2);
|
|
255
|
+
this.connectTimeoutMs = opts.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
|
|
256
|
+
this.helloTimeoutMs = opts.helloTimeoutMs ?? HELLO_TIMEOUT_MS;
|
|
257
|
+
this.pingIntervalMs = opts.pingIntervalMs ?? PING_INTERVAL_MS;
|
|
258
|
+
this.pingAckTimeoutMs = opts.pingAckTimeoutMs ?? PING_ACK_TIMEOUT_MS;
|
|
259
|
+
this.handoffRedialBudgetMs =
|
|
260
|
+
opts.handoffRedialBudgetMs ?? HANDOFF_REDIAL_BUDGET_MS;
|
|
261
|
+
this.sleep = opts.sleep ?? ((delayMs, signal) => setTimeoutPromise(delayMs, undefined, { signal }));
|
|
221
262
|
}
|
|
222
263
|
// --- public lifecycle ---------------------------------------------------
|
|
264
|
+
get status() {
|
|
265
|
+
return this.runtimeStatus;
|
|
266
|
+
}
|
|
267
|
+
get isConnected() {
|
|
268
|
+
return this.runtimeStatus === "connected";
|
|
269
|
+
}
|
|
270
|
+
get lastConnectedAt() {
|
|
271
|
+
return this.lastConnectedAtMs === null
|
|
272
|
+
? null
|
|
273
|
+
: new Date(this.lastConnectedAtMs);
|
|
274
|
+
}
|
|
223
275
|
/**
|
|
224
276
|
* Drive the runtime forever. Reconnects with jittered exponential
|
|
225
277
|
* backoff; rejects only on permanent auth failure (rotate the
|
|
226
278
|
* secret) or after `aclose()`.
|
|
227
279
|
*/
|
|
228
280
|
async serveForever() {
|
|
229
|
-
let backoff =
|
|
281
|
+
let backoff = INITIAL_BACKOFF_MS / 1000;
|
|
230
282
|
let consecutiveFailures = 0;
|
|
231
|
-
this.notifyStatus("connecting");
|
|
232
283
|
while (!this.stop) {
|
|
284
|
+
this.coldAttempt += 1;
|
|
285
|
+
const attempt = this.coldAttempt;
|
|
286
|
+
if (attempt === 1)
|
|
287
|
+
this.notifyStatus("connecting");
|
|
288
|
+
// eslint-disable-next-line no-console
|
|
289
|
+
console.info(`tunnel runtime: connection attempt #${attempt}`);
|
|
233
290
|
try {
|
|
234
291
|
await this.runOnce();
|
|
235
|
-
backoff =
|
|
292
|
+
backoff = INITIAL_BACKOFF_MS / 1000;
|
|
236
293
|
consecutiveFailures = 0;
|
|
237
294
|
}
|
|
238
295
|
catch (err) {
|
|
@@ -249,9 +306,21 @@ export class TunnelRuntime {
|
|
|
249
306
|
if (this.superseded) {
|
|
250
307
|
this.stopSuperseded();
|
|
251
308
|
}
|
|
309
|
+
if (this.stop) {
|
|
310
|
+
if (this.runtimeStatus !== "superseded")
|
|
311
|
+
this.notifyStatus("closed");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
252
314
|
consecutiveFailures += 1;
|
|
253
|
-
|
|
254
|
-
|
|
315
|
+
if (err instanceof TunnelPhaseTimeoutError) {
|
|
316
|
+
// eslint-disable-next-line no-console
|
|
317
|
+
console.warn(`tunnel runtime: ${err.phase} timeout on attempt #${attempt}; reconnecting`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// eslint-disable-next-line no-console
|
|
321
|
+
console.warn(`tunnel runtime: connection error on attempt #${attempt} ` +
|
|
322
|
+
`(consecutive failure #${consecutiveFailures}); reconnecting`, err);
|
|
323
|
+
}
|
|
255
324
|
this.notifyStatus("reconnecting");
|
|
256
325
|
}
|
|
257
326
|
if (this.stop) {
|
|
@@ -264,10 +333,11 @@ export class TunnelRuntime {
|
|
|
264
333
|
// backoff = min(backoff * 2, 30.0)
|
|
265
334
|
const jitter = backoff * BACKOFF_JITTER * (2 * this.rng() - 1);
|
|
266
335
|
const sleepFor = Math.max(0.1, backoff + jitter);
|
|
336
|
+
const delayMs = sleepFor * 1000;
|
|
337
|
+
// eslint-disable-next-line no-console
|
|
338
|
+
console.info(`tunnel runtime: retrying with attempt #${attempt + 1} in ${delayMs}ms`);
|
|
267
339
|
try {
|
|
268
|
-
await
|
|
269
|
-
signal: this.shutdownAbort.signal,
|
|
270
|
-
});
|
|
340
|
+
await this.sleep(delayMs, this.shutdownAbort.signal);
|
|
271
341
|
}
|
|
272
342
|
catch {
|
|
273
343
|
// aborted by aclose()
|
|
@@ -281,6 +351,9 @@ export class TunnelRuntime {
|
|
|
281
351
|
/** Graceful shutdown. Signals all loops to exit; closes every conn. */
|
|
282
352
|
async aclose() {
|
|
283
353
|
this.stop = true;
|
|
354
|
+
this.coldGeneration += 1;
|
|
355
|
+
if (this.runtimeStatus !== "superseded")
|
|
356
|
+
this.notifyStatus("closed");
|
|
284
357
|
this.shutdownAbort.abort();
|
|
285
358
|
if (this.passthroughDispatch !== null) {
|
|
286
359
|
try {
|
|
@@ -299,10 +372,12 @@ export class TunnelRuntime {
|
|
|
299
372
|
}
|
|
300
373
|
// Close active + every draining conn; stop each one's ping loop so no
|
|
301
374
|
// ping loop leaks across the handoff set.
|
|
302
|
-
const conns = [this.active, ...this.draining].filter((c) => c !== null);
|
|
303
|
-
for (const conn of conns) {
|
|
375
|
+
const conns = [this.active, this.pendingConnection, ...this.draining].filter((c) => c !== null);
|
|
376
|
+
for (const conn of new Set(conns)) {
|
|
304
377
|
this.stopPingLoop(conn);
|
|
305
378
|
await this.closeConnection(conn);
|
|
379
|
+
this.publishConnectionClosed(conn);
|
|
380
|
+
conn.session = null;
|
|
306
381
|
}
|
|
307
382
|
}
|
|
308
383
|
/**
|
|
@@ -310,55 +385,118 @@ export class TunnelRuntime {
|
|
|
310
385
|
* short grace. The intake pool parks streams indefinitely, so a plain
|
|
311
386
|
* `close()` would never resolve; we GOAWAY then destroy after 250ms.
|
|
312
387
|
*/
|
|
313
|
-
async closeConnection(conn) {
|
|
388
|
+
async closeConnection(conn, graceMs = 250) {
|
|
314
389
|
const session = conn.session;
|
|
315
|
-
|
|
316
|
-
|
|
390
|
+
if (session === null || session.closed || session.destroyed) {
|
|
391
|
+
this.publishConnectionClosed(conn);
|
|
317
392
|
return;
|
|
393
|
+
}
|
|
318
394
|
try {
|
|
319
395
|
session.goaway();
|
|
320
396
|
}
|
|
321
397
|
catch {
|
|
322
398
|
/* swallow */
|
|
323
399
|
}
|
|
400
|
+
if (graceMs <= 0) {
|
|
401
|
+
this.destroyConnection(conn);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
324
404
|
await new Promise((resolve) => {
|
|
325
405
|
const t = setTimeout(() => {
|
|
326
406
|
try {
|
|
327
407
|
session.destroy();
|
|
328
408
|
}
|
|
329
|
-
catch {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
},
|
|
334
|
-
|
|
409
|
+
catch { /* swallow */ }
|
|
410
|
+
setImmediate(() => {
|
|
411
|
+
this.publishConnectionClosed(conn);
|
|
412
|
+
});
|
|
413
|
+
}, graceMs);
|
|
414
|
+
void conn.closed.then(() => {
|
|
335
415
|
clearTimeout(t);
|
|
336
|
-
resolve
|
|
416
|
+
setTimeout(resolve, 10);
|
|
337
417
|
});
|
|
338
418
|
try {
|
|
339
419
|
session.close();
|
|
340
420
|
}
|
|
341
421
|
catch {
|
|
342
422
|
clearTimeout(t);
|
|
343
|
-
|
|
344
|
-
session.destroy();
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
/* swallow */
|
|
348
|
-
}
|
|
349
|
-
resolve();
|
|
423
|
+
this.destroyConnection(conn);
|
|
350
424
|
}
|
|
351
425
|
});
|
|
352
426
|
}
|
|
427
|
+
publishConnectionClosed(conn) {
|
|
428
|
+
if (!conn.closePublished) {
|
|
429
|
+
conn.closePublished = true;
|
|
430
|
+
for (const [, bus] of conn.streams) {
|
|
431
|
+
if (!bus.ended) {
|
|
432
|
+
bus.events.push({ kind: "reset", code: 0 });
|
|
433
|
+
bus.ended = true;
|
|
434
|
+
this.wake(bus);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
conn.resolveClosed();
|
|
438
|
+
}
|
|
439
|
+
conn.detachTransportListeners?.();
|
|
440
|
+
conn.detachTransportListeners = null;
|
|
441
|
+
conn.cancelConnectWait?.();
|
|
442
|
+
conn.cancelConnectWait = null;
|
|
443
|
+
}
|
|
444
|
+
destroyConnection(conn) {
|
|
445
|
+
const session = conn.session;
|
|
446
|
+
this.publishConnectionClosed(conn);
|
|
447
|
+
try {
|
|
448
|
+
session?.destroy();
|
|
449
|
+
}
|
|
450
|
+
catch { /* swallow */ }
|
|
451
|
+
}
|
|
353
452
|
// --- per-connection lifecycle -----------------------------------------
|
|
453
|
+
async withPhaseTimeout(conn, phase, timeoutMs, operation) {
|
|
454
|
+
let timer = null;
|
|
455
|
+
let timeoutError = null;
|
|
456
|
+
const timeout = new Promise((_, reject) => {
|
|
457
|
+
timer = setTimeout(() => {
|
|
458
|
+
timeoutError = new TunnelPhaseTimeoutError(phase, timeoutMs);
|
|
459
|
+
this.destroyConnection(conn);
|
|
460
|
+
reject(timeoutError);
|
|
461
|
+
}, Math.max(1, timeoutMs));
|
|
462
|
+
});
|
|
463
|
+
void operation.catch(() => undefined);
|
|
464
|
+
try {
|
|
465
|
+
return await Promise.race([operation, timeout]);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
if (timeoutError !== null &&
|
|
469
|
+
!(err instanceof TunnelAuthError) &&
|
|
470
|
+
!(err instanceof TunnelSupersededError)) {
|
|
471
|
+
throw timeoutError;
|
|
472
|
+
}
|
|
473
|
+
throw err;
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
if (timer !== null)
|
|
477
|
+
clearTimeout(timer);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
markConnected(emitStatus) {
|
|
481
|
+
const recovered = this.hasConnected;
|
|
482
|
+
this.hasConnected = true;
|
|
483
|
+
this.lastConnectedAtMs = Date.now();
|
|
484
|
+
if (emitStatus) {
|
|
485
|
+
this.notifyStatus("connected");
|
|
486
|
+
// eslint-disable-next-line no-console
|
|
487
|
+
console.info(recovered
|
|
488
|
+
? "tunnel runtime: connection recovered"
|
|
489
|
+
: "tunnel runtime: initial connection established");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
354
492
|
async runOnce() {
|
|
355
493
|
const first = new Connection(this.nextConnId++);
|
|
356
494
|
let conn = first;
|
|
357
495
|
this.active = first;
|
|
358
496
|
try {
|
|
359
|
-
await this.openConnection(first);
|
|
360
|
-
await this.sendHello(first);
|
|
361
|
-
this.
|
|
497
|
+
await this.withPhaseTimeout(first, "connect", this.connectTimeoutMs, this.openConnection(first));
|
|
498
|
+
await this.withPhaseTimeout(first, "hello", this.helloTimeoutMs, this.sendHello(first));
|
|
499
|
+
this.markConnected(true);
|
|
362
500
|
this.startServing(first);
|
|
363
501
|
// Supervise the active connection. A GOAWAY handoff swaps in a new
|
|
364
502
|
// active conn out-of-band; follow it without going through the
|
|
@@ -374,6 +512,8 @@ export class TunnelRuntime {
|
|
|
374
512
|
if (this.handoffInFlight && this.handoffPromise !== null) {
|
|
375
513
|
await this.handoffPromise;
|
|
376
514
|
}
|
|
515
|
+
if (this.terminalError !== null)
|
|
516
|
+
throw this.terminalError;
|
|
377
517
|
const next = this.active;
|
|
378
518
|
if (next !== null && next !== conn && !next.draining) {
|
|
379
519
|
conn = next;
|
|
@@ -387,11 +527,20 @@ export class TunnelRuntime {
|
|
|
387
527
|
if (this.superseded) {
|
|
388
528
|
throw new TunnelSupersededError("another client connected to this tunnel; not reconnecting");
|
|
389
529
|
}
|
|
530
|
+
if (!this.stop) {
|
|
531
|
+
// eslint-disable-next-line no-console
|
|
532
|
+
console.warn("tunnel runtime: connected session lost; reconnecting");
|
|
533
|
+
this.notifyStatus("reconnecting");
|
|
534
|
+
}
|
|
390
535
|
}
|
|
391
536
|
finally {
|
|
537
|
+
if (!this.stop)
|
|
538
|
+
this.coldGeneration += 1;
|
|
392
539
|
this.stopPingLoop(conn);
|
|
393
540
|
conn.streams.clear();
|
|
394
541
|
conn.bridgeStreamIds.clear();
|
|
542
|
+
await this.closeConnection(conn);
|
|
543
|
+
this.publishConnectionClosed(conn);
|
|
395
544
|
conn.session = null;
|
|
396
545
|
if (this.active === conn)
|
|
397
546
|
this.active = null;
|
|
@@ -409,18 +558,10 @@ export class TunnelRuntime {
|
|
|
409
558
|
}
|
|
410
559
|
/** Resolve once the supervised conn closes OR a handoff swaps active. */
|
|
411
560
|
waitCloseOrHandoff(conn) {
|
|
412
|
-
const closed = new Promise((resolve) => {
|
|
413
|
-
const session = conn.session;
|
|
414
|
-
if (session === null || session.closed) {
|
|
415
|
-
resolve();
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
session.once("close", () => resolve());
|
|
419
|
-
});
|
|
420
561
|
const woken = new Promise((resolve) => {
|
|
421
562
|
this.wakeSupervisor = resolve;
|
|
422
563
|
});
|
|
423
|
-
return Promise.race([closed, woken]).finally(() => {
|
|
564
|
+
return Promise.race([conn.closed, woken]).finally(() => {
|
|
424
565
|
this.wakeSupervisor = null;
|
|
425
566
|
});
|
|
426
567
|
}
|
|
@@ -507,6 +648,7 @@ export class TunnelRuntime {
|
|
|
507
648
|
try {
|
|
508
649
|
const newConn = await this.makeReplacementConnection();
|
|
509
650
|
this.active = newConn;
|
|
651
|
+
this.markConnected(false);
|
|
510
652
|
// Supervisor was watching oldConn; wake it to follow newConn.
|
|
511
653
|
this.signalSupervisor();
|
|
512
654
|
}
|
|
@@ -515,10 +657,12 @@ export class TunnelRuntime {
|
|
|
515
657
|
// External takeover during the handoff hello: `superseded` is set, so
|
|
516
658
|
// end the old conn and wake the supervisor to stop terminally (no cold
|
|
517
659
|
// redial, which would boot the client that replaced us).
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
660
|
+
this.destroyConnection(oldConn);
|
|
661
|
+
this.signalSupervisor();
|
|
662
|
+
}
|
|
663
|
+
else if (err instanceof TunnelAuthError) {
|
|
664
|
+
this.terminalError = err;
|
|
665
|
+
this.destroyConnection(oldConn);
|
|
522
666
|
this.signalSupervisor();
|
|
523
667
|
}
|
|
524
668
|
else {
|
|
@@ -527,10 +671,7 @@ export class TunnelRuntime {
|
|
|
527
671
|
// the old session closed so the supervisor returns.
|
|
528
672
|
// eslint-disable-next-line no-console
|
|
529
673
|
console.warn("tunnel runtime: handoff failed; reconnecting cold", err);
|
|
530
|
-
|
|
531
|
-
oldConn.session?.destroy();
|
|
532
|
-
}
|
|
533
|
-
catch { /* swallow */ }
|
|
674
|
+
this.destroyConnection(oldConn);
|
|
534
675
|
this.signalSupervisor();
|
|
535
676
|
}
|
|
536
677
|
}
|
|
@@ -544,20 +685,33 @@ export class TunnelRuntime {
|
|
|
544
685
|
/** Dial + hello + park a replacement, retrying transient hello failures. */
|
|
545
686
|
async makeReplacementConnection() {
|
|
546
687
|
let backoff = 0.1;
|
|
547
|
-
const
|
|
688
|
+
const deadline = performance.now() + this.handoffRedialBudgetMs;
|
|
548
689
|
while (!this.stop) {
|
|
690
|
+
let remaining = deadline - performance.now();
|
|
691
|
+
if (remaining <= 0)
|
|
692
|
+
throw new Error("handoff redial budget exhausted");
|
|
549
693
|
const conn = new Connection(this.nextConnId++);
|
|
694
|
+
this.pendingConnection = conn;
|
|
550
695
|
try {
|
|
551
|
-
await this.openConnection(conn);
|
|
552
|
-
|
|
696
|
+
await this.withPhaseTimeout(conn, "connect", Math.min(this.connectTimeoutMs, remaining), this.openConnection(conn));
|
|
697
|
+
remaining = deadline - performance.now();
|
|
698
|
+
if (remaining <= 0)
|
|
699
|
+
throw new Error("handoff redial budget exhausted");
|
|
700
|
+
await this.withPhaseTimeout(conn, "hello", Math.min(this.helloTimeoutMs, remaining), this.sendHello(conn));
|
|
553
701
|
this.startServing(conn);
|
|
702
|
+
this.pendingConnection = null;
|
|
554
703
|
return conn;
|
|
555
704
|
}
|
|
556
705
|
catch (err) {
|
|
706
|
+
remaining = deadline - performance.now();
|
|
557
707
|
try {
|
|
558
|
-
await this.closeConnection(conn);
|
|
708
|
+
await this.closeConnection(conn, Math.min(250, Math.max(0, remaining)));
|
|
559
709
|
}
|
|
560
710
|
catch { /* swallow */ }
|
|
711
|
+
this.publishConnectionClosed(conn);
|
|
712
|
+
conn.session = null;
|
|
713
|
+
if (this.pendingConnection === conn)
|
|
714
|
+
this.pendingConnection = null;
|
|
561
715
|
// A rejected key or a takeover is terminal; never retry either
|
|
562
716
|
// (retrying a takeover would boot the client that replaced us).
|
|
563
717
|
if (err instanceof TunnelAuthError || err instanceof TunnelSupersededError) {
|
|
@@ -569,15 +723,15 @@ export class TunnelRuntime {
|
|
|
569
723
|
if (this.superseded) {
|
|
570
724
|
throw new TunnelSupersededError("another client connected to this tunnel during handoff");
|
|
571
725
|
}
|
|
572
|
-
|
|
726
|
+
remaining = deadline - performance.now();
|
|
727
|
+
if (remaining <= 0) {
|
|
573
728
|
throw new Error("handoff redial budget exhausted");
|
|
574
729
|
}
|
|
575
730
|
// A drain 503 on the new hello means the NLB landed us back on the
|
|
576
731
|
// draining task; back off (jittered) so it re-routes us elsewhere.
|
|
577
732
|
const jitter = backoff * BACKOFF_JITTER * (2 * this.rng() - 1);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}).catch(() => undefined);
|
|
733
|
+
const delayMs = Math.min(Math.max(50, (backoff + jitter) * 1000), remaining);
|
|
734
|
+
await this.sleep(delayMs, this.shutdownAbort.signal).catch(() => undefined);
|
|
581
735
|
backoff = Math.min(backoff * 2, 5.0);
|
|
582
736
|
}
|
|
583
737
|
}
|
|
@@ -592,6 +746,8 @@ export class TunnelRuntime {
|
|
|
592
746
|
await setTimeoutPromise(250).catch(() => undefined);
|
|
593
747
|
}
|
|
594
748
|
await this.closeConnection(oldConn);
|
|
749
|
+
this.publishConnectionClosed(oldConn);
|
|
750
|
+
oldConn.session = null;
|
|
595
751
|
oldConn.streams.clear();
|
|
596
752
|
oldConn.bridgeStreamIds.clear();
|
|
597
753
|
this.draining.delete(oldConn);
|
|
@@ -607,27 +763,27 @@ export class TunnelRuntime {
|
|
|
607
763
|
// (Spike 1) — the setting line doesn't translate.
|
|
608
764
|
});
|
|
609
765
|
conn.session = session;
|
|
610
|
-
|
|
766
|
+
const socket = (() => {
|
|
767
|
+
try {
|
|
768
|
+
return session.socket ?? null;
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
})();
|
|
774
|
+
const onSessionClose = () => {
|
|
611
775
|
// eslint-disable-next-line no-console
|
|
612
776
|
console.info("tunnel runtime: h2 session closed");
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (!bus.ended) {
|
|
617
|
-
bus.events.push({ kind: "reset", code: 0 });
|
|
618
|
-
bus.ended = true;
|
|
619
|
-
this.wake(bus);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
session.on("error", (err) => {
|
|
777
|
+
this.publishConnectionClosed(conn);
|
|
778
|
+
};
|
|
779
|
+
const onSessionError = (err) => {
|
|
624
780
|
// Visibility into session-fatal errors. Stream-level errors
|
|
625
781
|
// surface separately via stream events; this is genuinely
|
|
626
782
|
// session-terminal.
|
|
627
783
|
// eslint-disable-next-line no-console
|
|
628
784
|
console.warn("tunnel runtime: h2 session error", err);
|
|
629
|
-
}
|
|
630
|
-
|
|
785
|
+
};
|
|
786
|
+
const onGoaway = (errorCode, lastStreamId, opaqueData) => {
|
|
631
787
|
// eslint-disable-next-line no-console
|
|
632
788
|
console.info(`tunnel runtime: GOAWAY received error_code=${errorCode} last_stream_id=${lastStreamId}`);
|
|
633
789
|
// NO_ERROR GOAWAY = drain (make-before-break handoff). A non-zero code
|
|
@@ -640,51 +796,69 @@ export class TunnelRuntime {
|
|
|
640
796
|
else {
|
|
641
797
|
this.maybeMarkSupersededGoaway(conn, errorCode, opaqueData);
|
|
642
798
|
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
799
|
+
};
|
|
800
|
+
const onSocketClose = (hadError) => {
|
|
801
|
+
onSocketDeath(`closed hadError=${hadError}`);
|
|
802
|
+
};
|
|
803
|
+
const onSocketError = (err) => {
|
|
804
|
+
onSocketDeath("error", err);
|
|
805
|
+
};
|
|
806
|
+
const onSocketDeath = (label, err) => {
|
|
807
|
+
// eslint-disable-next-line no-console
|
|
808
|
+
console.info(`tunnel runtime: underlying socket ${label}` +
|
|
809
|
+
(err !== undefined ? ` err=${err.message}` : ""));
|
|
810
|
+
if (!session.closed && !session.destroyed) {
|
|
653
811
|
// eslint-disable-next-line no-console
|
|
654
|
-
console.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
/* swallow */
|
|
676
|
-
}
|
|
812
|
+
console.warn("tunnel runtime: forcing session.destroy() from socket-death");
|
|
813
|
+
this.destroyConnection(conn);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
session.on("close", onSessionClose);
|
|
817
|
+
session.on("error", onSessionError);
|
|
818
|
+
session.on("goaway", onGoaway);
|
|
819
|
+
socket?.once("close", onSocketClose);
|
|
820
|
+
socket?.once("error", onSocketError);
|
|
821
|
+
conn.detachTransportListeners = () => {
|
|
822
|
+
session.off("close", onSessionClose);
|
|
823
|
+
session.off("error", onSessionError);
|
|
824
|
+
session.off("goaway", onGoaway);
|
|
825
|
+
try {
|
|
826
|
+
socket?.off("close", onSocketClose);
|
|
827
|
+
socket?.off("error", onSocketError);
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
/* the HTTP/2 socket proxy becomes inaccessible after disconnect */
|
|
831
|
+
}
|
|
832
|
+
};
|
|
677
833
|
await new Promise((resolve, reject) => {
|
|
678
|
-
|
|
834
|
+
let settled = false;
|
|
835
|
+
const cleanup = () => {
|
|
836
|
+
session.off("connect", onConnect);
|
|
679
837
|
session.off("error", onError);
|
|
838
|
+
session.off("close", onClose);
|
|
839
|
+
conn.cancelConnectWait = null;
|
|
840
|
+
};
|
|
841
|
+
const succeed = () => {
|
|
842
|
+
if (settled)
|
|
843
|
+
return;
|
|
844
|
+
settled = true;
|
|
845
|
+
cleanup();
|
|
680
846
|
resolve();
|
|
681
847
|
};
|
|
682
|
-
const
|
|
683
|
-
|
|
848
|
+
const fail = (err) => {
|
|
849
|
+
if (settled)
|
|
850
|
+
return;
|
|
851
|
+
settled = true;
|
|
852
|
+
cleanup();
|
|
684
853
|
reject(err);
|
|
685
854
|
};
|
|
855
|
+
const onConnect = () => succeed();
|
|
856
|
+
const onError = (err) => fail(err);
|
|
857
|
+
const onClose = () => fail(new Error("h2 session closed before connect completed"));
|
|
858
|
+
conn.cancelConnectWait = () => fail(new Error("h2 session closed before connect completed"));
|
|
686
859
|
session.once("connect", onConnect);
|
|
687
860
|
session.once("error", onError);
|
|
861
|
+
session.once("close", onClose);
|
|
688
862
|
});
|
|
689
863
|
// OS-level keepalive on the underlying TCP socket so a silently-
|
|
690
864
|
// dropped connection (NAT timeout, NLB idle eviction, peer power-
|
|
@@ -693,25 +867,12 @@ export class TunnelRuntime {
|
|
|
693
867
|
// tracking (see startPingLoop) is the load-bearing detector;
|
|
694
868
|
// this is defense-in-depth.
|
|
695
869
|
try {
|
|
696
|
-
|
|
697
|
-
sock?.setKeepAlive?.(true, 30_000);
|
|
870
|
+
socket?.setKeepAlive?.(true, 30_000);
|
|
698
871
|
}
|
|
699
872
|
catch {
|
|
700
873
|
/* swallow */
|
|
701
874
|
}
|
|
702
875
|
}
|
|
703
|
-
waitForSessionClose(conn) {
|
|
704
|
-
const session = conn.session;
|
|
705
|
-
if (session === null)
|
|
706
|
-
return Promise.resolve();
|
|
707
|
-
return new Promise((resolve) => {
|
|
708
|
-
if (session.closed) {
|
|
709
|
-
resolve();
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
session.once("close", () => resolve());
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
876
|
// --- handshake ---------------------------------------------------------
|
|
716
877
|
async sendHello(conn) {
|
|
717
878
|
conn.ownerToken = null;
|
|
@@ -731,7 +892,14 @@ export class TunnelRuntime {
|
|
|
731
892
|
helloHeaders[ControlHeaders.POOL_SIZE] = String(this.poolSize);
|
|
732
893
|
}
|
|
733
894
|
const stream = this.openStream(conn, helloHeaders, { endStream: true });
|
|
734
|
-
|
|
895
|
+
let status;
|
|
896
|
+
let body;
|
|
897
|
+
try {
|
|
898
|
+
({ status, body } = await this.awaitResponse(conn, stream.streamId));
|
|
899
|
+
}
|
|
900
|
+
finally {
|
|
901
|
+
conn.streams.delete(stream.streamId);
|
|
902
|
+
}
|
|
735
903
|
if (status === 401 || status === 403) {
|
|
736
904
|
throw new TunnelAuthError(`${ControlPaths.HELLO} returned ${status}; the API key was rejected (check the key matches the tunnel's identity scope, or use an admin-scoped key in the tunnel's org)`);
|
|
737
905
|
}
|
|
@@ -881,7 +1049,8 @@ export class TunnelRuntime {
|
|
|
881
1049
|
while (!this.stop &&
|
|
882
1050
|
!conn.draining &&
|
|
883
1051
|
conn.session !== null &&
|
|
884
|
-
!conn.session.closed
|
|
1052
|
+
!conn.session.closed &&
|
|
1053
|
+
!conn.session.destroyed) {
|
|
885
1054
|
let envelope;
|
|
886
1055
|
try {
|
|
887
1056
|
envelope = await this.parkOneIntake(conn, slot);
|
|
@@ -891,17 +1060,14 @@ export class TunnelRuntime {
|
|
|
891
1060
|
// Another client took over: force this conn down so the supervisor
|
|
892
1061
|
// observes the terminal flag and stops (no reconnect).
|
|
893
1062
|
this.markSuperseded();
|
|
894
|
-
|
|
895
|
-
conn.session?.destroy();
|
|
896
|
-
}
|
|
897
|
-
catch { /* swallow */ }
|
|
1063
|
+
this.destroyConnection(conn);
|
|
898
1064
|
return;
|
|
899
1065
|
}
|
|
900
1066
|
if (err instanceof OwnerTokenInvalidError) {
|
|
901
1067
|
// eslint-disable-next-line no-console
|
|
902
1068
|
console.warn(`intake slot ${slot}: owner_token rejected; ` +
|
|
903
1069
|
`forcing session.destroy() and reconnecting`);
|
|
904
|
-
|
|
1070
|
+
this.destroyConnection(conn);
|
|
905
1071
|
return;
|
|
906
1072
|
}
|
|
907
1073
|
if (isSessionTerminalError(err) || conn.session?.destroyed) {
|
|
@@ -916,10 +1082,7 @@ export class TunnelRuntime {
|
|
|
916
1082
|
console.warn(`intake slot ${slot}: h2 session terminal (` +
|
|
917
1083
|
`${err?.code ?? "no code"}); ` +
|
|
918
1084
|
`exiting slot`, err);
|
|
919
|
-
|
|
920
|
-
conn.session?.destroy();
|
|
921
|
-
}
|
|
922
|
-
catch { /* swallow */ }
|
|
1085
|
+
this.destroyConnection(conn);
|
|
923
1086
|
return;
|
|
924
1087
|
}
|
|
925
1088
|
// eslint-disable-next-line no-console
|
|
@@ -932,7 +1095,10 @@ export class TunnelRuntime {
|
|
|
932
1095
|
// Fire-and-forget dispatch; tracked on the runtime (not the conn) so
|
|
933
1096
|
// an in-flight handler survives this conn draining and can post its
|
|
934
1097
|
// reply on the new active conn during a handoff.
|
|
935
|
-
const
|
|
1098
|
+
const generation = this.coldGeneration;
|
|
1099
|
+
const task = this.dispatchGeneration
|
|
1100
|
+
.run(generation, () => this.dispatchEnvelope(conn, envelope))
|
|
1101
|
+
.catch((err) => {
|
|
936
1102
|
// eslint-disable-next-line no-console
|
|
937
1103
|
console.warn(`dispatch failed request_id=${envelope.requestId}`, err);
|
|
938
1104
|
});
|
|
@@ -1003,10 +1169,9 @@ export class TunnelRuntime {
|
|
|
1003
1169
|
}
|
|
1004
1170
|
// --- ping loop ---------------------------------------------------------
|
|
1005
1171
|
startPingLoop(conn) {
|
|
1006
|
-
conn.pingAbort = new AbortController();
|
|
1007
1172
|
conn.pingHandle = setInterval(() => {
|
|
1008
1173
|
const session = conn.session;
|
|
1009
|
-
if (session === null || session.closed)
|
|
1174
|
+
if (session === null || session.closed || session.destroyed)
|
|
1010
1175
|
return;
|
|
1011
1176
|
let ackTimer = null;
|
|
1012
1177
|
let acked = false;
|
|
@@ -1015,27 +1180,24 @@ export class TunnelRuntime {
|
|
|
1015
1180
|
acked = true;
|
|
1016
1181
|
if (ackTimer !== null) {
|
|
1017
1182
|
clearTimeout(ackTimer);
|
|
1183
|
+
conn.pingAckHandles.delete(ackTimer);
|
|
1018
1184
|
ackTimer = null;
|
|
1019
1185
|
}
|
|
1020
1186
|
if (err !== null && err !== undefined) {
|
|
1021
1187
|
// eslint-disable-next-line no-console
|
|
1022
1188
|
console.warn("tunnel runtime: PING errored; forcing session.destroy()", err);
|
|
1023
|
-
|
|
1024
|
-
session.destroy();
|
|
1025
|
-
}
|
|
1026
|
-
catch { /* swallow */ }
|
|
1189
|
+
this.destroyConnection(conn);
|
|
1027
1190
|
}
|
|
1028
1191
|
});
|
|
1029
1192
|
}
|
|
1030
1193
|
catch (err) {
|
|
1031
1194
|
// eslint-disable-next-line no-console
|
|
1032
1195
|
console.warn("tunnel runtime: session.ping() threw synchronously; forcing destroy", err);
|
|
1033
|
-
|
|
1034
|
-
session.destroy();
|
|
1035
|
-
}
|
|
1036
|
-
catch { /* swallow */ }
|
|
1196
|
+
this.destroyConnection(conn);
|
|
1037
1197
|
return;
|
|
1038
1198
|
}
|
|
1199
|
+
if (acked)
|
|
1200
|
+
return;
|
|
1039
1201
|
// Application-level liveness check: if the ack doesn't come
|
|
1040
1202
|
// back within PING_ACK_TIMEOUT_MS, the underlying TCP is gone
|
|
1041
1203
|
// (kernel send buffer absorbing writes silently is the typical
|
|
@@ -1043,18 +1205,18 @@ export class TunnelRuntime {
|
|
|
1043
1205
|
// without our help). Force-destroy the session; serveForever
|
|
1044
1206
|
// observes the close and reconnects.
|
|
1045
1207
|
ackTimer = setTimeout(() => {
|
|
1208
|
+
if (ackTimer !== null)
|
|
1209
|
+
conn.pingAckHandles.delete(ackTimer);
|
|
1046
1210
|
if (acked)
|
|
1047
1211
|
return;
|
|
1048
1212
|
// eslint-disable-next-line no-console
|
|
1049
1213
|
console.warn(`tunnel runtime: PING ack not received within ` +
|
|
1050
|
-
`${
|
|
1214
|
+
`${this.pingAckTimeoutMs}ms; assuming dead connection, ` +
|
|
1051
1215
|
`forcing reconnect`);
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
}, PING_ACK_TIMEOUT_MS);
|
|
1057
|
-
}, PING_INTERVAL_MS);
|
|
1216
|
+
this.destroyConnection(conn);
|
|
1217
|
+
}, this.pingAckTimeoutMs);
|
|
1218
|
+
conn.pingAckHandles.add(ackTimer);
|
|
1219
|
+
}, this.pingIntervalMs);
|
|
1058
1220
|
// Do NOT unref(): explicit cancellation in stopPingLoop().
|
|
1059
1221
|
}
|
|
1060
1222
|
stopPingLoop(conn) {
|
|
@@ -1062,8 +1224,9 @@ export class TunnelRuntime {
|
|
|
1062
1224
|
clearInterval(conn.pingHandle);
|
|
1063
1225
|
conn.pingHandle = null;
|
|
1064
1226
|
}
|
|
1065
|
-
conn.
|
|
1066
|
-
|
|
1227
|
+
for (const handle of conn.pingAckHandles)
|
|
1228
|
+
clearTimeout(handle);
|
|
1229
|
+
conn.pingAckHandles.clear();
|
|
1067
1230
|
}
|
|
1068
1231
|
// --- envelope dispatch -------------------------------------------------
|
|
1069
1232
|
async dispatchEnvelope(conn, envelope) {
|
|
@@ -1514,13 +1677,13 @@ export class TunnelRuntime {
|
|
|
1514
1677
|
if (id === null) {
|
|
1515
1678
|
if (conn.draining)
|
|
1516
1679
|
throw new WsServerDraining();
|
|
1517
|
-
|
|
1680
|
+
throw new WsConnectionLost();
|
|
1518
1681
|
}
|
|
1519
1682
|
const ev = await self.nextEvent(conn, id);
|
|
1520
1683
|
if (ev === null) {
|
|
1521
1684
|
if (conn.draining)
|
|
1522
1685
|
throw new WsServerDraining();
|
|
1523
|
-
|
|
1686
|
+
throw new WsConnectionLost();
|
|
1524
1687
|
}
|
|
1525
1688
|
if (ev.kind === "data") {
|
|
1526
1689
|
yield ev.data;
|
|
@@ -1528,14 +1691,14 @@ export class TunnelRuntime {
|
|
|
1528
1691
|
else if (ev.kind === "end") {
|
|
1529
1692
|
if (conn.draining)
|
|
1530
1693
|
throw new WsServerDraining();
|
|
1531
|
-
|
|
1694
|
+
throw new WsConnectionLost();
|
|
1532
1695
|
}
|
|
1533
1696
|
else if (ev.kind === "reset") {
|
|
1534
1697
|
// A reset while the conn is draining is the redeploy drain, not
|
|
1535
1698
|
// a peer error — surface it typed so the handler can reconnect.
|
|
1536
1699
|
if (conn.draining)
|
|
1537
1700
|
throw new WsServerDraining();
|
|
1538
|
-
throw new
|
|
1701
|
+
throw new WsConnectionLost(`bridge stream reset code=${ev.code}`);
|
|
1539
1702
|
}
|
|
1540
1703
|
}
|
|
1541
1704
|
})();
|
|
@@ -1958,6 +2121,8 @@ export class TunnelRuntime {
|
|
|
1958
2121
|
setTimeoutPromise(POST_ACTIVE_WAIT_MS),
|
|
1959
2122
|
]);
|
|
1960
2123
|
}
|
|
2124
|
+
if (!this.dispatchIsCurrent())
|
|
2125
|
+
return;
|
|
1961
2126
|
const target = this.pickReplyConnection(origin);
|
|
1962
2127
|
if (target === null) {
|
|
1963
2128
|
// eslint-disable-next-line no-console
|
|
@@ -1968,7 +2133,11 @@ export class TunnelRuntime {
|
|
|
1968
2133
|
}
|
|
1969
2134
|
/** The active conn if it can take new streams, else the origin if it can. */
|
|
1970
2135
|
pickReplyConnection(origin) {
|
|
1971
|
-
const usable = (c) => c !== null &&
|
|
2136
|
+
const usable = (c) => c !== null &&
|
|
2137
|
+
!c.draining &&
|
|
2138
|
+
c.session !== null &&
|
|
2139
|
+
!c.session.closed &&
|
|
2140
|
+
!c.session.destroyed;
|
|
1972
2141
|
if (usable(this.active))
|
|
1973
2142
|
return this.active;
|
|
1974
2143
|
if (usable(origin))
|
|
@@ -1976,6 +2145,8 @@ export class TunnelRuntime {
|
|
|
1976
2145
|
return null;
|
|
1977
2146
|
}
|
|
1978
2147
|
async postResponse(conn, requestId, status, userHeaders, body) {
|
|
2148
|
+
if (!this.dispatchIsCurrent())
|
|
2149
|
+
return;
|
|
1979
2150
|
const reqHeaders = {
|
|
1980
2151
|
[HTTP2_HEADER_METHOD]: "POST",
|
|
1981
2152
|
[HTTP2_HEADER_SCHEME]: "https",
|
|
@@ -2039,7 +2210,14 @@ export class TunnelRuntime {
|
|
|
2039
2210
|
}
|
|
2040
2211
|
}
|
|
2041
2212
|
// --- utilities ---------------------------------------------------------
|
|
2213
|
+
dispatchIsCurrent() {
|
|
2214
|
+
const generation = this.dispatchGeneration.getStore();
|
|
2215
|
+
return generation === undefined || generation === this.coldGeneration;
|
|
2216
|
+
}
|
|
2042
2217
|
notifyStatus(status) {
|
|
2218
|
+
if (this.runtimeStatus === status)
|
|
2219
|
+
return;
|
|
2220
|
+
this.runtimeStatus = status;
|
|
2043
2221
|
if (this.onStatus !== undefined) {
|
|
2044
2222
|
try {
|
|
2045
2223
|
this.onStatus(status);
|