Flow
One press, from page to receipt
1Check the device
If it isn't ready, wake it and wait until it is.
2Send once
The press goes out one time with one ID.
3Server claims the ID
The first request runs; a copy gets the saved result back.
4Save the result
Running, accepted, rejected, or unknown. Unknown is never retried.
5Lost answer
The next try at that press asks for the saved result first, never resends.
Built into the remote on purpose was a rule: if a key press didn't come back clean, wake the TV box and press the key again.
The box sleeps a lot, so it seemed smart. The movie launcher did the same with the media app: if it wasn't up after a wake, launch it again. Earlier that day I'd made the launch wait longer and check more carefully. It still kept the retry.
A couple of hours later I took the retries out. A press that doesn't come back clean means it's unknown what happened. The box may never have got it. Or it got it, acted, and the answer was lost. If you press "play" or a power toggle again in that second case, you just pressed it twice.
What the page does before it sends
If you control a device from a web page, flip the order. Check whether the device is ready. If it isn't, wake it and wait until it is. Sending happens once.
The command goes out once, and a result that isn't clearly good is reported as possibly sent instead of being sent again.
If that send comes back unclear, say so and stop. Don't loop.
The server's receipt store
The page isn't the only thing that can send twice. A double tap or a reload can do it too. So I put a guard on the server for every route that touches hardware.
The browser gives each press one ID. The server claims that ID in the database before the route runs. The first request runs. Any copy gets the saved result back instead. If the same ID shows up with a different command, it's refused.
Each saved result is running, accepted, rejected, or unknown. Treat unknown as a wall. A server error is unknown. A process that died mid-command is unknown. If the command ran but the result couldn't be saved, that's unknown too, not "try again". If the claim fails before anything was sent, say clearly that nothing was sent.
When the page loses an answer, it doesn't resend. The next try at that same press asks for the saved result by ID first, and that lookup must never run the command. Keep the IDs you're still waiting on in session storage so a reload doesn't forget them.
Routines and status
For chains like wake, launch, play, check the device after each step. If you can't confirm a step, stop the chain. Don't let the same scheduled minute fire twice.
Also show three states, not two: online, offline, or unknown. A slow read should show as delayed, not offline.
Duplicates, failed saves, failed claims
The tests check duplicates, failed saves and failed claims. An unknown on a remote is annoying. It still beats pressing power twice.