mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 09:12:57 +02:00
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:
+20
-2
@@ -761,8 +761,12 @@ function setupEventListeners() {
|
||||
document.getElementById('createChannelForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
if (submitBtn && submitBtn.disabled) return; // in-flight guard
|
||||
|
||||
const name = document.getElementById('newChannelName').value.trim();
|
||||
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/api/channels', {
|
||||
method: 'POST',
|
||||
@@ -775,7 +779,10 @@ function setupEventListeners() {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`Channel "${name}" created!`, 'success');
|
||||
const msg = data.already_existed
|
||||
? `Channel "${name}" already exists.`
|
||||
: `Channel "${name}" created!`;
|
||||
showNotification(msg, data.already_existed ? 'info' : 'success');
|
||||
|
||||
// Show warning if returned (e.g., exceeding soft limit of 7 channels)
|
||||
if (data.warning) {
|
||||
@@ -795,6 +802,8 @@ function setupEventListeners() {
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Failed to create channel', 'danger');
|
||||
} finally {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -802,6 +811,9 @@ function setupEventListeners() {
|
||||
document.getElementById('joinChannelFormSubmit').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
if (submitBtn && submitBtn.disabled) return; // in-flight guard
|
||||
|
||||
const name = document.getElementById('joinChannelName').value.trim();
|
||||
const key = document.getElementById('joinChannelKey').value.trim().toLowerCase();
|
||||
|
||||
@@ -817,6 +829,7 @@ function setupEventListeners() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
try {
|
||||
const payload = { name: name };
|
||||
if (key) {
|
||||
@@ -834,7 +847,10 @@ function setupEventListeners() {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`Joined channel "${name}"!`, 'success');
|
||||
const msg = data.already_existed
|
||||
? `Already joined channel "${name}".`
|
||||
: `Joined channel "${name}"!`;
|
||||
showNotification(msg, data.already_existed ? 'info' : 'success');
|
||||
|
||||
// Show warning if returned (e.g., exceeding soft limit of 7 channels)
|
||||
if (data.warning) {
|
||||
@@ -855,6 +871,8 @@ function setupEventListeners() {
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Failed to join channel', 'danger');
|
||||
} finally {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user