github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/client/realtime.go (about) 1 package client 2 3 import ( 4 "encoding/json" 5 "errors" 6 "io" 7 "net/http" 8 "net/url" 9 10 "github.com/cozy/cozy-stack/client/request" 11 "github.com/gorilla/websocket" 12 ) 13 14 // RealtimeOptions contains the options to create the realtime subscription 15 // channel. 16 type RealtimeOptions struct { 17 DocTypes []string 18 } 19 20 // RealtimeChannel is used to create a realtime connection with the server. The 21 // Channel method can be used to retrieve a channel on which the realtime 22 // events can be received. 23 type RealtimeChannel struct { 24 socket *websocket.Conn 25 ch chan *RealtimeServerMessage 26 closed chan struct{} 27 } 28 29 // RealtimeClientMessage is a struct containing the structure of the client 30 // messages sent to the server. 31 type RealtimeClientMessage struct { 32 Method string `json:"method"` 33 Payload interface{} `json:"payload"` 34 } 35 36 // RealtimeServerMessage is a struct containing the structure of the server 37 // messages received by the client. 38 type RealtimeServerMessage struct { 39 Event string `json:"event"` 40 Payload RealtimeServerPayload `json:"payload"` 41 } 42 43 // RealtimeServerPayload is the payload content of the RealtimeServerMessage. 44 type RealtimeServerPayload struct { 45 // Response payload 46 Type string `json:"type"` 47 ID string `json:"id"` 48 Doc json.RawMessage `json:"doc"` 49 50 // Error payload 51 Status string `json:"status"` 52 Code string `json:"code"` 53 Title string `json:"title"` 54 } 55 56 // RealtimeClient returns a new RealtimeChannel that instantiate a realtime 57 // connection with the client server. 58 func (c *Client) RealtimeClient(opts RealtimeOptions) (*RealtimeChannel, error) { 59 var scheme string 60 if c.Scheme == "https" { 61 scheme = "wss" 62 } else { 63 scheme = "ws" 64 } 65 66 var err error 67 var authorizer request.Authorizer 68 if c.Authorizer != nil { 69 authorizer = c.Authorizer 70 } else { 71 authorizer, err = c.Authenticate() 72 } 73 if err != nil { 74 return nil, err 75 } 76 77 u := url.URL{ 78 Scheme: scheme, 79 Host: c.Domain, 80 Path: "/realtime/", 81 } 82 headers := make(http.Header) 83 if authHeader := authorizer.AuthHeader(); authHeader != "" { 84 headers.Add("Authorization", authHeader) 85 } 86 socket, _, err := websocket.DefaultDialer.Dial(u.String(), headers) 87 if err != nil { 88 return nil, err 89 } 90 91 realtimeToken := authorizer.RealtimeToken() 92 if realtimeToken != "" { 93 err = socket.WriteJSON(RealtimeClientMessage{ 94 Method: "AUTH", 95 Payload: authorizer.RealtimeToken(), 96 }) 97 if err != nil { 98 return nil, err 99 } 100 } 101 102 for _, docType := range opts.DocTypes { 103 err = socket.WriteJSON(RealtimeClientMessage{ 104 Method: "SUBSCRIBE", 105 Payload: struct { 106 Type string `json:"type"` 107 }{Type: docType}, 108 }) 109 if err != nil { 110 socket.Close() 111 return nil, err 112 } 113 } 114 115 channel := &RealtimeChannel{ 116 socket: socket, 117 ch: make(chan *RealtimeServerMessage), 118 closed: make(chan struct{}), 119 } 120 121 go channel.pump() 122 123 return channel, nil 124 } 125 126 // Channel returns the channel of realtime server messages received by the client 127 // from the server. 128 func (r *RealtimeChannel) Channel() <-chan *RealtimeServerMessage { 129 return r.ch 130 } 131 132 func (r *RealtimeChannel) pump() { 133 defer close(r.ch) 134 var err error 135 for { 136 var msg RealtimeServerMessage 137 if err = r.socket.ReadJSON(&msg); err != nil { 138 break 139 } 140 select { 141 case r.ch <- &msg: 142 case <-r.closed: 143 return 144 } 145 } 146 if !errors.Is(err, io.EOF) { 147 msg := RealtimeServerMessage{ 148 Event: "error", 149 Payload: RealtimeServerPayload{Title: err.Error()}, 150 } 151 select { 152 case r.ch <- &msg: 153 case <-r.closed: 154 return 155 } 156 } 157 } 158 159 // Close will close the underlying connection of the realtime channel and close 160 // the channel of messages. 161 func (r *RealtimeChannel) Close() error { 162 close(r.closed) 163 return r.socket.Close() 164 }