- Update agent-ops project rules and roadmap files - Add proto-socket infrastructure communication rail milestone - Update Flutter pubspec.lock and contracts notes - Enhance core service: config, HTTP middleware, router - Add notification module improvements - Add protosocket internal package
153 lines
3.3 KiB
Go
153 lines
3.3 KiB
Go
package protosocket
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"sync"
|
|
|
|
toki "git.toki-labs.com/toki/proto-socket/go"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
"nhooyr.io/websocket"
|
|
)
|
|
|
|
type Config struct {
|
|
HeartbeatIntervalSec int
|
|
HeartbeatWaitSec int
|
|
}
|
|
|
|
type Server struct {
|
|
cfg Config
|
|
dispatcher *Dispatcher
|
|
logger *slog.Logger
|
|
mu sync.Mutex
|
|
clients map[*toki.WsClient]struct{}
|
|
}
|
|
|
|
func NewServer(cfg Config, logger *slog.Logger) *Server {
|
|
return &Server{
|
|
cfg: cfg,
|
|
dispatcher: NewDispatcher(),
|
|
logger: logger,
|
|
clients: make(map[*toki.WsClient]struct{}),
|
|
}
|
|
}
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
opts := &websocket.AcceptOptions{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
conn, err := websocket.Accept(w, r, opts)
|
|
if err != nil {
|
|
s.logger.Error("failed to accept websocket connection", "error", err)
|
|
return
|
|
}
|
|
|
|
client := toki.NewWsClient(
|
|
conn,
|
|
s.cfg.HeartbeatIntervalSec,
|
|
s.cfg.HeartbeatWaitSec,
|
|
ParserMap(),
|
|
)
|
|
|
|
s.mu.Lock()
|
|
s.clients[client] = struct{}{}
|
|
s.mu.Unlock()
|
|
|
|
client.AddDisconnectListener(func(c *toki.WsClient) {
|
|
s.mu.Lock()
|
|
delete(s.clients, c)
|
|
s.mu.Unlock()
|
|
s.logger.Info("websocket client disconnected")
|
|
})
|
|
|
|
toki.AddRequestListenerTyped[*structpb.Struct, *structpb.Struct](&client.Communicator, func(req *structpb.Struct) (*structpb.Struct, error) {
|
|
env, err := EnvelopeFromStruct(req)
|
|
if err != nil {
|
|
errEnv := Envelope{
|
|
ProtocolVersion: ProtocolVersion,
|
|
ID: generateID(),
|
|
CorrelationID: "",
|
|
Type: "error",
|
|
Error: &EnvelopeError{
|
|
Code: "INVALID_ENVELOPE",
|
|
Message: "failed to parse envelope: " + err.Error(),
|
|
},
|
|
}
|
|
res, _ := errEnv.ToStruct()
|
|
return res, nil
|
|
}
|
|
|
|
resEnv := s.dispatcher.Dispatch(r.Context(), env)
|
|
resStruct, err := resEnv.ToStruct()
|
|
if err != nil {
|
|
errEnv := Envelope{
|
|
ProtocolVersion: ProtocolVersion,
|
|
ID: generateID(),
|
|
CorrelationID: env.ID,
|
|
Type: "error",
|
|
Error: &EnvelopeError{
|
|
Code: "SERIALIZATION_ERROR",
|
|
Message: "failed to serialize response envelope: " + err.Error(),
|
|
},
|
|
}
|
|
res, _ := errEnv.ToStruct()
|
|
return res, nil
|
|
}
|
|
|
|
return resStruct, nil
|
|
})
|
|
|
|
s.logger.Info("websocket client connected successfully")
|
|
}
|
|
|
|
func (s *Server) Close() error {
|
|
s.mu.Lock()
|
|
clients := make([]*toki.WsClient, 0, len(s.clients))
|
|
for cl := range s.clients {
|
|
clients = append(clients, cl)
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
for _, cl := range clients {
|
|
_ = cl.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) BroadcastEnvelope(ctx context.Context, env Envelope) error {
|
|
structMsg, err := env.ToStruct()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
clients := make([]*toki.WsClient, 0, len(s.clients))
|
|
for cl := range s.clients {
|
|
clients = append(clients, cl)
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
for _, cl := range clients {
|
|
if cl.IsAlive() {
|
|
if err := cl.Send(structMsg); err != nil {
|
|
s.logger.Error("failed to send broadcast to client", "error", err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) Dispatcher() *Dispatcher {
|
|
return s.dispatcher
|
|
}
|
|
|
|
func ParserMap() toki.ParserMap {
|
|
return toki.ParserMap{
|
|
toki.TypeNameOf(&structpb.Struct{}): func(b []byte) (proto.Message, error) {
|
|
msg := &structpb.Struct{}
|
|
return msg, proto.Unmarshal(b, msg)
|
|
},
|
|
}
|
|
}
|