unified gateway interface

This commit is contained in:
Stephan D
2025-12-31 17:47:32 +01:00
parent 19b7b69bd8
commit 97ba7500dc
104 changed files with 8228 additions and 1742 deletions

View File

@@ -0,0 +1,157 @@
package discovery
import (
"os"
"strings"
"sync"
"time"
"github.com/google/uuid"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
)
type Announcer struct {
logger mlogger.Logger
producer msg.Producer
sender string
announce Announcement
startOnce sync.Once
stopOnce sync.Once
stopCh chan struct{}
doneCh chan struct{}
}
func NewAnnouncer(logger mlogger.Logger, producer msg.Producer, sender string, announce Announcement) *Announcer {
if logger != nil {
logger = logger.Named("discovery")
}
announce = normalizeAnnouncement(announce)
if announce.Service == "" {
announce.Service = strings.TrimSpace(sender)
}
if announce.ID == "" {
announce.ID = DefaultInstanceID(announce.Service)
}
if announce.InvokeURI == "" && announce.Service != "" {
announce.InvokeURI = DefaultInvokeURI(announce.Service)
}
return &Announcer{
logger: logger,
producer: producer,
sender: strings.TrimSpace(sender),
announce: announce,
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
}
func (a *Announcer) Start() {
if a == nil {
return
}
a.startOnce.Do(func() {
if a.producer == nil {
a.logWarn("Discovery announce skipped: producer not configured")
close(a.doneCh)
return
}
if strings.TrimSpace(a.announce.ID) == "" {
a.logWarn("Discovery announce skipped: missing instance id")
close(a.doneCh)
return
}
a.sendAnnouncement()
a.sendHeartbeat()
go a.heartbeatLoop()
})
}
func (a *Announcer) Stop() {
if a == nil {
return
}
a.stopOnce.Do(func() {
close(a.stopCh)
<-a.doneCh
})
}
func (a *Announcer) heartbeatLoop() {
defer close(a.doneCh)
interval := time.Duration(a.announce.Health.IntervalSec) * time.Second
if interval <= 0 {
interval = time.Duration(DefaultHealthIntervalSec) * time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-a.stopCh:
return
case <-ticker.C:
a.sendHeartbeat()
}
}
}
func (a *Announcer) sendAnnouncement() {
env := NewServiceAnnounceEnvelope(a.sender, a.announce)
if a.announce.Rail != "" {
env = NewGatewayAnnounceEnvelope(a.sender, a.announce)
}
if err := a.producer.SendMessage(env); err != nil {
a.logWarn("Failed to publish discovery announce: " + err.Error())
return
}
a.logInfo("Discovery announce published")
}
func (a *Announcer) sendHeartbeat() {
hb := Heartbeat{
ID: a.announce.ID,
Status: "ok",
TS: time.Now().Unix(),
}
if err := a.producer.SendMessage(NewHeartbeatEnvelope(a.sender, hb)); err != nil {
a.logWarn("Failed to publish discovery heartbeat: " + err.Error())
}
}
func (a *Announcer) logInfo(message string) {
if a.logger == nil {
return
}
a.logger.Info(message)
}
func (a *Announcer) logWarn(message string) {
if a.logger == nil {
return
}
a.logger.Warn(message)
}
func DefaultInstanceID(service string) string {
clean := strings.ToLower(strings.TrimSpace(service))
if clean == "" {
clean = "service"
}
host, _ := os.Hostname()
host = strings.ToLower(strings.TrimSpace(host))
uid := uuid.NewString()
if host == "" {
return clean + "_" + uid
}
return clean + "_" + host + "_" + uid
}
func DefaultInvokeURI(service string) string {
clean := strings.ToLower(strings.TrimSpace(service))
if clean == "" {
return ""
}
return "grpc://" + clean
}

137
api/pkg/discovery/client.go Normal file
View File

