fix: prevent duplicate channels from concurrent add/join requests

Two near-simultaneous POSTs to /api/channels/join (observed 7 ms apart
in demo-server logs) each found a different free slot and both
succeeded, producing two entries for the same channel name on the
device. This also shifted the sidebar so each channel rendered the
next one's messages.

- Wrap free-slot detection + set_channel in a module-level lock so
  concurrent requests serialize instead of racing.
- Idempotency: if a channel with this name already exists, return the
  existing slot with already_existed=true instead of creating a
  duplicate. Applies to both POST /api/channels and /api/channels/join
  (skipped when caller targets an explicit index).
- Disable submit buttons on create/join forms while a request is in
  flight, and guard against double-registration of the channel-link
  click delegate to stop a single click from firing N POSTs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-04-22 20:45:39 +02:00
parent 66c378c17d
commit cbcdbdcae9
3 changed files with 155 additions and 77 deletions
+8
View File
@@ -360,10 +360,18 @@ async function joinAndSwitchToChannel(channelName) {
/**
* Initialize channel link click handlers using event delegation
*/
let _channelLinkHandlersInitialized = false;
function initializeChannelLinkHandlers() {
// Guard against double registration - otherwise one click fires N handlers
// and sends N duplicate POSTs to /api/channels/join.
if (_channelLinkHandlersInitialized) return;
_channelLinkHandlersInitialized = true;
document.addEventListener('click', function(e) {
if (e.target.classList.contains('channel-link')) {
e.preventDefault();
// Swallow clicks while this link is already handling a request.
if (e.target.classList.contains('loading')) return;
const channelName = e.target.getAttribute('data-channel-name');
if (channelName) {