Bump version to 1.4.1 and tighten client handling
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function isTransientDbError(error) {
|
||||
const code = String(error && error.code ? error.code : '').trim();
|
||||
return [
|
||||
'ECONNREFUSED',
|
||||
'ECONNRESET',
|
||||
'ETIMEDOUT',
|
||||
'EPIPE',
|
||||
'ENOTFOUND',
|
||||
'PROTOCOL_CONNECTION_LOST',
|
||||
'POOL_CLOSED',
|
||||
'ERR_POOL_CLOSED'
|
||||
].indexOf(code) !== -1;
|
||||
}
|
||||
|
||||
function createOnboardingStore(filePath) {
|
||||
const normalizedFilePath = String(filePath || '').trim();
|
||||
|
||||
async function readEntries() {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(normalizedFilePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeEntries(entries) {
|
||||
await fs.promises.mkdir(path.dirname(normalizedFilePath), { recursive: true });
|
||||
const tempPath = `${normalizedFilePath}.tmp`;
|
||||
await fs.promises.writeFile(tempPath, JSON.stringify(Array.isArray(entries) ? entries : [], null, 2), 'utf8');
|
||||
await fs.promises.rename(tempPath, normalizedFilePath);
|
||||
}
|
||||
|
||||
async function enqueueBinding(entry) {
|
||||
const normalizedEntry = {
|
||||
deviceId: String(entry && entry.deviceId ? entry.deviceId : '').trim(),
|
||||
clientName: String(entry && entry.clientName ? entry.clientName : '').trim(),
|
||||
screenSlug: String(entry && entry.screenSlug ? entry.screenSlug : '').trim(),
|
||||
queuedAt: String(entry && entry.queuedAt ? entry.queuedAt : new Date().toISOString())
|
||||
};
|
||||
|
||||
if (!normalizedEntry.deviceId || !normalizedEntry.clientName || !normalizedEntry.screenSlug) {
|
||||
return readEntries();
|
||||
}
|
||||
|
||||
const entries = await readEntries();
|
||||
const nextEntries = entries.filter(function (queuedEntry) {
|
||||
return String(queuedEntry && queuedEntry.deviceId ? queuedEntry.deviceId : '').trim() !== normalizedEntry.deviceId;
|
||||
});
|
||||
nextEntries.push(normalizedEntry);
|
||||
await writeEntries(nextEntries);
|
||||
return nextEntries;
|
||||
}
|
||||
|
||||
async function flushBindings(applyBinding) {
|
||||
const entries = await readEntries();
|
||||
if (!entries.length) {
|
||||
return { flushed: 0, remaining: 0 };
|
||||
}
|
||||
|
||||
const remaining = [];
|
||||
let flushed = 0;
|
||||
for (let index = 0; index < entries.length; index += 1) {
|
||||
const entry = entries[index];
|
||||
try {
|
||||
await applyBinding(entry);
|
||||
flushed += 1;
|
||||
} catch (error) {
|
||||
if (isTransientDbError(error)) {
|
||||
remaining.push.apply(remaining, entries.slice(index));
|
||||
break;
|
||||
}
|
||||
remaining.push.apply(remaining, entries.slice(index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
await writeEntries(remaining);
|
||||
return { flushed: flushed, remaining: remaining.length };
|
||||
}
|
||||
|
||||
return {
|
||||
enqueueBinding: enqueueBinding,
|
||||
flushBindings: flushBindings,
|
||||
readEntries: readEntries
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createOnboardingStore: createOnboardingStore,
|
||||
isTransientDbError: isTransientDbError
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
const { isClientNameAvailable } = require('../client-name-check');
|
||||
const { isTransientDbError } = require('./onboarding-store');
|
||||
function normalizeDeviceId(value) {
|
||||
return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128);
|
||||
}
|
||||
@@ -31,7 +33,7 @@ async function getOnboardingStatus(pool, deviceId) {
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen) {
|
||||
async function commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, liveConnections) {
|
||||
const normalizedDeviceId = normalizeDeviceId(deviceId);
|
||||
const normalizedClientName = String(clientName || '').trim();
|
||||
const normalizedScreenSlug = String(screenSlug || '').trim();
|
||||
@@ -52,8 +54,8 @@ async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isName
|
||||
}
|
||||
const screen = screenRows[0];
|
||||
|
||||
const available = typeof isNameAvailableOnScreen === 'function'
|
||||
? await isNameAvailableOnScreen(pool, normalizedClientName, normalizedDeviceId)
|
||||
const available = typeof isClientNameAvailableOnScreen === 'function'
|
||||
? await isClientNameAvailable(pool, normalizedClientName, normalizedDeviceId, liveConnections)
|
||||
: true;
|
||||
if (!available) {
|
||||
const error = new Error('Client name already exists.');
|
||||
@@ -69,11 +71,42 @@ async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isName
|
||||
return getOnboardingStatus(pool, normalizedDeviceId);
|
||||
}
|
||||
|
||||
async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, playerRuntime, onboardingStore) {
|
||||
const liveConnections = playerRuntime && typeof playerRuntime.snapshotAllConnections === 'function'
|
||||
? playerRuntime.snapshotAllConnections()
|
||||
: [];
|
||||
|
||||
try {
|
||||
return await commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, liveConnections);
|
||||
} catch (error) {
|
||||
if (!isTransientDbError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (onboardingStore && typeof onboardingStore.enqueueBinding === 'function') {
|
||||
await onboardingStore.enqueueBinding({
|
||||
deviceId: deviceId,
|
||||
clientName: clientName,
|
||||
screenSlug: screenSlug,
|
||||
queuedAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
device_id: normalizeDeviceId(deviceId),
|
||||
client_name: String(clientName || '').trim(),
|
||||
screen_slug: String(screenSlug || '').trim(),
|
||||
queued: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function registerPlayerOnboardingRoutes(app, options) {
|
||||
const pool = options && options.pool ? options.pool : null;
|
||||
const common = options && options.common ? options.common : null;
|
||||
const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null;
|
||||
const QRCode = options && options.QRCode ? options.QRCode : null;
|
||||
const onboardingStore = options && options.onboardingStore ? options.onboardingStore : null;
|
||||
|
||||
if (!app || !pool || !common || !playerRuntime || !QRCode) {
|
||||
throw new Error('registerPlayerOnboardingRoutes requires app, pool, common, playerRuntime, and QRCode.');
|
||||
@@ -146,14 +179,15 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
return res.status(400).json({ error: 'Screen is required' });
|
||||
}
|
||||
|
||||
const status = await bindDeviceToScreen(pool, deviceId, clientName, screenSlug, playerRuntime.isClientNameAvailableOnScreen);
|
||||
const status = await bindDeviceToScreen(pool, deviceId, clientName, screenSlug, playerRuntime.isClientNameAvailableOnScreen, playerRuntime, onboardingStore);
|
||||
res.json({
|
||||
deviceId: deviceId,
|
||||
clientName: status ? status.client_name : clientName,
|
||||
screenId: status ? status.screen_id : null,
|
||||
screenSlug: status ? status.screen_slug : null,
|
||||
screenName: status ? status.screen_name : null,
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req)}/screen/${encodeURIComponent(status.screen_slug)}` : null
|
||||
screenId: status && status.screen_id ? status.screen_id : null,
|
||||
screenSlug: status ? status.screen_slug : screenSlug,
|
||||
screenName: status && status.screen_name ? status.screen_name : null,
|
||||
playerUrl: status && status.screen_slug ? `${getPublicBaseUrl(req)}/screen/${encodeURIComponent(status.screen_slug)}` : `${getPublicBaseUrl(req)}/screen/${encodeURIComponent(screenSlug)}`,
|
||||
queued: Boolean(status && status.queued)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -165,6 +199,7 @@ module.exports = {
|
||||
normalizeDeviceId: normalizeDeviceId,
|
||||
getPublicBaseUrl: getPublicBaseUrl,
|
||||
getOnboardingStatus: getOnboardingStatus,
|
||||
commitDeviceBinding: commitDeviceBinding,
|
||||
bindDeviceToScreen: bindDeviceToScreen,
|
||||
registerPlayerOnboardingRoutes: registerPlayerOnboardingRoutes
|
||||
};
|
||||
@@ -138,8 +138,8 @@ function registerPlayerRoutes(app, options) {
|
||||
}
|
||||
|
||||
const sent = connectionId
|
||||
? playerRuntime.sendCommandToConnection(req.params.slug, connectionId, commandPayload)
|
||||
: playerRuntime.broadcastCommand(req.params.slug, commandPayload);
|
||||
? await playerRuntime.sendCommandToConnection(req.params.slug, connectionId, commandPayload)
|
||||
: await playerRuntime.broadcastCommand(req.params.slug, commandPayload);
|
||||
|
||||
res.json({
|
||||
screen: screenRows[0] || null,
|
||||
|
||||
+16
-64
@@ -1,5 +1,6 @@
|
||||
const crypto = require('crypto');
|
||||
const { WebSocketServer, WebSocket } = require('ws');
|
||||
const { isClientNameAvailable } = require('../client-name-check');
|
||||
|
||||
function createPlayerRuntime(options) {
|
||||
const pool = options && options.pool ? options.pool : null;
|
||||
@@ -134,76 +135,25 @@ function createPlayerRuntime(options) {
|
||||
});
|
||||
}
|
||||
|
||||
async function isClientNameAvailableOnScreen(poolArg, clientName, excludeDeviceId) {
|
||||
const normalizedName = String(clientName || '').trim();
|
||||
if (!normalizedName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedDeviceId = normalizeDeviceId(excludeDeviceId);
|
||||
const lowerName = normalizedName.toLowerCase();
|
||||
const liveDeviceIds = new Set();
|
||||
const liveClientIds = new Set();
|
||||
|
||||
function snapshotAllConnections() {
|
||||
const allConnections = [];
|
||||
for (const bucket of connectionsBySlug.values()) {
|
||||
if (!bucket || typeof bucket.values !== 'function') {
|
||||
continue;
|
||||
}
|
||||
for (const connection of bucket.values()) {
|
||||
const existingDeviceId = normalizeDeviceId(connection && connection.deviceId ? connection.deviceId : '');
|
||||
const existingClientId = normalizeDeviceId(connection && connection.clientId ? connection.clientId : '');
|
||||
if (existingDeviceId) {
|
||||
liveDeviceIds.add(existingDeviceId);
|
||||
}
|
||||
if (existingClientId) {
|
||||
liveClientIds.add(existingClientId);
|
||||
}
|
||||
const existingName = String(connection && connection.clientName ? connection.clientName : '').trim();
|
||||
if (!existingName) {
|
||||
continue;
|
||||
}
|
||||
if (existingName.toLowerCase() !== lowerName) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedDeviceId && existingDeviceId && existingDeviceId === normalizedDeviceId) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
allConnections.push({
|
||||
clientId: connection.clientId || null,
|
||||
clientName: connection.clientName || null,
|
||||
deviceId: connection.deviceId || null
|
||||
});
|
||||
}
|
||||
}
|
||||
return allConnections;
|
||||
}
|
||||
|
||||
const activePool = poolArg || pool;
|
||||
if (!activePool || (!liveDeviceIds.size && !liveClientIds.size)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const [deviceRows] = await activePool.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(?))`,
|
||||
[normalizedName]
|
||||
);
|
||||
|
||||
for (let i = 0; i < deviceRows.length; i += 1) {
|
||||
const deviceId = normalizeDeviceId(deviceRows[i] && deviceRows[i].device_id ? deviceRows[i].device_id : '');
|
||||
if (!deviceId) {
|
||||
continue;
|
||||
}
|
||||
if (normalizedDeviceId && deviceId === normalizedDeviceId) {
|
||||
continue;
|
||||
}
|
||||
if (liveDeviceIds.has(deviceId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
async function isClientNameAvailableOnScreen(poolArg, clientName, excludeDeviceId) {
|
||||
return isClientNameAvailable(poolArg || pool, clientName, excludeDeviceId, snapshotAllConnections());
|
||||
}
|
||||
|
||||
function broadcastConnectionSnapshot(slug) {
|
||||
@@ -227,7 +177,7 @@ function createPlayerRuntime(options) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendCommandToConnection(slug, connectionId, commandOrPayload) {
|
||||
async function sendCommandToConnection(slug, connectionId, commandOrPayload) {
|
||||
const bucket = connectionsBySlug.get(String(slug || '').trim());
|
||||
if (!bucket || !bucket.size) {
|
||||
return 0;
|
||||
@@ -249,7 +199,7 @@ function createPlayerRuntime(options) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function broadcastCommand(slug, commandOrPayload) {
|
||||
async function broadcastCommand(slug, commandOrPayload) {
|
||||
const bucket = connectionsBySlug.get(String(slug || '').trim());
|
||||
if (!bucket || !bucket.size) {
|
||||
return 0;
|
||||
@@ -269,6 +219,7 @@ function createPlayerRuntime(options) {
|
||||
connection.socket.send(JSON.stringify(payload));
|
||||
sent += 1;
|
||||
});
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
@@ -405,6 +356,7 @@ function createPlayerRuntime(options) {
|
||||
return {
|
||||
installWebsocket: installWebsocket,
|
||||
snapshotConnections: snapshotConnections,
|
||||
snapshotAllConnections: snapshotAllConnections,
|
||||
isClientNameAvailableOnScreen: isClientNameAvailableOnScreen,
|
||||
sendCommandToConnection: sendCommandToConnection,
|
||||
broadcastCommand: broadcastCommand
|
||||
|
||||
Reference in New Issue
Block a user