@@ -0,0 +1,137 @@
package discovery
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"github.com/google/uuid"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/messaging/broker"
cons "github.com/tech/sendico/pkg/messaging/consumer"
me "github.com/tech/sendico/pkg/messaging/envelope"
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
type Client struct {
logger mlogger.Logger
producer msg.Producer
consumer msg.Consumer
sender string
mu sync.Mutex
pending map[string]chan LookupResponse
}
func NewClient(logger mlogger.Logger, broker broker.Broker, producer msg.Producer, sender string) (*Client, error) {
if broker == nil {
return nil, errors.New("discovery client: broker is nil")
}
if logger != nil {
logger = logger.Named("discovery_client")
}
if producer == nil {
producer = msgproducer.NewProducer(logger, broker)
}
sender = strings.TrimSpace(sender)
if sender == "" {
sender = "discovery_client"
}
consumer, err := cons.NewConsumer(logger, broker, LookupResponseEvent())
if err != nil {
return nil, err
}
client := &Client{
logger: logger,
producer: producer,
consumer: consumer,
sender: sender,
pending: map[string]chan LookupResponse{},
}
go func() {
if err := consumer.ConsumeMessages(client.handleLookupResponse); err != nil && client.logger != nil {
client.logger.Warn("Discovery lookup consumer stopped", zap.Error(err))
}
}()
return client, nil
}
func (c *Client) Close() {
if c == nil {
return
}
if c.consumer != nil {
c.consumer.Close()
}
c.mu.Lock()
for key, ch := range c.pending {
close(ch)
delete(c.pending, key)
}
c.mu.Unlock()
}
func (c *Client) Lookup(ctx context.Context) (LookupResponse, error) {
if c == nil || c.producer == nil {
return LookupResponse{}, errors.New("discovery client: producer not configured")
}
requestID := uuid.NewString()
ch := make(chan LookupResponse, 1)
c.mu.Lock()
c.pending[requestID] = ch
c.mu.Unlock()
defer func() {
c.mu.Lock()
delete(c.pending, requestID)
c.mu.Unlock()
}()
req := LookupRequest{RequestID: requestID}
if err := c.producer.SendMessage(NewLookupRequestEnvelope(c.sender, req)); err != nil {
return LookupResponse{}, err
}
select {
case <-ctx.Done():
return LookupResponse{}, ctx.Err()
case resp := <-ch:
return resp, nil
}
}
func (c *Client) handleLookupResponse(_ context.Context, env me.Envelope) error {
var payload LookupResponse
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
c.logWarn("Failed to decode discovery lookup response", zap.Error(err))
return err
}
requestID := strings.TrimSpace(payload.RequestID)
if requestID == "" {
return nil
}
c.mu.Lock()
ch := c.pending[requestID]
c.mu.Unlock()
if ch != nil {
ch <- payload
}
return nil
}
func (c *Client) logWarn(message string, fields ...zap.Field) {
if c == nil || c.logger == nil {
return
}
c.logger.Warn(message, fields...)
}

View File

@@ -0,0 +1,31 @@
package discovery
import (
"github.com/tech/sendico/pkg/model"
nm "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
)
func ServiceAnnounceEvent() model.NotificationEvent {
return model.NewNotification(mservice.Discovery, nm.NADiscoveryServiceAnnounce)
}
func GatewayAnnounceEvent() model.NotificationEvent {
return model.NewNotification(mservice.Discovery, nm.NADiscoveryGatewayAnnounce)
}
func HeartbeatEvent() model.NotificationEvent {
return model.NewNotification(mservice.Discovery, nm.NADiscoveryHeartbeat)
}
func LookupRequestEvent() model.NotificationEvent {
return model.NewNotification(mservice.Discovery, nm.NADiscoveryLookupRequest)
}
func LookupResponseEvent() model.NotificationEvent {
return model.NewNotification(mservice.Discovery, nm.NADiscoveryLookupResponse)
}
func RefreshUIEvent() model.NotificationEvent {
return model.NewNotification(mservice.Discovery, nm.NADiscoveryRefreshUI)
}

View File

