mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-18 18:40:47 -04:00
95 lines
3.3 KiB
JavaScript
95 lines
3.3 KiB
JavaScript
// Owns connection replacement and recovery from failed initial logins.
|
|
function createConnectionManager({ createRuntime, logger }) {
|
|
let desired = { enabled: false, token: '' };
|
|
let revision = 0;
|
|
let runtime = null;
|
|
let queue = Promise.resolve();
|
|
let retryTimer = null;
|
|
let retryDelay = 5000;
|
|
|
|
function enqueue(operation) {
|
|
const pending = queue.then(operation);
|
|
// Report failures without poisoning subsequent configuration changes.
|
|
queue = pending.catch((err) => logger.error('Discord connection update failed', err.message));
|
|
return pending;
|
|
}
|
|
|
|
async function stop() {
|
|
const previous = runtime;
|
|
runtime = null;
|
|
if (!previous) return;
|
|
previous.stop();
|
|
// discord.js cannot cancel login while gateway discovery is pending.
|
|
// Let that attempt settle before destroying it, otherwise it can open a
|
|
// socket after destroy() has already run and leave an orphan connection.
|
|
await previous.login;
|
|
await previous.client.destroy();
|
|
}
|
|
|
|
function scheduleRetry(version) {
|
|
if (version !== revision || !desired.enabled) return;
|
|
const delay = retryDelay;
|
|
retryDelay = Math.min(retryDelay * 2, 60000);
|
|
logger.warn('Retrying Discord login', { delayMs: delay });
|
|
retryTimer = setTimeout(() => {
|
|
retryTimer = null;
|
|
enqueue(() => start(version));
|
|
}, delay);
|
|
retryTimer.unref?.();
|
|
}
|
|
|
|
async function start(version) {
|
|
if (version !== revision || !desired.enabled) return;
|
|
await stop();
|
|
if (version !== revision || !desired.enabled) return;
|
|
const current = createRuntime();
|
|
runtime = current;
|
|
const { client } = current;
|
|
client.on('error', (err) => logger.error('Discord client error', err.message));
|
|
client.on('shardError', (err, shardId) => {
|
|
logger.warn('Discord connection error', { shardId, error: err.message });
|
|
});
|
|
client.on('shardDisconnect', (event, shardId) => {
|
|
logger.warn('Discord disconnected', { shardId, code: event.code });
|
|
});
|
|
client.on('clientReady', () => { retryDelay = 5000; });
|
|
|
|
// Login must not block configuration saves while the network is down.
|
|
// Each attempt has its own client, so stale login completion/cleanup cannot
|
|
// clear the credentials or stop the connection of its replacement.
|
|
current.login = client.login(desired.token).catch((err) => {
|
|
if (version !== revision || runtime !== current) return;
|
|
logger.error('Discord login failed', err.message);
|
|
enqueue(async () => {
|
|
if (version !== revision || runtime !== current) return;
|
|
await stop();
|
|
// Credentials require an operator edit; retry network/service failures.
|
|
if (err.code === 'TokenInvalid' || err.status === 401) return;
|
|
scheduleRetry(version);
|
|
});
|
|
});
|
|
}
|
|
|
|
function configure({ enabled, token }) {
|
|
const next = { enabled: Boolean(enabled), token };
|
|
if (next.enabled === desired.enabled && next.token === desired.token) return queue;
|
|
desired = next;
|
|
const version = ++revision;
|
|
clearTimeout(retryTimer);
|
|
retryTimer = null;
|
|
retryDelay = 5000;
|
|
return enqueue(async () => {
|
|
if (version !== revision) return;
|
|
if (desired.enabled) await start(version);
|
|
else await stop();
|
|
});
|
|
}
|
|
|
|
return {
|
|
configure,
|
|
refresh() { runtime?.refresh(); },
|
|
};
|
|
}
|
|
|
|
module.exports = { createConnectionManager };
|