Retry transient Foodsharing network failures such as socket hang up for safe read/check actions and add explicit pickup fallback scheduling, tick, and result logs so missed fallback checks can be diagnosed.
188 lines
5.3 KiB
JavaScript
188 lines
5.3 KiB
JavaScript
const foodsharingClient = require('./foodsharingClient');
|
||
const sessionStore = require('./sessionStore');
|
||
const notificationService = require('./notificationService');
|
||
|
||
const ADMIN_SESSION_ERROR_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
||
const DEFAULT_TRANSIENT_RETRY_DELAYS_MS = [1000, 3000];
|
||
const NON_RETRIABLE_TRANSIENT_LABELS = new Set(['bookSlot']);
|
||
const adminSessionErrorCooldowns = new Map();
|
||
|
||
function wait(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
function shouldNotifyAdminSessionError(key) {
|
||
if (!key) {
|
||
return false;
|
||
}
|
||
const now = Date.now();
|
||
const lastNotified = adminSessionErrorCooldowns.get(key) || 0;
|
||
if (now - lastNotified < ADMIN_SESSION_ERROR_COOLDOWN_MS) {
|
||
return false;
|
||
}
|
||
adminSessionErrorCooldowns.set(key, now);
|
||
return true;
|
||
}
|
||
|
||
function isUnauthorizedError(error) {
|
||
const status = error?.response?.status;
|
||
if (status === 401) {
|
||
return true;
|
||
}
|
||
if (status !== 403) {
|
||
return false;
|
||
}
|
||
const data = error?.response?.data;
|
||
const message =
|
||
typeof data === 'string'
|
||
? data
|
||
: typeof data?.message === 'string'
|
||
? data.message
|
||
: '';
|
||
return message.toLowerCase().includes('not logged in');
|
||
}
|
||
|
||
function isCsrfError(error) {
|
||
const status = error?.response?.status;
|
||
if (status !== 400) {
|
||
return false;
|
||
}
|
||
const data = error?.response?.data;
|
||
const message =
|
||
typeof data === 'string'
|
||
? data
|
||
: typeof data?.message === 'string'
|
||
? data.message
|
||
: '';
|
||
return message.toLowerCase().includes('csrf');
|
||
}
|
||
|
||
function isTransientNetworkError(error) {
|
||
if (!error) {
|
||
return false;
|
||
}
|
||
if (error.response) {
|
||
return false;
|
||
}
|
||
const code = String(error.code || '').toUpperCase();
|
||
if (['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'ENOTFOUND', 'EAI_AGAIN'].includes(code)) {
|
||
return true;
|
||
}
|
||
const message = String(error.message || '').toLowerCase();
|
||
return (
|
||
message.includes('socket hang up') ||
|
||
message.includes('timeout') ||
|
||
message.includes('network error') ||
|
||
message.includes('connection reset')
|
||
);
|
||
}
|
||
|
||
async function refreshSession(session, { label } = {}) {
|
||
if (!session?.credentials?.email || !session?.credentials?.password) {
|
||
console.warn(
|
||
`[SESSION] Session ${session?.id || 'unbekannt'} kann nicht erneuert werden – keine Zugangsdaten gespeichert.`
|
||
);
|
||
return false;
|
||
}
|
||
try {
|
||
const refreshed = await foodsharingClient.login(
|
||
session.credentials.email,
|
||
session.credentials.password
|
||
);
|
||
sessionStore.update(session.id, {
|
||
cookieHeader: refreshed.cookieHeader,
|
||
csrfToken: refreshed.csrfToken,
|
||
profile: {
|
||
...session.profile,
|
||
...refreshed.profile
|
||
}
|
||
});
|
||
console.log(
|
||
`[SESSION] Session ${session.id} wurde erfolgreich erneuert${label ? ` (${label})` : ''}.`
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error(
|
||
`[SESSION] Session ${session?.id || 'unbekannt'} konnte nicht erneuert werden${label ? ` (${label})` : ''}:`,
|
||
error.message
|
||
);
|
||
const profileId = session?.profile?.id ? String(session.profile.id) : null;
|
||
const notifyKey = profileId || session?.id || null;
|
||
if (shouldNotifyAdminSessionError(notifyKey)) {
|
||
try {
|
||
await notificationService.sendAdminSessionErrorNotification({
|
||
profileId,
|
||
profileEmail: session?.credentials?.email || session?.profile?.email,
|
||
profileName: session?.profile?.name,
|
||
sessionId: session?.id,
|
||
error: error?.message,
|
||
label
|
||
});
|
||
} catch (notifyError) {
|
||
console.error('[NOTIFY] Admin-Session-Fehler konnte nicht gemeldet werden:', notifyError.message);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function ensureSession(session) {
|
||
if (!session?.profile?.id) {
|
||
return false;
|
||
}
|
||
if (!session.cookieHeader) {
|
||
return refreshSession(session, { label: 'missing-cookie' });
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function withSessionRetry(session, action, { label, transientRetries } = {}) {
|
||
if (!session) {
|
||
throw new Error('Session fehlt');
|
||
}
|
||
if (!session.cookieHeader && session.credentials) {
|
||
const refreshed = await refreshSession(session, { label });
|
||
if (!refreshed) {
|
||
throw new Error('Session konnte nicht erneuert werden');
|
||
}
|
||
}
|
||
const retryDelays = NON_RETRIABLE_TRANSIENT_LABELS.has(label)
|
||
? []
|
||
: DEFAULT_TRANSIENT_RETRY_DELAYS_MS.slice(
|
||
0,
|
||
Number.isFinite(transientRetries) ? Math.max(0, transientRetries) : DEFAULT_TRANSIENT_RETRY_DELAYS_MS.length
|
||
);
|
||
let transientAttempt = 0;
|
||
while (true) {
|
||
try {
|
||
return await action();
|
||
} catch (error) {
|
||
if (isTransientNetworkError(error) && transientAttempt < retryDelays.length) {
|
||
transientAttempt += 1;
|
||
const delay = retryDelays[transientAttempt - 1];
|
||
console.warn(
|
||
`[SESSION] Transienter Fehler${label ? ` (${label})` : ''}: ${error.message}. ` +
|
||
`Retry ${transientAttempt}/${retryDelays.length} in ${delay}ms.`
|
||
);
|
||
await wait(delay);
|
||
continue;
|
||
}
|
||
if (!isUnauthorizedError(error) && !isCsrfError(error)) {
|
||
throw error;
|
||
}
|
||
const refreshed = await refreshSession(session, { label });
|
||
if (!refreshed) {
|
||
throw error;
|
||
}
|
||
return action();
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
ensureSession,
|
||
isTransientNetworkError,
|
||
refreshSession,
|
||
withSessionRetry
|
||
};
|