@@ -0,0 +1,69 @@
package discovery
type LookupRequest struct {
RequestID string `json:"requestId,omitempty"`
}
type LookupResponse struct {
RequestID string `json:"requestId,omitempty"`
Services []ServiceSummary `json:"services,omitempty"`
Gateways []GatewaySummary `json:"gateways,omitempty"`
}
type ServiceSummary struct {
ID string `json:"id"`
Service string `json:"service"`
Ops []string `json:"ops,omitempty"`
Version string `json:"version,omitempty"`
Healthy bool `json:"healthy,omitempty"`
InvokeURI string `json:"invokeURI,omitempty"`
}
type GatewaySummary struct {
ID string `json:"id"`
Rail string `json:"rail"`
Network string `json:"network,omitempty"`
Currencies []string `json:"currencies,omitempty"`
Ops []string `json:"ops,omitempty"`
Limits *Limits `json:"limits,omitempty"`
Version string `json:"version,omitempty"`
Healthy bool `json:"healthy,omitempty"`
RoutingPriority int `json:"routingPriority,omitempty"`
InvokeURI string `json:"invokeURI,omitempty"`
}
func (r *Registry) Lookup(now time.Time) LookupResponse {
entries := r.List(now, true)
resp := LookupResponse{
Services: make([]ServiceSummary, 0),
Gateways: make([]GatewaySummary, 0),
}
for _, entry := range entries {
if entry.Rail != "" {
resp.Gateways = append(resp.Gateways, GatewaySummary{
ID: entry.ID,
Rail: entry.Rail,
Network: entry.Network,
Currencies: cloneStrings(entry.Currencies),
Ops: cloneStrings(entry.Operations),
Limits: cloneLimits(entry.Limits),
Version: entry.Version,
Healthy: entry.Healthy,
RoutingPriority: entry.RoutingPriority,
InvokeURI: entry.InvokeURI,
})
continue
}
resp.Services = append(resp.Services, ServiceSummary{
ID: entry.ID,
Service: entry.Service,
Ops: cloneStrings(entry.Operations),
Version: entry.Version,
Healthy: entry.Healthy,
InvokeURI: entry.InvokeURI,
})
}
return resp
}

View File

@@ -0,0 +1,56 @@
package discovery
import (
"encoding/json"
"errors"
messaging "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/model"
)
type jsonEnvelope struct {
messaging.Envelope
payload any
}
func (e *jsonEnvelope) Serialize() ([]byte, error) {
if e.payload == nil {
return nil, errors.New("discovery envelope payload is nil")
}
data, err := json.Marshal(e.payload)
if err != nil {
return nil, err
}
return e.Envelope.Wrap(data)
}
func newEnvelope(sender string, event model.NotificationEvent, payload any) messaging.Envelope {
return &jsonEnvelope{
Envelope: messaging.CreateEnvelope(sender, event),
payload: payload,
}
}
func NewServiceAnnounceEnvelope(sender string, payload Announcement) messaging.Envelope {
return newEnvelope(sender, ServiceAnnounceEvent(), payload)
}
func NewGatewayAnnounceEnvelope(sender string, payload Announcement) messaging.Envelope {
return newEnvelope(sender, GatewayAnnounceEvent(), payload)
}
func NewHeartbeatEnvelope(sender string, payload Heartbeat) messaging.Envelope {
return newEnvelope(sender, HeartbeatEvent(), payload)
}
func NewLookupRequestEnvelope(sender string, payload LookupRequest) messaging.Envelope {
return newEnvelope(sender, LookupRequestEvent(), payload)
}
func NewLookupResponseEnvelope(sender string, payload LookupResponse) messaging.Envelope {
return newEnvelope(sender, LookupResponseEvent(), payload)
}
func NewRefreshUIEnvelope(sender string, payload RefreshEvent) messaging.Envelope {
return newEnvelope(sender, RefreshUIEvent(), payload)
}

View File

