const crypto = require('crypto'); function normalizeClientName(value) { return String(value || '').trim(); } function normalizeDeviceId(value) { return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128); } function collectLiveConnections(liveConnections) { return Array.isArray(liveConnections) ? liveConnections : []; } async function isClientNameAvailable(pool, clientName, excludeDeviceId, liveConnections) { const normalizedName = normalizeClientName(clientName); if (!normalizedName) { return false; } const normalizedDeviceId = normalizeDeviceId(excludeDeviceId); const live = collectLiveConnections(liveConnections); const lowerName = normalizedName.toLowerCase(); try { if (pool) { const [deviceRows] = await pool.query( `SELECT device_id FROM player_onboarding_devices WHERE client_name IS NOT NULL AND TRIM(client_name) <> '' AND LOWER(TRIM(client_name)) = LOWER(TRIM(?)) AND device_id <> ? LIMIT 1`, [normalizedName, normalizedDeviceId] ); if (deviceRows.length) { return false; } } for (const connection of live) { const existingName = normalizeClientName(connection && connection.clientName ? connection.clientName : ''); if (!existingName || existingName.toLowerCase() !== lowerName) { continue; } const existingDeviceId = normalizeDeviceId(connection && (connection.deviceId || connection.clientId) ? (connection.deviceId || connection.clientId) : ''); if (normalizedDeviceId && existingDeviceId && existingDeviceId === normalizedDeviceId) { continue; } return false; } return true; } catch (_error) { for (const connection of live) { const existingName = normalizeClientName(connection && connection.clientName ? connection.clientName : ''); if (!existingName || existingName.toLowerCase() !== lowerName) { continue; } const existingDeviceId = normalizeDeviceId(connection && (connection.deviceId || connection.clientId) ? (connection.deviceId || connection.clientId) : ''); if (normalizedDeviceId && existingDeviceId && existingDeviceId === normalizedDeviceId) { continue; } return false; } return true; } } function buildClientNameLockName(clientName) { return `ps_client_name_${crypto.createHash('sha1').update(String(clientName || '').trim().toLowerCase()).digest('hex')}`; } async function withClientNameReservation(pool, clientName, handler) { if (!pool || typeof pool.getConnection !== 'function') { return handler(); } const normalizedName = normalizeClientName(clientName); if (!normalizedName) { return handler(); } const connection = await pool.getConnection(); const lockName = buildClientNameLockName(normalizedName); let lockAcquired = false; try { const [lockRows] = await connection.query('SELECT GET_LOCK(?, 5) AS lock_result', [lockName]); const lockResult = lockRows && lockRows[0] ? Number(lockRows[0].lock_result) : 0; if (lockResult !== 1) { const error = new Error('Client name is busy. Please try again.'); error.statusCode = 409; throw error; } lockAcquired = true; return await handler(); } finally { if (lockAcquired) { try { await connection.query('SELECT RELEASE_LOCK(?)', [lockName]); } catch (_error) {} } connection.release(); } } module.exports = { normalizeClientName: normalizeClientName, normalizeDeviceId: normalizeDeviceId, collectLiveConnections: collectLiveConnections, isClientNameAvailable: isClientNameAvailable, withClientNameReservation: withClientNameReservation };