package realtime import ( "context" "encoding/json" "fmt" "strings" "sync" "github.com/redis/go-redis/v9" ) type RedisOptions struct { Addr string Password string DB int Prefix string } type RedisBus struct { client *redis.Client prefix string } type redisSubscription struct { pubsub *redis.PubSub ch chan Event closeOnce sync.Once chOnce sync.Once } func NewRedisBus(ctx context.Context, opts RedisOptions) (*RedisBus, error) { opts.Addr = strings.TrimSpace(opts.Addr) if opts.Addr == "" { opts.Addr = "localhost:6379" } prefix := strings.TrimSpace(opts.Prefix) if prefix == "" { prefix = "canvas:realtime" } client := redis.NewClient(&redis.Options{ Addr: opts.Addr, Password: opts.Password, DB: opts.DB, }) if err := client.Ping(ctx).Err(); err != nil { _ = client.Close() return nil, err } return &RedisBus{client: client, prefix: prefix}, nil } func (b *RedisBus) Publish(ctx context.Context, event Event) error { if b == nil || b.client == nil { return nil } data, err := json.Marshal(event) if err != nil { return err } return b.client.Publish(ctx, b.channel(event.ProjectID), data).Err() } func (b *RedisBus) Subscribe(ctx context.Context, projectID string) (Subscription, error) { if b == nil || b.client == nil { return nil, nil } pubsub := b.client.Subscribe(ctx, b.channel(projectID)) if _, err := pubsub.Receive(ctx); err != nil { _ = pubsub.Close() return nil, err } sub := &redisSubscription{ pubsub: pubsub, ch: make(chan Event, 32), } go sub.run(ctx) return sub, nil } func (b *RedisBus) Close() error { if b == nil || b.client == nil { return nil } return b.client.Close() } func (b *RedisBus) channel(projectID string) string { return fmt.Sprintf("%s:project:%s", b.prefix, strings.TrimSpace(projectID)) } func (s *redisSubscription) C() <-chan Event { return s.ch } func (s *redisSubscription) Close() error { var err error s.closeOnce.Do(func() { err = s.pubsub.Close() }) return err } func (s *redisSubscription) run(ctx context.Context) { defer s.closeChannel() defer s.Close() for { msg, err := s.pubsub.ReceiveMessage(ctx) if err != nil { return } var event Event if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil { continue } select { case s.ch <- event: case <-ctx.Done(): return } } } func (s *redisSubscription) closeChannel() { s.chOnce.Do(func() { close(s.ch) }) }