@@ -54,6 +54,7 @@ const DefaultServiceWorkerReadyTimeoutMS = 60_000
5454const DefaultServiceWorkerPollIntervalMS = 100
5555const DefaultTargetSessionPollIntervalMS = 20
5656const DefaultWSConnectErrorSettleTimeoutMS = 250
57+ const DefaultClientHeartbeatIntervalMS = 250
5758
5859func boolPointer (value bool ) * bool {
5960 return & value
@@ -180,14 +181,16 @@ func freePort() (int, error) {
180181// --- public types --------------------------------------------------------
181182
182183type ServerConfig struct {
183- ServerLoopbackCDPURL string `json:"server_loopback_cdp_url,omitempty"`
184- ServerRoutes map [string ]string `json:"server_routes,omitempty"`
185- ServerBrowserToken string `json:"server_browser_token,omitempty"`
186- ServerCDPSendTimeoutMS int `json:"server_cdp_send_timeout_ms,omitempty"`
187- ServerLoopbackExecutionContextTimeoutMS int `json:"server_loopback_execution_context_timeout_ms,omitempty"`
188- ServerWSConnectErrorSettleTimeoutMS int `json:"server_ws_connect_error_settle_timeout_ms,omitempty"`
189- Options map [string ]any `json:"-"`
190- disabled bool
184+ ServerLoopbackCDPURL string `json:"server_loopback_cdp_url,omitempty"`
185+ ServerRoutes map [string ]string `json:"server_routes,omitempty"`
186+ ServerBrowserToken string `json:"server_browser_token,omitempty"`
187+ ServerCDPSendTimeoutMS int `json:"server_cdp_send_timeout_ms,omitempty"`
188+ ServerLoopbackExecutionContextTimeoutMS int `json:"server_loopback_execution_context_timeout_ms,omitempty"`
189+ ServerWSConnectErrorSettleTimeoutMS int `json:"server_ws_connect_error_settle_timeout_ms,omitempty"`
190+ ServerDownstreamClientTimeoutMS int `json:"server_downstream_client_timeout_ms,omitempty"`
191+ ServerCloseBrowserOnDownstreamDisconnect * bool `json:"server_close_browser_on_downstream_disconnect,omitempty"`
192+ Options map [string ]any `json:"-"`
193+ disabled bool
191194}
192195
193196var ServerNone = & ServerConfig {disabled : true }
@@ -254,6 +257,7 @@ type ClientConfig struct {
254257 ClientMirrorUpstreamEvents * bool `json:"client_mirror_upstream_events,omitempty"`
255258 ClientCDPSendTimeoutMS int `json:"client_cdp_send_timeout_ms,omitempty"`
256259 ClientEventWaitTimeoutMS int `json:"client_event_wait_timeout_ms,omitempty"`
260+ ClientHeartbeatIntervalMS int `json:"client_heartbeat_interval_ms,omitempty"`
257261}
258262
259263type Options struct {
@@ -406,6 +410,7 @@ type ModCDPClient struct {
406410 launchedBrowser * LaunchedBrowser
407411 extensionInjectors []extensionInjector
408412 configuredPeerGeneration int64
413+ heartbeatStop chan struct {}
409414}
410415
411416type extensionInjector interface {
@@ -501,6 +506,9 @@ func New(opts Options) *ModCDPClient {
501506 if opts .Client .ClientEventWaitTimeoutMS == 0 {
502507 opts .Client .ClientEventWaitTimeoutMS = DefaultEventWaitTimeoutMS
503508 }
509+ if opts .Client .ClientHeartbeatIntervalMS == 0 {
510+ opts .Client .ClientHeartbeatIntervalMS = DefaultClientHeartbeatIntervalMS
511+ }
504512 if opts .Injector .InjectorExecutionContextTimeoutMS == 0 {
505513 opts .Injector .InjectorExecutionContextTimeoutMS = DefaultExecutionContextTimeoutMS
506514 }
@@ -575,7 +583,10 @@ func (c *ModCDPClient) Connect() error {
575583 return fmt .Errorf ("upstream transport did not connect" )
576584 }
577585 c .transport .OnRecv (func (message map [string ]any ) { c .handleMessage (message ) })
578- c .transport .OnClose (func (err error ) { c .rejectAll (err ) })
586+ c .transport .OnClose (func (err error ) {
587+ c .stopHeartbeat ()
588+ c .rejectAll (err )
589+ })
579590 if transportpkg .EndpointKindForUpstream (c .Upstream .UpstreamMode ) == UpstreamEndpointKindModCDPServer {
580591 if err := c .transport .WaitForPeer (); err != nil {
581592 c .Close ()
@@ -588,6 +599,7 @@ func (c *ModCDPClient) Connect() error {
588599 }
589600 c .configuredPeerGeneration = c .transport .PeerGeneration ()
590601 }
602+ c .startHeartbeat ()
591603 c .startPingLatencyMeasurement ()
592604 connectedAt := time .Now ().UnixMilli ()
593605 c .ConnectTiming = map [string ]any {
@@ -683,6 +695,7 @@ func (c *ModCDPClient) Connect() error {
683695 return fmt .Errorf ("Mod.configure: %w" , err )
684696 }
685697 }
698+ c .startHeartbeat ()
686699 c .startPingLatencyMeasurement ()
687700 connectedAt := time .Now ().UnixMilli ()
688701 c .ConnectTiming = map [string ]any {
@@ -899,6 +912,7 @@ func (c *ModCDPClient) serverConfigureParams(customCommands []map[string]any, cu
899912 "server_cdp_send_timeout_ms" : c .Client .ClientCDPSendTimeoutMS ,
900913 "server_loopback_execution_context_timeout_ms" : c .Injector .InjectorExecutionContextTimeoutMS ,
901914 "server_ws_connect_error_settle_timeout_ms" : c .Upstream .UpstreamWSConnectErrorSettleTimeoutMS ,
915+ "server_downstream_client_timeout_ms" : maxInt (c .Client .ClientHeartbeatIntervalMS * 4 , 1_000 ),
902916 }
903917 if c .Server != nil {
904918 server ["server_loopback_cdp_url" ] = c .Server .ServerLoopbackCDPURL
@@ -915,6 +929,12 @@ func (c *ModCDPClient) serverConfigureParams(customCommands []map[string]any, cu
915929 if c .Server .ServerWSConnectErrorSettleTimeoutMS != 0 {
916930 server ["server_ws_connect_error_settle_timeout_ms" ] = c .Server .ServerWSConnectErrorSettleTimeoutMS
917931 }
932+ if c .Server .ServerDownstreamClientTimeoutMS != 0 {
933+ server ["server_downstream_client_timeout_ms" ] = c .Server .ServerDownstreamClientTimeoutMS
934+ }
935+ if c .Server .ServerCloseBrowserOnDownstreamDisconnect != nil {
936+ server ["server_close_browser_on_downstream_disconnect" ] = * c .Server .ServerCloseBrowserOnDownstreamDisconnect
937+ }
918938 for key , value := range c .Server .Options {
919939 server [key ] = value
920940 }
@@ -1410,6 +1430,7 @@ func handlerPointer(handler Handler) uintptr {
14101430}
14111431
14121432func (c * ModCDPClient ) Close () {
1433+ c .stopHeartbeat ()
14131434 if c .launchedBrowser != nil {
14141435 c .launchedBrowser .Close ()
14151436 c .launchedBrowser = nil
@@ -1664,6 +1685,48 @@ func (c *ModCDPClient) startPingLatencyMeasurement() {
16641685 }()
16651686}
16661687
1688+ func (c * ModCDPClient ) startHeartbeat () {
1689+ c .stopHeartbeat ()
1690+ if c .Server == nil || c .Server .ServerCloseBrowserOnDownstreamDisconnect == nil || ! * c .Server .ServerCloseBrowserOnDownstreamDisconnect {
1691+ return
1692+ }
1693+ interval := c .Client .ClientHeartbeatIntervalMS
1694+ if interval <= 0 {
1695+ return
1696+ }
1697+ stop := make (chan struct {})
1698+ c .heartbeatStop = stop
1699+ go func () {
1700+ ticker := time .NewTicker (time .Duration (interval ) * time .Millisecond )
1701+ defer ticker .Stop ()
1702+ for {
1703+ select {
1704+ case <- ticker .C :
1705+ if _ , err := c .Send ("Mod.ping" , map [string ]any {"sent_at" : time .Now ().UnixMilli ()}); err != nil {
1706+ return
1707+ }
1708+ case <- stop :
1709+ return
1710+ }
1711+ }
1712+ }()
1713+ }
1714+
1715+ func (c * ModCDPClient ) stopHeartbeat () {
1716+ if c .heartbeatStop == nil {
1717+ return
1718+ }
1719+ close (c .heartbeatStop )
1720+ c .heartbeatStop = nil
1721+ }
1722+
1723+ func maxInt (left int , right int ) int {
1724+ if left > right {
1725+ return left
1726+ }
1727+ return right
1728+ }
1729+
16671730func numberAsInt64 (value any ) (int64 , bool ) {
16681731 switch v := value .(type ) {
16691732 case int64 :
0 commit comments