The pending-write queue: coalescing, and Hue's per-type rate limits.
This is the reason writes go through Hue.Bridge's process while reads
bypass it. Nothing about a read needs serialising. Coalescing and pacing are
both statements about all writes together, and there is nowhere else in the
design to make them.
Coalescing merges, it does not replace
Twenty slider drags on one light collapse to one request carrying the last
value. Two writes setting different things — on: true, then
brightness: 40 — collapse to one request carrying both. Deep-merging the
new body over the pending one gives both behaviours from a single rule:
last-wins per leaf, union across keys.
Pacing is per type
Roughly 10 writes/second for lights and 1/second for grouped_lights, per Hue's own guidance. One request leaves per interval per type, not per pending item: three lights queued at once go out over 300 ms, and a queued grouped_light does not delay them.
A scene recall is paced at the grouped_light rate rather than the light
rate, even though it is one HTTP request like a light write. Hue does not
document a scene-recall limit, so this is inferred rather than cited — see
the comment on @scene_interval.
No clock
Every function that cares about time takes now as a monotonic millisecond
argument. The queue holds no timers and reads no clock, so its behaviour
under a 1-second grouped_light interval is tested by passing 1_000, not by
sleeping for a second.
Summary
Functions
Milliseconds until a write of type may be sent, 0 if now, or :never if
nothing of that type is pending.
Queues a write, merging it into whatever is already pending for that target.
An empty queue.
Every type with something pending.
Takes the oldest pending write of type and records the send at now.
Types
Functions
@spec due_in(t(), atom(), integer()) :: non_neg_integer() | :never
Milliseconds until a write of type may be sent, 0 if now, or :never if
nothing of that type is pending.
@spec enqueue(t(), key(), map()) :: {t(), non_neg_integer()}
Queues a write, merging it into whatever is already pending for that target.
Returns the queue and the number of writes that have now been absorbed into
this one pending body — 0 the first time, 1 the second, and so on. The
server reports that as [:hue, :write, :coalesced].
@spec new() :: t()
An empty queue.
Every type with something pending.
Takes the oldest pending write of type and records the send at now.
Returns :empty when nothing of that type is pending. Does not check whether
the write is due — that is due_in/3's question, and the server asks it
before calling this.