package driver import ( "context" "fmt" "slices" "time" "onehouse/protocol" ) const commandQueueCapacity = 64 // Router owns one bounded FIFO for each supplied command-capable device. A // queue entry is one single command or one ordered batch. Handle is safe for // concurrent calls. Each queue has one consumer that serializes device work. // The caller stops producers and consumers through their contexts. Queues do // not close and belong to the router for its lifetime. type Router struct { queues map[string]chan []Command subscriptions []protocol.Subscription } // NewRouter validates unique device IDs and makes 64-entry queues. Read-only // devices do not belong in deviceIDs. The constructor performs no I/O. func NewRouter(deviceIDs []string) (*Router, error) { ids := slices.Clone(deviceIDs) slices.Sort(ids) router := &Router{queues: make(map[string]chan []Command, len(deviceIDs))} for _, deviceID := range ids { _, exists := router.queues[deviceID] if exists { return nil, fmt.Errorf("duplicate command device ID") } subscription, err := protocol.CommandSubscription(deviceID) if err != nil { return nil, fmt.Errorf("build command subscription: %w", err) } router.queues[deviceID] = make(chan []Command, commandQueueCapacity) router.subscriptions = append(router.subscriptions, subscription) } return router, nil } // Subscriptions returns an independent, sorted list including batch intake. func (r *Router) Subscriptions() []protocol.Subscription { return slices.Clone(r.subscriptions) } // Queue returns the receive-only command queue for a configured device. func (r *Router) Queue(deviceID string) (<-chan []Command, bool) { queue, exists := r.queues[deviceID] return queue, exists } // Handle parses one delivery once and queues all its commands together. It // rejects malformed, retained, expired, non-command, and unowned deliveries. A // full queue waits until space, caller cancellation, or the earliest member // expiry. A driver must check expiry again when it consumes the queue entry. func (r *Router) Handle(ctx context.Context, topic string, data []byte, retained bool) error { err := ctx.Err() if err != nil { return err } message, err := protocol.Parse(topic, data, retained) if err != nil { return fmt.Errorf("parse command delivery: %w", err) } var commands []Command switch payload := message.Payload.(type) { case protocol.ValuePayload: if message.Address.Kind != protocol.Command { return fmt.Errorf("delivery is not a command") } commands = []Command{{ DeviceID: message.Address.ID, Capability: message.Address.Target, Metadata: message.Metadata, Payload: payload, }} case protocol.BatchPayload: for _, member := range payload.Commands { commands = append(commands, Command{ DeviceID: message.Address.ID, Capability: member.Capability, Metadata: member.Metadata, Payload: member.Payload, }) } default: return fmt.Errorf("delivery is not a command") } queue, exists := r.queues[message.Address.ID] if !exists { return fmt.Errorf("command device has no queue") } expiresAt := *commands[0].Metadata.ExpiresAt for _, command := range commands { if command.Expired(time.Now()) { return fmt.Errorf("command expired") } if command.Metadata.ExpiresAt.Before(expiresAt) { expiresAt = *command.Metadata.ExpiresAt } } ctx, cancel := context.WithDeadline(ctx, expiresAt) defer cancel() err = ctx.Err() if err != nil { return err } select { case queue <- commands: return nil case <-ctx.Done(): return ctx.Err() } }