Implement notifications service with shared messaging and email drivers (#36)

This commit is contained in:
Stephan D committed 2026-09-12 14:41:55 +02:00
1 parent 36277b6c45
commit 5c2d36281b
54 files changed
+3746 -6

No files matched your search

@@ -0,0 +1,178 @@
package natscap
import (
"context"
"errors"
"strings"
"sync"
"time"
merrors "git.sendico.io/tech/sxchnge/shared/errors"
"git.sendico.io/tech/sxchnge/shared/messaging/capability"
"git.sendico.io/tech/sxchnge/shared/messaging/reliable"
"github.com/nats-io/nats.go"
)
type natsSubscription struct {
mu sync.Mutex
sub *nats.Subscription
closed bool
}
func fetchPollContext(ctx context.Context, wait time.Duration) (context.Context, context.CancelFunc, bool) {
if ctx == nil {
ctx = context.Background()
}
if wait <= 0 {
return ctx, nil, false
}
if deadline, hasDeadline := ctx.Deadline(); hasDeadline && time.Until(deadline) <= wait {
return ctx, nil, false
}
nextCtx, cancel := context.WithTimeout(ctx, wait)
return nextCtx, cancel, true
}
func (s *natsSubscription) Fetch(ctx context.Context, limit int, wait time.Duration) ([]reliable.Delivery, error) {
if limit <= 0 {
return nil, merrors.InvalidArgument("fetch limit must be greater than zero", "limit")
}
s.mu.Lock()
closed := s.closed
sub := s.sub
s.mu.Unlock()
if closed || sub == nil {
return nil, capability.ErrClosed
}
fetchCtx, cancel, derivedDeadline := fetchPollContext(ctx, wait)
if cancel != nil {
defer cancel()
}
msgs, err := sub.Fetch(limit, nats.Context(fetchCtx))
if err != nil {
switch {
case errors.Is(err, nats.ErrTimeout):
return nil, nil
case derivedDeadline && errors.Is(err, context.DeadlineExceeded):
return nil, nil
default:
return nil, err
}
}
s.mu.Lock()
closed = s.closed
s.mu.Unlock()
if closed {
return nil, capability.ErrClosed
}
result := make([]reliable.Delivery, 0, len(msgs))
for _, msg := range msgs {
if msg == nil {
continue
}
result = append(result, &natsDelivery{sub: s, msg: msg, record: messageRecord(msg), attempt: deliveryAttempt(msg)})
}
return result, nil
}
func (s *natsSubscription) Close() error {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil
}
s.closed = true
sub := s.sub
s.sub = nil
s.mu.Unlock()
if sub == nil {
return nil
}
if err := sub.Unsubscribe(); err != nil && !errors.Is(err, nats.ErrBadSubscription) {
return err
}
return nil
}
func (s *natsSubscription) isClosed() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.closed
}
type natsDelivery struct {
sub *natsSubscription
msg *nats.Msg
record reliable.Record
attempt int
mu sync.Mutex
acked bool
}
func (d *natsDelivery) Record() reliable.Record {
return d.record
}
func (d *natsDelivery) Attempt() int {
if d.attempt <= 0 {
return 1
}
return d.attempt
}
func (d *natsDelivery) Ack(ctx context.Context) error {
d.mu.Lock()
if d.acked {
d.mu.Unlock()
return nil
}
if d.sub != nil && d.sub.isClosed() {
d.mu.Unlock()
return capability.ErrClosed
}
msg := d.msg
d.mu.Unlock()
if msg == nil {
return capability.ErrClosed
}
if err := msg.AckSync(nats.Context(ctx)); err != nil {
return err
}
d.mu.Lock()
d.acked = true
d.mu.Unlock()
return nil
}
func messageRecord(msg *nats.Msg) reliable.Record {
headers := make(map[string][]string, len(msg.Header)+1)
for key, values := range msg.Header {
headers[strings.ToLower(strings.TrimSpace(key))] = append([]string(nil), values...)
}
messageID := strings.TrimSpace(msg.Header.Get(nats.MsgIdHdr))
if businessID := reliable.HeaderValue(headers, reliable.HeaderOriginalBusinessMsgID); businessID != "" {
messageID = businessID
}
headers[reliable.HeaderTransportMessageID] = []string{strings.TrimSpace(msg.Header.Get(nats.MsgIdHdr))}
return reliable.Record{
Subject: strings.TrimSpace(msg.Subject),
Payload: append([]byte(nil), msg.Data...),
Headers: headers,
MessageID: strings.TrimSpace(messageID),
}
}
func deliveryAttempt(msg *nats.Msg) int {
if msg == nil {
return 1
}
meta, err := msg.Metadata()
if err != nil || meta == nil || meta.NumDelivered == 0 {
return 1
}
if meta.NumDelivered > uint64(^uint(0)>>1) {
return int(^uint(0) >> 1)
}
return int(meta.NumDelivered)
}