package subscriber import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strconv" "strings" "sync" "time" "aiaa-notification-service/internal/cache" ) type Deduper interface { Claim(ctx context.Context, hash string) (bool, error) Release(ctx context.Context, hash string) error } func MessageHash(body []byte) string { sum := sha256.Sum256(body) return hex.EncodeToString(sum[:]) } func SignalHash(source string, data map[string]interface{}) string { sum := sha256.Sum256([]byte(signalKey(source, data))) return hex.EncodeToString(sum[:]) } func signalKey(source string, data map[string]interface{}) string { if data == nil { data = map[string]interface{}{} } strategy := fieldString(data["strategyCode"]) symbol := firstNonEmptyField(fieldString(data["symbol"]), fieldString(data["currency"])) period := fieldString(data["period"]) direction := strings.ToUpper(firstNonEmptyField(fieldString(data["direction"]), fieldString(data["side"]))) price := fieldString(data["price"]) return strings.Join([]string{strings.TrimSpace(source), strategy, symbol, period, direction, price}, "\x1f") } func firstNonEmptyField(a, b string) string { if a != "" { return a } return b } func fieldString(v any) string { if v == nil { return "" } switch n := v.(type) { case string: return strings.TrimSpace(n) case float64: return strconv.FormatFloat(n, 'f', -1, 64) case float32: return strconv.FormatFloat(float64(n), 'f', -1, 64) case int: return strconv.Itoa(n) case int64: return strconv.FormatInt(n, 10) case json.Number: return n.String() default: return strings.TrimSpace(fmt.Sprint(v)) } } type MemoryDeduper struct { mu sync.Mutex seen map[string]struct{} } func NewMemoryDeduper() *MemoryDeduper { return &MemoryDeduper{seen: make(map[string]struct{})} } func (d *MemoryDeduper) Claim(_ context.Context, hash string) (bool, error) { d.mu.Lock() defer d.mu.Unlock() if _, ok := d.seen[hash]; ok { return false, nil } d.seen[hash] = struct{}{} return true, nil } func (d *MemoryDeduper) Release(_ context.Context, hash string) error { d.mu.Lock() defer d.mu.Unlock() delete(d.seen, hash) return nil } type cacheDeduper struct { c *cache.Cache ttl time.Duration } func NewCacheDeduper(c *cache.Cache, ttl time.Duration) Deduper { if c == nil { return NewMemoryDeduper() } if ttl <= 0 { ttl = time.Hour } return &cacheDeduper{c: c, ttl: ttl} } func (d *cacheDeduper) Claim(ctx context.Context, hash string) (bool, error) { return d.c.ClaimDedup(ctx, hash, d.ttl) } func (d *cacheDeduper) Release(ctx context.Context, hash string) error { return d.c.ReleaseDedup(ctx, hash) }