@@ -0,0 +1,258 @@
package discovery
import (
"strings"
"sync"
"time"
)
const (
DefaultHealthIntervalSec = 10
DefaultHealthTimeoutSec = 30
)
type RegistryEntry struct {
ID string `json:"id"`
Service string `json:"service"`
Rail string `json:"rail,omitempty"`
Network string `json:"network,omitempty"`
Operations []string `json:"operations,omitempty"`
Currencies []string `json:"currencies,omitempty"`
Limits *Limits `json:"limits,omitempty"`
InvokeURI string `json:"invokeURI,omitempty"`
RoutingPriority int `json:"routingPriority,omitempty"`
Version string `json:"version,omitempty"`
Health HealthParams `json:"health,omitempty"`
LastHeartbeat time.Time `json:"lastHeartbeat,omitempty"`
Status string `json:"status,omitempty"`
Healthy bool `json:"healthy,omitempty"`
}
type Registry struct {
mu sync.RWMutex
entries map[string]*RegistryEntry
}
type UpdateResult struct {
Entry RegistryEntry
IsNew bool
WasHealthy bool
BecameHealthy bool
}
func NewRegistry() *Registry {
return &Registry{
entries: map[string]*RegistryEntry{},
}
}
func (r *Registry) UpsertFromAnnouncement(announce Announcement, now time.Time) UpdateResult {
entry := registryEntryFromAnnouncement(normalizeAnnouncement(announce), now)
r.mu.Lock()
defer r.mu.Unlock()
existing, ok := r.entries[entry.ID]
wasHealthy := false
if ok && existing != nil {
wasHealthy = existing.isHealthyAt(now)
}
entry.Healthy = entry.isHealthyAt(now)
r.entries[entry.ID] = &entry
return UpdateResult{
Entry: entry,
IsNew: !ok,
WasHealthy: wasHealthy,
BecameHealthy: !wasHealthy && entry.Healthy,
}
}
func (r *Registry) UpdateHeartbeat(id string, status string, ts time.Time, now time.Time) (UpdateResult, bool) {
id = strings.TrimSpace(id)
if id == "" {
return UpdateResult{}, false
}
if status == "" {
status = "ok"
}
if ts.IsZero() {
ts = now
}
r.mu.Lock()
defer r.mu.Unlock()
entry, ok := r.entries[id]
if !ok || entry == nil {
return UpdateResult{}, false
}
wasHealthy := entry.isHealthyAt(now)
entry.Status = status
entry.LastHeartbeat = ts
entry.Healthy = entry.isHealthyAt(now)
return UpdateResult{
Entry: *entry,
IsNew: false,
WasHealthy: wasHealthy,
BecameHealthy: !wasHealthy && entry.Healthy,
}, true
}
func (r *Registry) List(now time.Time, onlyHealthy bool) []RegistryEntry {
r.mu.Lock()
defer r.mu.Unlock()
result := make([]RegistryEntry, 0, len(r.entries))
for _, entry := range r.entries {
if entry == nil {
continue
}
entry.Healthy = entry.isHealthyAt(now)
if onlyHealthy && !entry.Healthy {
continue
}
cp := *entry
result = append(result, cp)
}
return result
}
func registryEntryFromAnnouncement(announce Announcement, now time.Time) RegistryEntry {
status := "ok"
return RegistryEntry{
ID: strings.TrimSpace(announce.ID),
Service: strings.TrimSpace(announce.Service),
Rail: strings.ToUpper(strings.TrimSpace(announce.Rail)),
Network: strings.ToUpper(strings.TrimSpace(announce.Network)),
Operations: cloneStrings(announce.Operations),
Currencies: cloneStrings(announce.Currencies),
Limits: cloneLimits(announce.Limits),
InvokeURI: strings.TrimSpace(announce.InvokeURI),
RoutingPriority: announce.RoutingPriority,
Version: strings.TrimSpace(announce.Version),
Health: normalizeHealth(announce.Health),
LastHeartbeat: now,
Status: status,
}
}
func normalizeAnnouncement(announce Announcement) Announcement {
announce.ID = strings.TrimSpace(announce.ID)
announce.Service = strings.TrimSpace(announce.Service)
announce.Rail = strings.ToUpper(strings.TrimSpace(announce.Rail))
announce.Network = strings.ToUpper(strings.TrimSpace(announce.Network))
announce.Operations = normalizeStrings(announce.Operations, false)
announce.Currencies = normalizeStrings(announce.Currencies, true)
announce.InvokeURI = strings.TrimSpace(announce.InvokeURI)
announce.Version = strings.TrimSpace(announce.Version)
announce.Health = normalizeHealth(announce.Health)
if announce.Limits != nil {
announce.Limits = normalizeLimits(*announce.Limits)
}
return announce
}
func normalizeHealth(h HealthParams) HealthParams {
if h.IntervalSec <= 0 {
h.IntervalSec = DefaultHealthIntervalSec
}
if h.TimeoutSec <= 0 {
h.TimeoutSec = DefaultHealthTimeoutSec
}
if h.TimeoutSec < h.IntervalSec {
h.TimeoutSec = h.IntervalSec * 2
}
return h
}
func normalizeLimits(l Limits) *Limits {
res := l
if len(res.VolumeLimit) == 0 {
res.VolumeLimit = nil
}
if len(res.VelocityLimit) == 0 {
res.VelocityLimit = nil
}
return &res
}
func cloneLimits(src *Limits) *Limits {
if src == nil {
return nil
}
dst := *src
if src.VolumeLimit != nil {
dst.VolumeLimit = map[string]string{}
for key, value := range src.VolumeLimit {
if strings.TrimSpace(key) == "" {
continue
}
dst.VolumeLimit[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
}
if src.VelocityLimit != nil {
dst.VelocityLimit = map[string]int{}
for key, value := range src.VelocityLimit {
if strings.TrimSpace(key) == "" {
continue
}
dst.VelocityLimit[strings.TrimSpace(key)] = value
}
}
return &dst
}
func normalizeStrings(values []string, upper bool) []string {
if len(values) == 0 {
return nil
}
seen := map[string]bool{}
result := make([]string, 0, len(values))
for _, value := range values {
clean := strings.TrimSpace(value)
if clean == "" {
continue
}
if upper {
clean = strings.ToUpper(clean)
}
if seen[clean] {
continue
}
seen[clean] = true
result = append(result, clean)
}
if len(result) == 0 {
return nil
}
return result
}
func cloneStrings(values []string) []string {
if len(values) == 0 {
return nil
}
out := make([]string, len(values))
copy(out, values)
return out
}
func (e *RegistryEntry) isHealthyAt(now time.Time) bool {
if e == nil {
return false
}
status := strings.ToLower(strings.TrimSpace(e.Status))
if status != "" && status != "ok" {
return false
}
if e.LastHeartbeat.IsZero() {
return false
}
timeout := time.Duration(e.Health.TimeoutSec) * time.Second
if timeout <= 0 {
timeout = time.Duration(DefaultHealthTimeoutSec) * time.Second
}
return now.Sub(e.LastHeartbeat) <= timeout
}

View File

@@ -0,0 +1,188 @@
package discovery
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"time"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/messaging/broker"
cons "github.com/tech/sendico/pkg/messaging/consumer"
me "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
type RegistryService struct {
logger mlogger.Logger
registry *Registry
producer msg.Producer
sender string
consumers []consumerHandler
startOnce sync.Once
stopOnce sync.Once
}
type consumerHandler struct {
consumer msg.Consumer
handler msg.MessageHandlerT
}
func NewRegistryService(logger mlogger.Logger, broker broker.Broker, producer msg.Producer, registry *Registry, sender string) (*RegistryService, error) {
if broker == nil {
return nil, errors.New("discovery registry: broker is nil")
}
if registry == nil {
registry = NewRegistry()
}
if logger != nil {
logger = logger.Named("discovery_registry")
}
sender = strings.TrimSpace(sender)
if sender == "" {
sender = "discovery"
}
serviceConsumer, err := cons.NewConsumer(logger, broker, ServiceAnnounceEvent())
if err != nil {
return nil, err
}
gatewayConsumer, err := cons.NewConsumer(logger, broker, GatewayAnnounceEvent())
if err != nil {
return nil, err
}
heartbeatConsumer, err := cons.NewConsumer(logger, broker, HeartbeatEvent())
if err != nil {
return nil, err
}
lookupConsumer, err := cons.NewConsumer(logger, broker, LookupRequestEvent())
if err != nil {
return nil, err
}
svc := &RegistryService{
logger: logger,
registry: registry,
producer: producer,
sender: sender,
consumers: []consumerHandler{
{consumer: serviceConsumer, handler: func(ctx context.Context, env me.Envelope) error {
return svc.handleAnnounce(ctx, env)
}},
{consumer: gatewayConsumer, handler: func(ctx context.Context, env me.Envelope) error {
return svc.handleAnnounce(ctx, env)
}},
{consumer: heartbeatConsumer, handler: svc.handleHeartbeat},
{consumer: lookupConsumer, handler: svc.handleLookup},
},
}
return svc, nil
}
func (s *RegistryService) Start() {
if s == nil {
return
}
s.startOnce.Do(func() {
for _, ch := range s.consumers {
ch := ch
go func() {
if err := ch.consumer.ConsumeMessages(ch.handler); err != nil && s.logger != nil {
s.logger.Warn("Discovery consumer stopped with error", zap.Error(err))
}
}()
}
})
}
func (s *RegistryService) Stop() {
if s == nil {
return
}
s.stopOnce.Do(func() {
for _, ch := range s.consumers {
if ch.consumer != nil {
ch.consumer.Close()
}
}
})
}
func (s *RegistryService) handleAnnounce(_ context.Context, env me.Envelope) error {
var payload Announcement
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
s.logWarn("Failed to decode discovery announce payload", zap.Error(err))
return err
}
now := time.Now()
result := s.registry.UpsertFromAnnouncement(payload, now)
if result.IsNew || result.BecameHealthy {
s.publishRefresh(result.Entry)
}
return nil
}
func (s *RegistryService) handleHeartbeat(_ context.Context, env me.Envelope) error {
var payload Heartbeat
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
s.logWarn("Failed to decode discovery heartbeat payload", zap.Error(err))
return err
}
if payload.ID == "" {
return nil
}
ts := time.Unix(payload.TS, 0)
if ts.Unix() <= 0 {
ts = time.Now()
}
result, ok := s.registry.UpdateHeartbeat(payload.ID, strings.TrimSpace(payload.Status), ts, time.Now())
if ok && result.BecameHealthy {
s.publishRefresh(result.Entry)
}
return nil
}
func (s *RegistryService) handleLookup(_ context.Context, env me.Envelope) error {
if s.producer == nil {
s.logWarn("Discovery lookup request ignored: producer not configured")
return nil
}
var payload LookupRequest
if err := json.Unmarshal(env.GetData(), &payload); err != nil {
s.logWarn("Failed to decode discovery lookup payload", zap.Error(err))
return err
}
resp := s.registry.Lookup(time.Now())
resp.RequestID = strings.TrimSpace(payload.RequestID)
if err := s.producer.SendMessage(NewLookupResponseEnvelope(s.sender, resp)); err != nil {
s.logWarn("Failed to publish discovery lookup response", zap.Error(err))
return err
}
return nil
}
func (s *RegistryService) publishRefresh(entry RegistryEntry) {
if s == nil || s.producer == nil {
return
}
payload := RefreshEvent{
Service: entry.Service,
Rail: entry.Rail,
Network: entry.Network,
Message: "new module available",
}
if err := s.producer.SendMessage(NewRefreshUIEnvelope(s.sender, payload)); err != nil {
s.logWarn("Failed to publish discovery refresh event", zap.Error(err))
}
}
func (s *RegistryService) logWarn(message string, fields ...zap.Field) {
if s.logger == nil {
return
}
s.logger.Warn(message, fields...)
}

