feat: notification service

This commit is contained in:
Ilia Denisov
2026-04-22 08:49:45 +02:00
committed by GitHub
parent 5b7593e6f6
commit 32dc29359a
135 changed files with 21828 additions and 130 deletions
@@ -0,0 +1,105 @@
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
}