// Package dedup provides idempotent event acceptance via Redis SETNX. // // Key shape: dedup:{workspace_id}:{message_id} // TTL: 24h by default (configurable) // // CheckAndSet returns true when the message_id is new (first time seen). // If it returns false the caller MUST drop the event silently and return 200. package dedup import ( "context" "fmt" "time" "github.com/redis/rueidis" ) type Dedup interface { CheckAndSet(ctx context.Context, workspaceID, messageID string) (bool, error) } type redisDedup struct { client rueidis.Client ttl time.Duration } func New(client rueidis.Client, ttl time.Duration) Dedup { return &redisDedup{client: client, ttl: ttl} } func key(workspaceID, messageID string) string { return fmt.Sprintf("dedup:%s:%s", workspaceID, messageID) } func (d *redisDedup) CheckAndSet(ctx context.Context, workspaceID, messageID string) (bool, error) { k := key(workspaceID, messageID) cmd := d.client.B().Set().Key(k).Value("1"). Nx(). Ex(d.ttl). Build() resp := d.client.Do(ctx, cmd) if err := resp.Error(); err != nil { return false, fmt.Errorf("dedup setnx: %w", err) } // SET with NX returns "OK" when set, nil reply when key already exists. if resp.IsNil() { return false, nil } return true, nil }