package redisstate import ( "encoding/base64" "fmt" "strings" "galaxy/notification/internal/api/intentstream" ) const defaultPrefix = "notification:" // Keyspace builds the frozen Notification Service Redis keys. All dynamic key // segments are encoded with base64url so raw key structure does not depend on // caller-provided characters. type Keyspace struct{} // Notification returns the primary Redis key for one notification_record. func (Keyspace) Notification(notificationID string) string { return defaultPrefix + "records:" + encodeKeyComponent(notificationID) } // Route returns the primary Redis key for one notification_route. func (Keyspace) Route(notificationID string, routeID string) string { return defaultPrefix + "routes:" + encodeKeyComponent(notificationID) + ":" + encodeKeyComponent(routeID) } // ParseRoute returns the notification identifier and route identifier encoded // inside routeKey. func (Keyspace) ParseRoute(routeKey string) (string, string, error) { trimmedPrefix := defaultPrefix + "routes:" if !strings.HasPrefix(routeKey, trimmedPrefix) { return "", "", fmt.Errorf("parse route key: %q does not use %q prefix", routeKey, trimmedPrefix) } encoded := strings.TrimPrefix(routeKey, trimmedPrefix) parts := strings.Split(encoded, ":") if len(parts) != 2 { return "", "", fmt.Errorf("parse route key: %q must contain exactly two encoded segments", routeKey) } notificationID, err := decodeKeyComponent(parts[0]) if err != nil { return "", "", fmt.Errorf("parse route key: notification id: %w", err) } routeID, err := decodeKeyComponent(parts[1]) if err != nil { return "", "", fmt.Errorf("parse route key: route id: %w", err) } return notificationID, routeID, nil } // Idempotency returns the primary Redis key for one // notification_idempotency_record. func (Keyspace) Idempotency(producer intentstream.Producer, idempotencyKey string) string { return defaultPrefix + "idempotency:" + encodeKeyComponent(string(producer)) + ":" + encodeKeyComponent(idempotencyKey) } // DeadLetter returns the primary Redis key for one // notification_dead_letter_entry. func (Keyspace) DeadLetter(notificationID string, routeID string) string { return defaultPrefix + "dead_letters:" + encodeKeyComponent(notificationID) + ":" + encodeKeyComponent(routeID) } // RouteLease returns the temporary Redis key used to coordinate exclusive // publication of one notification_route across replicas. func (Keyspace) RouteLease(notificationID string, routeID string) string { return defaultPrefix + "route_leases:" + encodeKeyComponent(notificationID) + ":" + encodeKeyComponent(routeID) } // MalformedIntent returns the primary Redis key for one malformed-intent // record. func (Keyspace) MalformedIntent(streamEntryID string) string { return defaultPrefix + "malformed_intents:" + encodeKeyComponent(streamEntryID) } // StreamOffset returns the primary Redis key for one persisted intent-consumer // offset. func (Keyspace) StreamOffset(stream string) string { return defaultPrefix + "stream_offsets:" + encodeKeyComponent(stream) } // Intents returns the frozen ingress Redis Stream key. func (Keyspace) Intents() string { return defaultPrefix + "intents" } // RouteSchedule returns the frozen route schedule sorted-set key. func (Keyspace) RouteSchedule() string { return defaultPrefix + "route_schedule" } func encodeKeyComponent(value string) string { return base64.RawURLEncoding.EncodeToString([]byte(value)) } func decodeKeyComponent(value string) (string, error) { decoded, err := base64.RawURLEncoding.DecodeString(value) if err != nil { return "", err } return string(decoded), nil }