github.com/pojntfx/hydrapp/hydrapp@v0.0.0-20240516002902-d08759d6ca9f/pkg/generators/backend_react_panrpc.go.tpl (about) 1 package backend 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "log" 9 "net" 10 "net/http" 11 "net/url" 12 "strings" 13 "time" 14 15 "github.com/pojntfx/hydrapp/hydrapp/pkg/utils" 16 "github.com/pojntfx/panrpc/go/pkg/rpc" 17 "nhooyr.io/websocket" 18 ) 19 20 type exampleStruct struct { 21 Name string `json:"name"` 22 } 23 24 type local struct { 25 ForRemotes func( 26 cb func(remoteID string, remote remote) error, 27 ) error 28 } 29 30 func (l *local) ExamplePrintString(ctx context.Context, msg string) error { 31 fmt.Println(msg) 32 33 return nil 34 } 35 36 func (l *local) ExamplePrintStruct(ctx context.Context, input exampleStruct) error { 37 fmt.Println(input) 38 39 return nil 40 } 41 42 func (l *local) ExampleReturnError(ctx context.Context) error { 43 return errors.New("test error") 44 } 45 46 func (l *local) ExampleReturnString(ctx context.Context) (string, error) { 47 return "Test string", nil 48 } 49 50 func (l *local) ExampleReturnStruct(ctx context.Context) (exampleStruct, error) { 51 return exampleStruct{ 52 Name: "Alice", 53 }, nil 54 } 55 56 func (l *local) ExampleReturnStringAndError(ctx context.Context) (string, error) { 57 return "Test string", errors.New("test error") 58 } 59 60 func (l *local) ExampleCallback(ctx context.Context) error { 61 var peer *remote 62 63 _ = l.ForRemotes(func(remoteID string, remote remote) error { 64 peer = &remote 65 66 return nil 67 }) 68 69 if peer != nil { 70 ticker := time.NewTicker(time.Second) 71 i := 0 72 for { 73 if i >= 3 { 74 ticker.Stop() 75 76 return nil 77 } 78 79 <-ticker.C 80 81 if err := peer.ExampleNotification(ctx, "Backend time: "+time.Now().Format(time.RFC3339)); err != nil { 82 return err 83 } 84 85 i++ 86 } 87 } 88 89 return nil 90 } 91 92 func (s *local) ExampleClosure( 93 ctx context.Context, 94 length int, 95 onIteration func(ctx context.Context, i int, b string) (string, error), 96 ) (int, error) { 97 for i := 0; i < length; i++ { 98 rv, err := onIteration(ctx, i, "This is from the backend") 99 if err != nil { 100 return -1, err 101 } 102 103 log.Println("Closure returned:", rv) 104 } 105 106 return length, nil 107 } 108 109 type remote struct { 110 ExampleNotification func(ctx context.Context, msg string) error 111 } 112 113 func StartServer(ctx context.Context, addr string, heartbeat time.Duration, localhostize bool) (string, func() error, error) { 114 if strings.TrimSpace(addr) == "" { 115 addr = ":0" 116 } 117 118 service := &local{} 119 120 clients := 0 121 registry := rpc.NewRegistry[remote, json.RawMessage]( 122 service, 123 124 ctx, 125 126 &rpc.Options{ 127 OnClientConnect: func(remoteID string) { 128 clients++ 129 130 log.Printf("%v clients connected", clients) 131 }, 132 OnClientDisconnect: func(remoteID string) { 133 clients-- 134 135 log.Printf("%v clients connected", clients) 136 }, 137 }, 138 ) 139 service.ForRemotes = registry.ForRemotes 140 141 listener, err := net.Listen("tcp", addr) 142 if err != nil { 143 panic(err) 144 } 145 146 log.Println("Listening on", listener.Addr()) 147 148 go func() { 149 defer listener.Close() 150 151 if err := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 152 defer func() { 153 if err := recover(); err != nil { 154 w.WriteHeader(http.StatusInternalServerError) 155 156 log.Printf("Client disconnected with error: %v", err) 157 } 158 }() 159 160 switch r.Method { 161 case http.MethodGet: 162 c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ 163 OriginPatterns: []string{"*"}, 164 }) 165 if err != nil { 166 panic(err) 167 } 168 169 pings := time.NewTicker(time.Second / 2) 170 defer pings.Stop() 171 172 errs := make(chan error) 173 go func() { 174 for range pings.C { 175 if err := c.Ping(ctx); err != nil { 176 errs <- err 177 178 return 179 } 180 } 181 }() 182 183 conn := websocket.NetConn(ctx, c, websocket.MessageText) 184 defer conn.Close() 185 186 encoder := json.NewEncoder(conn) 187 decoder := json.NewDecoder(conn) 188 189 go func() { 190 if err := registry.LinkStream( 191 func(v rpc.Message[json.RawMessage]) error { 192 return encoder.Encode(v) 193 }, 194 func(v *rpc.Message[json.RawMessage]) error { 195 return decoder.Decode(v) 196 }, 197 198 func(v any) (json.RawMessage, error) { 199 b, err := json.Marshal(v) 200 if err != nil { 201 return nil, err 202 } 203 204 return json.RawMessage(b), nil 205 }, 206 func(data json.RawMessage, v any) error { 207 return json.Unmarshal([]byte(data), v) 208 }, 209 ); err != nil { 210 errs <- err 211 212 return 213 } 214 }() 215 216 if err := <-errs; err != nil { 217 panic(err) 218 } 219 default: 220 w.WriteHeader(http.StatusMethodNotAllowed) 221 } 222 })); err != nil { 223 if strings.HasSuffix(err.Error(), "use of closed network connection") { 224 return 225 } 226 227 panic(err) 228 } 229 }() 230 231 url, err := url.Parse("ws://" + listener.Addr().String()) 232 if err != nil { 233 return "", nil, err 234 } 235 236 if localhostize { 237 return utils.Localhostize(url.String()), listener.Close, nil 238 } 239 240 return url.String(), listener.Close, nil 241 } 242