11import assert from "assert" ;
22import crypto from "crypto" ;
3+ import { Abortable } from "events" ;
34import fs from "fs" ;
45import http from "http" ;
56import net from "net" ;
@@ -98,15 +99,24 @@ import {
9899 LogLevel ,
99100 Mutex ,
100101 SharedHeaders ,
101- maybeApply ,
102102} from "./workers" ;
103103import { _formatZodError } from "./zod-format" ;
104104
105105const DEFAULT_HOST = "127.0.0.1" ;
106+ function getURLSafeHost ( host : string ) {
107+ return net . isIPv6 ( host ) ? `[${ host } ]` : host ;
108+ }
106109function getAccessibleHost ( host : string ) {
107- return host === "*" || host === "0.0.0.0" || host === "::"
108- ? "127.0.0.1"
109- : host ;
110+ const accessibleHost =
111+ host === "*" || host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host ;
112+ return getURLSafeHost ( accessibleHost ) ;
113+ }
114+
115+ function getServerPort ( server : http . Server ) {
116+ const address = server . address ( ) ;
117+ // Note address would be string with unix socket
118+ assert ( address !== null && typeof address === "object" ) ;
119+ return address . port ;
110120}
111121
112122// ===== `Miniflare` User Options =====
@@ -539,11 +549,9 @@ export class Miniflare {
539549 #sharedOpts: PluginSharedOptions ;
540550 #workerOpts: PluginWorkerOptions [ ] ;
541551 #log: Log ;
542- readonly #host: string ;
543- readonly #accessibleHost: string ;
544552
545- #runtime?: Runtime ;
546- #removeRuntimeExitHook?: ( ) => void ;
553+ readonly #runtime?: Runtime ;
554+ readonly #removeRuntimeExitHook?: ( ) => void ;
547555 #runtimeEntryURL?: URL ;
548556 #socketPorts?: Map < SocketIdentifier , number > ;
549557 #runtimeClient?: Client ;
@@ -567,7 +575,7 @@ export class Miniflare {
567575 // Aborted when dispose() is called
568576 readonly #disposeController: AbortController ;
569577 #loopbackServer?: StoppableServer ;
570- #loopbackPort ?: number ;
578+ #loopbackHost ?: string ;
571579 readonly #liveReloadServer: WebSocketServer ;
572580 readonly #webSocketServer: WebSocketServer ;
573581 readonly #webSocketExtraHeaders: WeakMap < http . IncomingMessage , Headers > ;
@@ -587,15 +595,6 @@ export class Miniflare {
587595 }
588596
589597 this . #log = this . #sharedOpts. core . log ?? new NoOpLog ( ) ;
590- this . #host = this . #sharedOpts. core . host ?? DEFAULT_HOST ;
591- // TODO: maybe remove `#accessibleHost` field, and just get whenever
592- // constructing entry URL, then extract constructing entry URL into
593- // function used `getUnsafeGetDirectURL()` too?
594- this . #accessibleHost = getAccessibleHost ( this . #host) ;
595-
596- if ( net . isIPv6 ( this . #accessibleHost) ) {
597- this . #accessibleHost = `[${ this . #accessibleHost} ]` ;
598- }
599598
600599 this . #liveReloadServer = new WebSocketServer ( { noServer : true } ) ;
601600 this . #webSocketServer = new WebSocketServer ( {
@@ -630,10 +629,14 @@ export class Miniflare {
630629 fs . rmSync ( this . #tmpPath, { force : true , recursive : true } ) ;
631630 } ) ;
632631
632+ // Setup runtime
633+ this . #runtime = new Runtime ( ) ;
634+ this . #removeRuntimeExitHook = exitHook ( ( ) => void this . #runtime?. dispose ( ) ) ;
635+
633636 this . #disposeController = new AbortController ( ) ;
634637 this . #runtimeMutex = new Mutex ( ) ;
635638 this . #initPromise = this . #runtimeMutex
636- . runWith ( ( ) => this . #init ( ) )
639+ . runWith ( ( ) => this . #assembleAndUpdateConfig ( ) )
637640 . catch ( ( e ) => {
638641 // If initialisation failed, attempting to `dispose()` this instance
639642 // will too. Therefore, remove from the instance registry now, so we
@@ -655,35 +658,6 @@ export class Miniflare {
655658 }
656659 }
657660
658- async #init( ) {
659- // This function must be run with `#runtimeMutex` held
660-
661- // Start loopback server (how the runtime accesses with Miniflare's storage)
662- // using the same host as the main runtime server. This means we can use the
663- // loopback server for live reload updates too.
664- this . #loopbackServer = await this . #startLoopbackServer( 0 , this . #host) ;
665- const address = this . #loopbackServer. address ( ) ;
666- // Note address would be string with unix socket
667- assert ( address !== null && typeof address === "object" ) ;
668- // noinspection JSObjectNullOrUndefined
669- this . #loopbackPort = address . port ;
670-
671- // Start runtime
672- const port = this . #sharedOpts. core . port ?? 0 ;
673- const opts : RuntimeOptions = {
674- entryHost : net . isIPv6 ( this . #host) ? `[${ this . #host} ]` : this . #host,
675- entryPort : port ,
676- loopbackPort : this . #loopbackPort,
677- inspectorPort : this . #sharedOpts. core . inspectorPort ,
678- verbose : this . #sharedOpts. core . verbose ,
679- } ;
680- this . #runtime = new Runtime ( opts ) ;
681- this . #removeRuntimeExitHook = exitHook ( ( ) => void this . #runtime?. dispose ( ) ) ;
682-
683- // Update config and wait for runtime to start
684- await this . #assembleAndUpdateConfig( ) ;
685- }
686-
687661 async #handleLoopbackCustomService(
688662 request : Request ,
689663 customService : string
@@ -862,21 +836,37 @@ export class Miniflare {
862836 await writeResponse ( response , res ) ;
863837 } ;
864838
865- #startLoopbackServer(
866- port : number ,
867- hostname : string
868- ) : Promise < StoppableServer > {
869- if ( hostname === "*" ) {
870- hostname = "::" ;
839+ async #getLoopbackPort( ) : Promise < number > {
840+ // This function must be run with `#runtimeMutex` held
841+
842+ // Start loopback server (how the runtime accesses Node.js) using the same
843+ // host as the main runtime server. This means we can use the loopback
844+ // server for live reload updates too.
845+ const loopbackHost = this . #sharedOpts. core . host ?? DEFAULT_HOST ;
846+ // If we've already started the loopback server...
847+ if ( this . #loopbackServer !== undefined ) {
848+ // ...and it's using the correct host, reuse it
849+ if ( this . #loopbackHost === loopbackHost ) {
850+ return getServerPort ( this . #loopbackServer) ;
851+ }
852+ // Otherwise, stop it, and create a new one
853+ await this . #stopLoopbackServer( ) ;
871854 }
855+ this . #loopbackServer = await this . #startLoopbackServer( loopbackHost ) ;
856+ this . #loopbackHost = loopbackHost ;
857+ return getServerPort ( this . #loopbackServer) ;
858+ }
859+
860+ #startLoopbackServer( hostname : string ) : Promise < StoppableServer > {
861+ if ( hostname === "*" ) hostname = "::" ;
872862
873863 return new Promise ( ( resolve ) => {
874864 const server = stoppable (
875865 http . createServer ( this . #handleLoopback) ,
876866 /* grace */ 0
877867 ) ;
878868 server . on ( "upgrade" , this . #handleLoopbackUpgrade) ;
879- server . listen ( port , hostname , ( ) => resolve ( server ) ) ;
869+ server . listen ( 0 , hostname , ( ) => resolve ( server ) ) ;
880870 } ) ;
881871 }
882872
@@ -887,12 +877,9 @@ export class Miniflare {
887877 } ) ;
888878 }
889879
890- async #assembleConfig( ) : Promise < Config > {
880+ async #assembleConfig( loopbackPort : number ) : Promise < Config > {
891881 const allWorkerOpts = this . #workerOpts;
892882 const sharedOpts = this . #sharedOpts;
893- const loopbackPort = this . #loopbackPort;
894- // #assembleConfig is always called after the loopback server is created
895- assert ( loopbackPort !== undefined ) ;
896883
897884 sharedOpts . core . cf = await setupCf ( this . #log, sharedOpts . core . cf ) ;
898885
@@ -1049,9 +1036,11 @@ export class Miniflare {
10491036 }
10501037
10511038 async #assembleAndUpdateConfig( ) {
1039+ // This function must be run with `#runtimeMutex` held
10521040 const initial = ! this . #runtimeEntryURL;
10531041 assert ( this . #runtime !== undefined ) ;
1054- const config = await this . #assembleConfig( ) ;
1042+ const loopbackPort = await this . #getLoopbackPort( ) ;
1043+ const config = await this . #assembleConfig( loopbackPort ) ;
10551044 const configBuffer = serializeConfig ( config ) ;
10561045
10571046 // Get all socket names we expect to get ports for
@@ -1062,18 +1051,26 @@ export class Miniflare {
10621051 return name ;
10631052 }
10641053 ) ;
1065- // TODO(now): there's a bug here if the inspector was not enabled initially,
1066- // fixed by a later commit in this PR
10671054 if ( this . #sharedOpts. core . inspectorPort !== undefined ) {
10681055 requiredSockets . push ( kInspectorSocket ) ;
10691056 }
1057+
1058+ // Reload runtime
1059+ const host = this . #sharedOpts. core . host ?? DEFAULT_HOST ;
1060+ const urlSafeHost = getURLSafeHost ( host ) ;
1061+ const accessibleHost = getAccessibleHost ( host ) ;
1062+ const runtimeOpts : Abortable & RuntimeOptions = {
1063+ signal : this . #disposeController. signal ,
1064+ entryHost : urlSafeHost ,
1065+ entryPort : this . #sharedOpts. core . port ?? 0 ,
1066+ loopbackPort,
1067+ requiredSockets,
1068+ inspectorPort : this . #sharedOpts. core . inspectorPort ,
1069+ verbose : this . #sharedOpts. core . verbose ,
1070+ } ;
10701071 const maybeSocketPorts = await this . #runtime. updateConfig (
10711072 configBuffer ,
1072- requiredSockets ,
1073- {
1074- signal : this . #disposeController. signal ,
1075- entryPort : maybeApply ( parseInt , this . #runtimeEntryURL?. port ) ,
1076- }
1073+ runtimeOpts
10771074 ) ;
10781075 if ( this . #disposeController. signal . aborted ) return ;
10791076 if ( maybeSocketPorts === undefined ) {
@@ -1094,7 +1091,7 @@ export class Miniflare {
10941091 const entryPort = maybeSocketPorts . get ( SOCKET_ENTRY ) ;
10951092 assert ( entryPort !== undefined ) ;
10961093 this . #runtimeEntryURL = new URL (
1097- `${ secure ? "https" : "http" } ://${ this . # accessibleHost} :${ entryPort } `
1094+ `${ secure ? "https" : "http" } ://${ accessibleHost } :${ entryPort } `
10981095 ) ;
10991096 if ( previousEntryURL ?. toString ( ) !== this . #runtimeEntryURL. toString ( ) ) {
11001097 this . #runtimeClient = new Client ( this . #runtimeEntryURL, {
@@ -1118,16 +1115,15 @@ export class Miniflare {
11181115 // Only log and trigger reload if there aren't pending updates
11191116 const ready = initial ? "Ready" : "Updated and ready" ;
11201117
1121- const host = net . isIPv6 ( this . #host) ? `[${ this . #host} ]` : this . #host;
11221118 this . #log. info (
1123- `${ ready } on ${ secure ? "https" : "http" } ://${ host } :${ entryPort } `
1119+ `${ ready } on ${ secure ? "https" : "http" } ://${ urlSafeHost } :${ entryPort } `
11241120 ) ;
11251121
11261122 if ( initial ) {
11271123 let hosts : string [ ] ;
1128- if ( this . # host === "::" || this . # host === "*" ) {
1124+ if ( host === "::" || host === "*" ) {
11291125 hosts = getAccessibleHosts ( false ) ;
1130- } else if ( this . # host === "0.0.0.0" ) {
1126+ } else if ( host === "0.0.0.0" ) {
11311127 hosts = getAccessibleHosts ( true ) ;
11321128 } else {
11331129 hosts = [ ] ;
0 commit comments