feat: add TypedMessageHandler + generic Decode[T] helper
- handler: add OnTypedMessage() for typed NATS message callbacks Avoids double-decode (msgpack→map→typed) by skipping map step - handler: refactor wrapHandler into wrapTypedHandler + wrapMapHandler - natsutil: add generic Decode[T](data) for direct msgpack→struct decode - tests: add typed handler tests + benchmark (11 tests pass)
This commit is contained in:
@@ -21,6 +21,12 @@ import (
|
||||
// data is the msgpack-decoded map. Return a response map (or nil for no reply).
|
||||
type MessageHandler func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error)
|
||||
|
||||
// TypedMessageHandler processes the raw NATS message without pre-decoding to
|
||||
// map[string]any. Services unmarshal msg.Data into their own typed structs,
|
||||
// avoiding the double-decode overhead. Return any msgpack-serialisable value
|
||||
// (a typed struct, map, or nil for no reply).
|
||||
type TypedMessageHandler func(ctx context.Context, msg *nats.Msg) (any, error)
|
||||
|
||||
// SetupFunc is called once before the handler starts processing messages.
|
||||
type SetupFunc func(ctx context.Context) error
|
||||
|
||||
@@ -35,10 +41,11 @@ type Handler struct {
|
||||
Subject string
|
||||
QueueGroup string
|
||||
|
||||
onSetup SetupFunc
|
||||
onTeardown TeardownFunc
|
||||
onMessage MessageHandler
|
||||
running bool
|
||||
onSetup SetupFunc
|
||||
onTeardown TeardownFunc
|
||||
onMessage MessageHandler
|
||||
onTypedMessage TypedMessageHandler
|
||||
running bool
|
||||
}
|
||||
|
||||
// New creates a Handler for the given NATS subject.
|
||||
@@ -70,6 +77,11 @@ func (h *Handler) OnTeardown(fn TeardownFunc) { h.onTeardown = fn }
|
||||
// OnMessage registers the message handler callback.
|
||||
func (h *Handler) OnMessage(fn MessageHandler) { h.onMessage = fn }
|
||||
|
||||
// OnTypedMessage registers a typed message handler. It replaces OnMessage —
|
||||
// wrapHandler will skip the map[string]any decode and let the callback
|
||||
// unmarshal msg.Data directly.
|
||||
func (h *Handler) OnTypedMessage(fn TypedMessageHandler) { h.onTypedMessage = fn }
|
||||
|
||||
// Run starts the handler: telemetry, health server, NATS subscription, and blocks until SIGTERM/SIGINT.
|
||||
func (h *Handler) Run() error {
|
||||
// Structured logging
|
||||
@@ -119,7 +131,7 @@ func (h *Handler) Run() error {
|
||||
}
|
||||
|
||||
// Subscribe
|
||||
if h.onMessage == nil {
|
||||
if h.onMessage == nil && h.onTypedMessage == nil {
|
||||
return fmt.Errorf("no message handler registered")
|
||||
}
|
||||
if err := h.NATS.Subscribe(h.Subject, h.wrapHandler(ctx), h.QueueGroup); err != nil {
|
||||
@@ -148,8 +160,41 @@ func (h *Handler) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapHandler creates a nats.MsgHandler that decodes msgpack and dispatches to the user handler.
|
||||
// wrapHandler creates a nats.MsgHandler that dispatches to the registered callback.
|
||||
// If OnTypedMessage was used, msg.Data is passed directly without map decode.
|
||||
// If OnMessage was used, msg.Data is decoded to map[string]any first.
|
||||
func (h *Handler) wrapHandler(ctx context.Context) nats.MsgHandler {
|
||||
if h.onTypedMessage != nil {
|
||||
return h.wrapTypedHandler(ctx)
|
||||
}
|
||||
return h.wrapMapHandler(ctx)
|
||||
}
|
||||
|
||||
// wrapTypedHandler dispatches to the TypedMessageHandler (no map decode).
|
||||
func (h *Handler) wrapTypedHandler(ctx context.Context) nats.MsgHandler {
|
||||
return func(msg *nats.Msg) {
|
||||
response, err := h.onTypedMessage(ctx, msg)
|
||||
if err != nil {
|
||||
slog.Error("handler error", "subject", msg.Subject, "error", err)
|
||||
if msg.Reply != "" {
|
||||
_ = h.NATS.Publish(msg.Reply, map[string]any{
|
||||
"error": true,
|
||||
"message": err.Error(),
|
||||
"type": fmt.Sprintf("%T", err),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if response != nil && msg.Reply != "" {
|
||||
if err := h.NATS.Publish(msg.Reply, response); err != nil {
|
||||
slog.Error("failed to publish reply", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wrapMapHandler dispatches to the legacy MessageHandler (decodes to map first).
|
||||
func (h *Handler) wrapMapHandler(ctx context.Context) nats.MsgHandler {
|
||||
return func(msg *nats.Msg) {
|
||||
data, err := natsutil.DecodeMsgpackMap(msg.Data)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user