@@ -215,8 +215,16 @@ export class Runner {
215
215
this . #started = true ;
216
216
217
217
//console.log("[RUNNER] Starting runner");
218
- await this . #openPegboardWebSocket( ) ;
219
- this . #openTunnel( ) ;
218
+
219
+ try {
220
+ // Connect tunnel first and wait for it to be ready before connecting runner WebSocket
221
+ // This prevents a race condition where the runner appears ready but can't accept network requests
222
+ await this . #openTunnelAndWait( ) ;
223
+ await this . #openPegboardWebSocket( ) ;
224
+ } catch ( error ) {
225
+ this . #started = false ;
226
+ throw error ;
227
+ }
220
228
221
229
if ( ! this . #config. noAutoShutdown ) {
222
230
process . on ( "SIGTERM" , this . shutdown . bind ( this , false , true ) ) ;
@@ -373,6 +381,44 @@ export class Runner {
373
381
return `${ wsEndpoint } /tunnel?namespace=${ encodeURIComponent ( this . #config. namespace ) } ` ;
374
382
}
375
383
384
+ async #openTunnelAndWait( ) : Promise < void > {
385
+ return new Promise ( ( resolve , reject ) => {
386
+ const url = this . pegboardRelayUrl ;
387
+ //console.log("[RUNNER] Opening tunnel to:", url);
388
+ //console.log("[RUNNER] Current runner ID:", this.runnerId || "none");
389
+ //console.log("[RUNNER] Active actors count:", this.#actors.size);
390
+
391
+ let connected = false ;
392
+
393
+ this . #tunnel = new Tunnel ( url ) ;
394
+ this . #tunnel. setCallbacks ( {
395
+ fetch : this . #config. fetch ,
396
+ websocket : this . #config. websocket ,
397
+ onConnected : ( ) => {
398
+ if ( ! connected ) {
399
+ connected = true ;
400
+ //console.log("[RUNNER] Tunnel connected");
401
+ resolve ( ) ;
402
+ }
403
+ } ,
404
+ onDisconnected : ( ) => {
405
+ if ( ! connected ) {
406
+ // First connection attempt failed
407
+ reject ( new Error ( "Tunnel connection failed" ) ) ;
408
+ }
409
+ // If already connected, tunnel will handle reconnection automatically
410
+ } ,
411
+ } ) ;
412
+ this . #tunnel. start ( ) ;
413
+
414
+ // Re-register all active actors with the new tunnel
415
+ for ( const actorId of this . #actors. keys ( ) ) {
416
+ //console.log("[RUNNER] Re-registering actor with tunnel:", actorId);
417
+ this . #tunnel. registerActor ( actorId ) ;
418
+ }
419
+ } ) ;
420
+ }
421
+
376
422
#openTunnel( ) {
377
423
const url = this . pegboardRelayUrl ;
378
424
//console.log("[RUNNER] Opening tunnel to:", url);
@@ -500,6 +546,7 @@ export class Runner {
500
546
// Handle message
501
547
if ( message . tag === "ToClientInit" ) {
502
548
const init = message . val ;
549
+ const hadRunnerId = ! ! this . runnerId ;
503
550
this . runnerId = init . runnerId ;
504
551
505
552
// Store the runner lost threshold from metadata
@@ -513,13 +560,17 @@ export class Runner {
513
560
// runnerLostThreshold: this.#runnerLostThreshold,
514
561
//});
515
562
516
- // Reopen tunnel with runner ID
517
- //console.log("[RUNNER] Received runner ID, reopening tunnel");
518
- if ( this . #tunnel) {
519
- //console.log("[RUNNER] Shutting down existing tunnel");
520
- this . #tunnel. shutdown ( ) ;
563
+ // Only reopen tunnel if we didn't have a runner ID before
564
+ // This happens on reconnection after losing connection
565
+ if ( ! hadRunnerId && this . runnerId ) {
566
+ // Reopen tunnel with runner ID
567
+ //console.log("[RUNNER] Received runner ID, reopening tunnel");
568
+ if ( this . #tunnel) {
569
+ //console.log("[RUNNER] Shutting down existing tunnel");
570
+ this . #tunnel. shutdown ( ) ;
571
+ }
572
+ this . #openTunnel( ) ;
521
573
}
522
- this . #openTunnel( ) ;
523
574
524
575
// Resend events that haven't been acknowledged
525
576
this . #resendUnacknowledgedEvents( init . lastEventIdx ) ;
0 commit comments