View File

@@ -0,0 +1,10 @@
package discovery
const (
SubjectServiceAnnounce = "discovery.service.announce"
SubjectGatewayAnnounce = "discovery.gateway.announce"
SubjectHeartbeat = "discovery.service.heartbeat"
SubjectLookupRequest = "discovery.request.lookup"
SubjectLookupResponse = "discovery.response.lookup"
SubjectRefreshUI = "discovery.event.refresh_ui"
)

View File

@@ -0,0 +1,40 @@
package discovery
type HealthParams struct {
IntervalSec int `json:"intervalSec"`
TimeoutSec int `json:"timeoutSec"`
}
type Limits struct {
MinAmount string `json:"minAmount,omitempty"`
MaxAmount string `json:"maxAmount,omitempty"`
VolumeLimit map[string]string `json:"volumeLimit,omitempty"`
VelocityLimit map[string]int `json:"velocityLimit,omitempty"`
}
type Announcement struct {
ID string `json:"id"`
Service string `json:"service"`
Rail string `json:"rail,omitempty"`
Network string `json:"network,omitempty"`
Operations []string `json:"operations,omitempty"`
Currencies []string `json:"currencies,omitempty"`
Limits *Limits `json:"limits,omitempty"`
InvokeURI string `json:"invokeURI,omitempty"`
RoutingPriority int `json:"routingPriority,omitempty"`
Version string `json:"version,omitempty"`
Health HealthParams `json:"health,omitempty"`
}
type Heartbeat struct {
ID string `json:"id"`
Status string `json:"status"`
TS int64 `json:"ts"`
}
type RefreshEvent struct {
Service string `json:"service,omitempty"`
Rail string `json:"rail,omitempty"`
Network string `json:"network,omitempty"`
Message string `json:"message,omitempty"`
}