package main import ( "context" "fmt" "log" "net" "time" messages "git.tornberg.me/go-cart-actor/proto" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) // cartActorGRPCServer implements the CartActor and ControlPlane gRPC services. // It delegates cart operations to a grain pool and cluster operations to a synced pool. type cartActorGRPCServer struct { messages.UnimplementedCartActorServer messages.UnimplementedControlPlaneServer pool GrainPool // For cart state mutations and queries syncedPool *SyncedPool // For cluster membership and control } // NewCartActorGRPCServer creates and initializes the server. func NewCartActorGRPCServer(pool GrainPool, syncedPool *SyncedPool) *cartActorGRPCServer { return &cartActorGRPCServer{ pool: pool, syncedPool: syncedPool, } } // applyMutation routes a single cart mutation to the target grain (used by per-mutation RPC handlers). func (s *cartActorGRPCServer) applyMutation(cartID string, mutation interface{}) *messages.CartMutationReply { grain, err := s.pool.Apply(ToCartId(cartID), mutation) if err != nil { return &messages.CartMutationReply{ StatusCode: 500, Result: &messages.CartMutationReply_Error{Error: err.Error()}, ServerTimestamp: time.Now().Unix(), } } cartState := ToCartState(grain) return &messages.CartMutationReply{ StatusCode: 200, Result: &messages.CartMutationReply_State{State: cartState}, ServerTimestamp: time.Now().Unix(), } } func (s *cartActorGRPCServer) AddRequest(ctx context.Context, req *messages.AddRequestRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } func (s *cartActorGRPCServer) AddItem(ctx context.Context, req *messages.AddItemRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } func (s *cartActorGRPCServer) RemoveItem(ctx context.Context, req *messages.RemoveItemRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } func (s *cartActorGRPCServer) RemoveDelivery(ctx context.Context, req *messages.RemoveDeliveryRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } func (s *cartActorGRPCServer) ChangeQuantity(ctx context.Context, req *messages.ChangeQuantityRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } func (s *cartActorGRPCServer) SetDelivery(ctx context.Context, req *messages.SetDeliveryRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } func (s *cartActorGRPCServer) SetPickupPoint(ctx context.Context, req *messages.SetPickupPointRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } /* Checkout RPC removed. Checkout is handled at the HTTP layer (PoolServer.HandleCheckout). */ func (s *cartActorGRPCServer) SetCartItems(ctx context.Context, req *messages.SetCartItemsRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } func (s *cartActorGRPCServer) OrderCompleted(ctx context.Context, req *messages.OrderCompletedRequest) (*messages.CartMutationReply, error) { if req.GetCartId() == "" { return &messages.CartMutationReply{ StatusCode: 400, Result: &messages.CartMutationReply_Error{Error: "cart_id is required"}, ServerTimestamp: time.Now().Unix(), }, nil } return s.applyMutation(req.GetCartId(), req.GetPayload()), nil } // GetState retrieves the current state of a cart grain. func (s *cartActorGRPCServer) GetState(ctx context.Context, req *messages.StateRequest) (*messages.StateReply, error) { if req.GetCartId() == "" { return &messages.StateReply{ StatusCode: 400, Result: &messages.StateReply_Error{Error: "cart_id is required"}, }, nil } cartID := ToCartId(req.GetCartId()) grain, err := s.pool.Get(cartID) if err != nil { return &messages.StateReply{ StatusCode: 500, Result: &messages.StateReply_Error{Error: err.Error()}, }, nil } cartState := ToCartState(grain) return &messages.StateReply{ StatusCode: 200, Result: &messages.StateReply_State{State: cartState}, }, nil } // StartGRPCServer configures and starts the unified gRPC server on the given address. // It registers both the CartActor and ControlPlane services. func StartGRPCServer(addr string, pool GrainPool, syncedPool *SyncedPool) (*grpc.Server, error) { lis, err := net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("failed to listen: %w", err) } grpcServer := grpc.NewServer() server := NewCartActorGRPCServer(pool, syncedPool) messages.RegisterCartActorServer(grpcServer, server) messages.RegisterControlPlaneServer(grpcServer, server) reflection.Register(grpcServer) log.Printf("gRPC server listening on %s", addr) go func() { if err := grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve gRPC: %v", err) } }() return grpcServer, nil }