A node that fires twice in under one second leaves clients showing stale state (stuck at NONE) after the second fire. The UI lights up for the first fire but silently ignores the second.
Two independent 1-second debounce layers in the server’s event pipeline both suppressed the second fire:
ServerNodeManager.update() timestamp guard — the condition
node.timestamp - origin.timestamp > 1000 prevented posting STATE_CHANGE
events when consecutive writes happened within 1 second of each other. Since
EventClient (shared/) connects to /events and updates ClientNodeManager
exclusively via STATE_CHANGE events, rapid transitions were silently dropped.
EventTracker.dbounce() payload-blind comparison — even if the guard in
update() was absent, EventTracker.dbounce() checked only event.type (not
the payload’s target state) within a 1-second window. Two STATE_CHANGE
events for the same node with different states (e.g. EXECUTED → WARN) were
incorrectly treated as duplicates and the second was suppressed. Critically,
processedEvents was never cleared on NONE settle, so a NONE → EXECUTED →
NONE → EXECUTED sequence left the second EXECUTED matching the first in the
dedup map.
node.timestamp - origin.timestamp > 1000 guard from
ServerNodeManager.update(). The condition is now
node.state != NodeState.NONE && origin.state != node.state, which fires
for every genuine non-NONE transition.EventTracker.clear(node.id) in update() when node.state == NodeState.NONE,
resetting the dedup window whenever a node settles to baseline so the next fire
is never mistaken for a duplicate of the preceding one.EventTracker.dbounce() to include event.payload == e.payload in the
comparison, so transitions to a different state (e.g. EXECUTED → WARN) are
never collapsed even without a NONE settle.EventTracker.clear(id) public method.ServerNodeManagerStateChangeTest with three cases: rapid second fire
with NONE settle (regression), true duplicate without NONE settle (dedup still
works), and WARN → SEVERE without NONE settle (different-state transition fires).> N ms timestamp guard that gates event emission on the server is a
potential silent-drop. Debouncing belongs in the event dedup layer
(EventTracker), not in the write path (update()).EventTracker.dbounce() must compare the full event identity (type + payload),
not just the type, to distinguish rapid transitions to different states from
true duplicates.