12 const commandQueueCapacity = 64
14 // Router owns one bounded FIFO for each supplied command-capable device. A
15 // queue entry is one single command or one ordered batch. Handle is safe for
16 // concurrent calls. Each queue has one consumer that serializes device work.
17 // The caller stops producers and consumers through their contexts. Queues do
18 // not close and belong to the router for its lifetime.
20 queues map[string]chan []Command
21 subscriptions []protocol.Subscription
24 // NewRouter validates unique device IDs and makes 64-entry queues. Read-only
25 // devices do not belong in deviceIDs. The constructor performs no I/O.
26 func NewRouter(deviceIDs []string) (*Router, error) {
27 ids := slices.Clone(deviceIDs)
30 router := &Router{queues: make(map[string]chan []Command, len(deviceIDs))}
32 for _, deviceID := range ids {
33 _, exists := router.queues[deviceID]
35 return nil, fmt.Errorf("duplicate command device ID")
38 subscription, err := protocol.CommandSubscription(deviceID)
40 return nil, fmt.Errorf("build command subscription: %w", err)
43 router.queues[deviceID] = make(chan []Command, commandQueueCapacity)
44 router.subscriptions = append(router.subscriptions, subscription)
50 // Subscriptions returns an independent, sorted list including batch intake.
51 func (r *Router) Subscriptions() []protocol.Subscription {
52 return slices.Clone(r.subscriptions)
55 // Queue returns the receive-only command queue for a configured device.
56 func (r *Router) Queue(deviceID string) (<-chan []Command, bool) {
57 queue, exists := r.queues[deviceID]
62 // Handle parses one delivery once and queues all its commands together. It
63 // rejects malformed, retained, expired, non-command, and unowned deliveries. A
64 // full queue waits until space, caller cancellation, or the earliest member
65 // expiry. A driver must check expiry again when it consumes the queue entry.
66 func (r *Router) Handle(ctx context.Context, topic string, data []byte, retained bool) error {
72 message, err := protocol.Parse(topic, data, retained)
74 return fmt.Errorf("parse command delivery: %w", err)
77 var commands []Command
79 switch payload := message.Payload.(type) {
80 case protocol.ValuePayload:
81 if message.Address.Kind != protocol.Command {
82 return fmt.Errorf("delivery is not a command")
85 commands = []Command{{
86 DeviceID: message.Address.ID,
87 Capability: message.Address.Target,
88 Metadata: message.Metadata,
91 case protocol.BatchPayload:
92 for _, member := range payload.Commands {
93 commands = append(commands, Command{
94 DeviceID: message.Address.ID,
95 Capability: member.Capability,
96 Metadata: member.Metadata,
97 Payload: member.Payload,
101 return fmt.Errorf("delivery is not a command")
104 queue, exists := r.queues[message.Address.ID]
106 return fmt.Errorf("command device has no queue")
109 expiresAt := *commands[0].Metadata.ExpiresAt
111 for _, command := range commands {
112 if command.Expired(time.Now()) {
113 return fmt.Errorf("command expired")
116 if command.Metadata.ExpiresAt.Before(expiresAt) {
117 expiresAt = *command.Metadata.ExpiresAt
121 ctx, cancel := context.WithDeadline(ctx, expiresAt)
130 case queue <- commands: