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  }