Skip to content

Commit d060d01

Browse files
authored
feat: add WebSocket routing (#503)
* feat: add WebSocket routing * chore: roll to Playwright v1.48.2
1 parent 3e8cb5f commit d060d01

19 files changed

+1029
-179
lines changed

browser_context.go

+92-52
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type browserContextImpl struct {
2020
options *BrowserNewContextOptions
2121
pages []Page
2222
routes []*routeHandlerEntry
23+
webSocketRoutes []*webSocketRouteHandler
2324
ownedPage Page
2425
browser *browserImpl
2526
serviceWorkers []Worker
@@ -44,7 +45,7 @@ func (b *browserContextImpl) SetDefaultNavigationTimeout(timeout float64) {
4445

4546
func (b *browserContextImpl) setDefaultNavigationTimeoutImpl(timeout *float64) {
4647
b.timeoutSettings.SetDefaultNavigationTimeout(timeout)
47-
b.channel.SendNoReply("setDefaultNavigationTimeoutNoReply", true, map[string]interface{}{
48+
b.channel.SendNoReplyInternal("setDefaultNavigationTimeoutNoReply", map[string]interface{}{
4849
"timeout": timeout,
4950
})
5051
}
@@ -55,7 +56,7 @@ func (b *browserContextImpl) SetDefaultTimeout(timeout float64) {
5556

5657
func (b *browserContextImpl) setDefaultTimeoutImpl(timeout *float64) {
5758
b.timeoutSettings.SetDefaultTimeout(timeout)
58-
b.channel.SendNoReply("setDefaultTimeoutNoReply", true, map[string]interface{}{
59+
b.channel.SendNoReplyInternal("setDefaultTimeoutNoReply", map[string]interface{}{
5960
"timeout": timeout,
6061
})
6162
}
@@ -541,7 +542,7 @@ func (b *browserContextImpl) onBinding(binding *bindingCallImpl) {
541542
if !ok || function == nil {
542543
return
543544
}
544-
binding.Call(function)
545+
go binding.Call(function)
545546
}
546547

547548
func (b *browserContextImpl) onClose() {
@@ -572,58 +573,56 @@ func (b *browserContextImpl) onPage(page Page) {
572573
}
573574

574575
func (b *browserContextImpl) onRoute(route *routeImpl) {
575-
go func() {
576-
b.Lock()
577-
route.context = b
578-
page := route.Request().(*requestImpl).safePage()
579-
routes := make([]*routeHandlerEntry, len(b.routes))
580-
copy(routes, b.routes)
581-
b.Unlock()
576+
b.Lock()
577+
route.context = b
578+
page := route.Request().(*requestImpl).safePage()
579+
routes := make([]*routeHandlerEntry, len(b.routes))
580+
copy(routes, b.routes)
581+
b.Unlock()
582582

583-
checkInterceptionIfNeeded := func() {
584-
b.Lock()
585-
defer b.Unlock()
586-
if len(b.routes) == 0 {
587-
_, err := b.connection.WrapAPICall(func() (interface{}, error) {
588-
err := b.updateInterceptionPatterns()
589-
return nil, err
590-
}, true)
591-
if err != nil {
592-
logger.Printf("could not update interception patterns: %v\n", err)
593-
}
583+
checkInterceptionIfNeeded := func() {
584+
b.Lock()
585+
defer b.Unlock()
586+
if len(b.routes) == 0 {
587+
_, err := b.connection.WrapAPICall(func() (interface{}, error) {
588+
err := b.updateInterceptionPatterns()
589+
return nil, err
590+
}, true)
591+
if err != nil {
592+
logger.Printf("could not update interception patterns: %v\n", err)
594593
}
595594
}
595+
}
596596

597-
url := route.Request().URL()
598-
for _, handlerEntry := range routes {
599-
// If the page or the context was closed we stall all requests right away.
600-
if (page != nil && page.closeWasCalled) || b.closeWasCalled {
601-
return
602-
}
603-
if !handlerEntry.Matches(url) {
604-
continue
605-
}
606-
if !slices.ContainsFunc(b.routes, func(entry *routeHandlerEntry) bool {
607-
return entry == handlerEntry
608-
}) {
609-
continue
610-
}
611-
if handlerEntry.WillExceed() {
612-
b.routes = slices.DeleteFunc(b.routes, func(rhe *routeHandlerEntry) bool {
613-
return rhe == handlerEntry
614-
})
615-
}
616-
handled := handlerEntry.Handle(route)
617-
checkInterceptionIfNeeded()
618-
yes := <-handled
619-
if yes {
620-
return
621-
}
597+
url := route.Request().URL()
598+
for _, handlerEntry := range routes {
599+
// If the page or the context was closed we stall all requests right away.
600+
if (page != nil && page.closeWasCalled) || b.closeWasCalled {
601+
return
622602
}
623-
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
624-
// the method will throw an error - silence it.
625-
_ = route.internalContinue(true)
626-
}()
603+
if !handlerEntry.Matches(url) {
604+
continue
605+
}
606+
if !slices.ContainsFunc(b.routes, func(entry *routeHandlerEntry) bool {
607+
return entry == handlerEntry
608+
}) {
609+
continue
610+
}
611+
if handlerEntry.WillExceed() {
612+
b.routes = slices.DeleteFunc(b.routes, func(rhe *routeHandlerEntry) bool {
613+
return rhe == handlerEntry
614+
})
615+
}
616+
handled := handlerEntry.Handle(route)
617+
checkInterceptionIfNeeded()
618+
yes := <-handled
619+
if yes {
620+
return
621+
}
622+
}
623+
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
624+
// the method will throw an error - silence it.
625+
_ = route.internalContinue(true)
627626
}
628627

629628
func (b *browserContextImpl) updateInterceptionPatterns() error {
@@ -726,6 +725,40 @@ func (b *browserContextImpl) OnWebError(fn func(WebError)) {
726725
b.On("weberror", fn)
727726
}
728727

728+
func (b *browserContextImpl) RouteWebSocket(url interface{}, handler func(WebSocketRoute)) error {
729+
b.Lock()
730+
defer b.Unlock()
731+
b.webSocketRoutes = slices.Insert(b.webSocketRoutes, 0, newWebSocketRouteHandler(newURLMatcher(url, b.options.BaseURL), handler))
732+
733+
return b.updateWebSocketInterceptionPatterns()
734+
}
735+
736+
func (b *browserContextImpl) onWebSocketRoute(wr WebSocketRoute) {
737+
b.Lock()
738+
index := slices.IndexFunc(b.webSocketRoutes, func(r *webSocketRouteHandler) bool {
739+
return r.Matches(wr.URL())
740+
})
741+
if index == -1 {
742+
b.Unlock()
743+
_, err := wr.ConnectToServer()
744+
if err != nil {
745+
logger.Println(err)
746+
}
747+
return
748+
}
749+
handler := b.webSocketRoutes[index]
750+
b.Unlock()
751+
handler.Handle(wr)
752+
}
753+
754+
func (b *browserContextImpl) updateWebSocketInterceptionPatterns() error {
755+
patterns := prepareWebSocketRouteHandlerInterceptionPatterns(b.webSocketRoutes)
756+
_, err := b.channel.Send("setWebSocketInterceptionPatterns", map[string]interface{}{
757+
"patterns": patterns,
758+
})
759+
return err
760+
}
761+
729762
func (b *browserContextImpl) effectiveCloseReason() *string {
730763
b.Lock()
731764
defer b.Unlock()
@@ -758,15 +791,22 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
758791
bt.request = fromChannel(initializer["requestContext"]).(*apiRequestContextImpl)
759792
bt.clock = newClock(bt)
760793
bt.channel.On("bindingCall", func(params map[string]interface{}) {
761-
go bt.onBinding(fromChannel(params["binding"]).(*bindingCallImpl))
794+
bt.onBinding(fromChannel(params["binding"]).(*bindingCallImpl))
762795
})
763796

764797
bt.channel.On("close", bt.onClose)
765798
bt.channel.On("page", func(payload map[string]interface{}) {
766799
bt.onPage(fromChannel(payload["page"]).(*pageImpl))
767800
})
768801
bt.channel.On("route", func(params map[string]interface{}) {
769-
bt.onRoute(fromChannel(params["route"]).(*routeImpl))
802+
bt.channel.CreateTask(func() {
803+
bt.onRoute(fromChannel(params["route"]).(*routeImpl))
804+
})
805+
})
806+
bt.channel.On("webSocketRoute", func(params map[string]interface{}) {
807+
bt.channel.CreateTask(func() {
808+
bt.onWebSocketRoute(fromChannel(params["webSocketRoute"]).(*webSocketRouteImpl))
809+
})
770810
})
771811
bt.channel.On("backgroundPage", bt.onBackgroundPage)
772812
bt.channel.On("serviceWorker", func(params map[string]interface{}) {

channel.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package playwright
22

3-
import "encoding/json"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
47

58
type channel struct {
69
eventEmitter
@@ -16,6 +19,23 @@ func (c *channel) MarshalJSON() ([]byte, error) {
1619
})
1720
}
1821

22+
// for catch errors of route handlers etc.
23+
func (c *channel) CreateTask(fn func()) {
24+
go func() {
25+
defer func() {
26+
if e := recover(); e != nil {
27+
err, ok := e.(error)
28+
if ok {
29+
c.connection.err.Set(err)
30+
} else {
31+
c.connection.err.Set(fmt.Errorf("%v", e))
32+
}
33+
}
34+
}()
35+
fn()
36+
}()
37+
}
38+
1939
func (c *channel) Send(method string, options ...interface{}) (interface{}, error) {
2040
return c.connection.WrapAPICall(func() (interface{}, error) {
2141
return c.innerSend(method, options...).GetResultValue()
@@ -30,16 +50,33 @@ func (c *channel) SendReturnAsDict(method string, options ...interface{}) (map[s
3050
}
3151

3252
func (c *channel) innerSend(method string, options ...interface{}) *protocolCallback {
53+
if err := c.connection.err.Get(); err != nil {
54+
c.connection.err.Set(nil)
55+
pc := newProtocolCallback(false, c.connection.abort)
56+
pc.SetError(err)
57+
return pc
58+
}
3359
params := transformOptions(options...)
3460
return c.connection.sendMessageToServer(c.owner, method, params, false)
3561
}
3662

37-
func (c *channel) SendNoReply(method string, isInternal bool, options ...interface{}) {
63+
// SendNoReply ignores return value and errors
64+
// almost equivalent to `send(...).catch(() => {})`
65+
func (c *channel) SendNoReply(method string, options ...interface{}) {
66+
c.innerSendNoReply(method, c.owner.isInternalType, options...)
67+
}
68+
69+
func (c *channel) SendNoReplyInternal(method string, options ...interface{}) {
70+
c.innerSendNoReply(method, true, options...)
71+
}
72+
73+
func (c *channel) innerSendNoReply(method string, isInternal bool, options ...interface{}) {
3874
params := transformOptions(options...)
3975
_, err := c.connection.WrapAPICall(func() (interface{}, error) {
4076
return c.connection.sendMessageToServer(c.owner, method, params, true).GetResult()
4177
}, isInternal)
4278
if err != nil {
79+
// ignore error actively, log only for debug
4380
logger.Printf("SendNoReply failed: %v\n", err)
4481
}
4582
}

channel_owner.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (c *channelOwner) setEventSubscriptionMapping(mapping map[string]string) {
4949
func (c *channelOwner) updateSubscription(event string, enabled bool) {
5050
protocolEvent, ok := c.eventToSubscriptionMapping[event]
5151
if ok {
52-
c.channel.SendNoReply("updateSubscription", true, map[string]interface{}{
52+
c.channel.SendNoReplyInternal("updateSubscription", map[string]interface{}{
5353
"event": protocolEvent,
5454
"enabled": enabled,
5555
})

connection.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type connection struct {
3434
tracingCount atomic.Int32
3535
abort chan struct{}
3636
abortOnce sync.Once
37+
err *safeValue[error] // for event listener error
3738
closedError *safeValue[error]
3839
}
3940

@@ -301,6 +302,7 @@ func newConnection(transport transport, localUtils ...*localUtilsImpl) *connecti
301302
objects: safe.NewSyncMap[string, *channelOwner](),
302303
transport: transport,
303304
isRemote: false,
305+
err: &safeValue[error]{},
304306
closedError: &safeValue[error]{},
305307
}
306308
if len(localUtils) > 0 {
@@ -393,7 +395,7 @@ func newProtocolCallback(noReply bool, abort <-chan struct{}) *protocolCallback
393395
}
394396
}
395397
return &protocolCallback{
396-
done: make(chan struct{}),
398+
done: make(chan struct{}, 1),
397399
abort: abort,
398400
}
399401
}

0 commit comments

Comments
 (0)