feat(myTischtennis): implement asynchronous team data fetching and job status tracking
- Added a new endpoint to start an asynchronous job for fetching team data, allowing for non-blocking operations. - Implemented job status tracking to retrieve the status of ongoing fetch jobs, enhancing user experience with real-time updates. - Updated the frontend to initiate async fetch requests and poll for job completion, improving data retrieval efficiency and user feedback.
This commit is contained in:
@@ -3,14 +3,132 @@ import myTischtennisService from '../services/myTischtennisService.js';
|
||||
import MemberService from '../services/memberService.js';
|
||||
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
|
||||
import apiLogService from '../services/apiLogService.js';
|
||||
import axios from 'axios';
|
||||
import ClubTeam from '../models/ClubTeam.js';
|
||||
import League from '../models/League.js';
|
||||
import Season from '../models/Season.js';
|
||||
import User from '../models/User.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const teamDataFetchJobs = new Map();
|
||||
const TEAM_DATA_JOB_TTL_MS = 60 * 60 * 1000;
|
||||
|
||||
const cleanupFinishedTeamDataJobs = () => {
|
||||
const now = Date.now();
|
||||
for (const [jobId, job] of teamDataFetchJobs.entries()) {
|
||||
if (job.finishedAt && (now - job.finishedAt) > TEAM_DATA_JOB_TTL_MS) {
|
||||
teamDataFetchJobs.delete(jobId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class MyTischtennisUrlController {
|
||||
async startFetchTeamDataJob(req, res, next) {
|
||||
try {
|
||||
const { clubTeamId } = req.body || {};
|
||||
if (!clubTeamId) {
|
||||
throw new HttpError('clubTeamId is required', 400);
|
||||
}
|
||||
|
||||
cleanupFinishedTeamDataJobs();
|
||||
|
||||
const jobId = randomUUID();
|
||||
const startedAt = Date.now();
|
||||
teamDataFetchJobs.set(jobId, {
|
||||
jobId,
|
||||
status: 'queued',
|
||||
startedAt,
|
||||
updatedAt: startedAt,
|
||||
finishedAt: null,
|
||||
clubTeamId,
|
||||
result: null,
|
||||
error: null
|
||||
});
|
||||
|
||||
const authHeaders = {
|
||||
authcode: req.headers.authcode,
|
||||
userid: req.headers.userid
|
||||
};
|
||||
const internalPort = process.env.PORT || 3050;
|
||||
const internalUrl = `http://127.0.0.1:${internalPort}/api/mytischtennis/fetch-team-data`;
|
||||
|
||||
// Background execution; response is returned immediately.
|
||||
(async () => {
|
||||
const job = teamDataFetchJobs.get(jobId);
|
||||
if (!job) return;
|
||||
job.status = 'running';
|
||||
job.updatedAt = Date.now();
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
internalUrl,
|
||||
{ clubTeamId },
|
||||
{
|
||||
headers: authHeaders,
|
||||
timeout: 10 * 60 * 1000,
|
||||
validateStatus: () => true
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status >= 200 && response.status < 300 && response.data?.success) {
|
||||
job.status = 'completed';
|
||||
job.result = response.data;
|
||||
job.error = null;
|
||||
} else {
|
||||
job.status = 'failed';
|
||||
job.result = null;
|
||||
job.error = response.data?.error || response.data?.message || `Job failed with status ${response.status}`;
|
||||
}
|
||||
} catch (error) {
|
||||
job.status = 'failed';
|
||||
job.result = null;
|
||||
job.error = error?.message || String(error);
|
||||
} finally {
|
||||
job.updatedAt = Date.now();
|
||||
job.finishedAt = Date.now();
|
||||
}
|
||||
})();
|
||||
|
||||
return res.status(202).json({
|
||||
success: true,
|
||||
jobId,
|
||||
status: 'queued'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getFetchTeamDataJobStatus(req, res, next) {
|
||||
try {
|
||||
const { jobId } = req.params;
|
||||
cleanupFinishedTeamDataJobs();
|
||||
const job = teamDataFetchJobs.get(jobId);
|
||||
|
||||
if (!job) {
|
||||
throw new HttpError('Job not found', 404);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
job: {
|
||||
jobId: job.jobId,
|
||||
status: job.status,
|
||||
startedAt: job.startedAt,
|
||||
updatedAt: job.updatedAt,
|
||||
finishedAt: job.finishedAt,
|
||||
clubTeamId: job.clubTeamId,
|
||||
result: job.result,
|
||||
error: job.error
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse myTischtennis URL and return configuration data
|
||||
* POST /api/mytischtennis/parse-url
|
||||
|
||||
@@ -64,6 +64,12 @@ router.post('/configure-league', myTischtennisUrlController.configureLeague);
|
||||
// POST /api/mytischtennis/fetch-team-data - Manually fetch team data
|
||||
router.post('/fetch-team-data', myTischtennisUrlController.fetchTeamData);
|
||||
|
||||
// POST /api/mytischtennis/fetch-team-data/async - Start async manual fetch
|
||||
router.post('/fetch-team-data/async', myTischtennisUrlController.startFetchTeamDataJob);
|
||||
|
||||
// GET /api/mytischtennis/fetch-team-data/jobs/:jobId - Get async fetch job status
|
||||
router.get('/fetch-team-data/jobs/:jobId', myTischtennisUrlController.getFetchTeamDataJobStatus);
|
||||
|
||||
// GET /api/mytischtennis/team-url/:teamId - Get myTischtennis URL for team
|
||||
router.get('/team-url/:teamId', myTischtennisUrlController.getTeamUrl);
|
||||
|
||||
|
||||
@@ -1176,20 +1176,50 @@ export default {
|
||||
myTischtennisSuccess.value = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/mytischtennis/fetch-team-data', {
|
||||
const startResponse = await apiClient.post('/mytischtennis/fetch-team-data/async', {
|
||||
clubTeamId: teamToEdit.value.id
|
||||
}, { timeout: 30000 });
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const successMessage = getSafeMessage(response.data.message, 'Teamdaten erfolgreich abgerufen.');
|
||||
});
|
||||
|
||||
const jobId = startResponse?.data?.jobId;
|
||||
if (!jobId) {
|
||||
throw new Error('Async-Job konnte nicht gestartet werden.');
|
||||
}
|
||||
|
||||
const maxPollAttempts = 120; // ~4 Minuten bei 2s Intervall
|
||||
let completedResponse = null;
|
||||
for (let attempt = 0; attempt < maxPollAttempts; attempt++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
const pollResponse = await apiClient.get(`/mytischtennis/fetch-team-data/jobs/${jobId}`, {
|
||||
timeout: 15000
|
||||
});
|
||||
const job = pollResponse?.data?.job;
|
||||
if (!job) continue;
|
||||
|
||||
if (job.status === 'completed') {
|
||||
completedResponse = job.result;
|
||||
break;
|
||||
}
|
||||
|
||||
if (job.status === 'failed') {
|
||||
const failedMsg = getSafeMessage(job.error, 'Daten konnten nicht abgerufen werden.');
|
||||
throw new Error(failedMsg);
|
||||
}
|
||||
}
|
||||
|
||||
if (!completedResponse) {
|
||||
throw new Error('Zeitüberschreitung beim Abruf (Async-Job läuft zu lange).');
|
||||
}
|
||||
|
||||
if (completedResponse && completedResponse.success) {
|
||||
const successMessage = getSafeMessage(completedResponse.message, 'Teamdaten erfolgreich abgerufen.');
|
||||
myTischtennisSuccess.value = successMessage;
|
||||
|
||||
const teamName = getSafeMessage(response.data.data?.teamName, teamToEdit.value?.name || 'Unbekanntes Team');
|
||||
const fetchedCount = getSafeMessage(String(response.data.data?.fetchedCount ?? ''), '0');
|
||||
const teamName = getSafeMessage(completedResponse.data?.teamName, teamToEdit.value?.name || 'Unbekanntes Team');
|
||||
const fetchedCount = getSafeMessage(String(completedResponse.data?.fetchedCount ?? ''), '0');
|
||||
let detailsMessage = `Team: ${teamName}\nAbgerufene Datensätze: ${fetchedCount}`;
|
||||
|
||||
if (response.data.data?.tableUpdate) {
|
||||
const tableUpdate = getSafeMessage(response.data.data.tableUpdate);
|
||||
if (completedResponse.data?.tableUpdate) {
|
||||
const tableUpdate = getSafeMessage(completedResponse.data.tableUpdate);
|
||||
if (tableUpdate) {
|
||||
detailsMessage += `\n\nTabellenaktualisierung:\n${tableUpdate}`;
|
||||
}
|
||||
@@ -1201,23 +1231,25 @@ export default {
|
||||
detailsMessage,
|
||||
'success'
|
||||
);
|
||||
} else if (response.data && response.data.success === false) {
|
||||
const errorTitle = response.data.needsMyTischtennisReauth ? 'Login bei myTischtennis erforderlich' : 'Fehler';
|
||||
const errorMessage = getSafeMessage(response.data.error, 'Daten konnten nicht abgerufen werden.');
|
||||
const details = response.data.debug ? getSafeMessage(JSON.stringify(response.data.debug, null, 2)) : '';
|
||||
} else if (completedResponse && completedResponse.success === false) {
|
||||
const errorTitle = completedResponse.needsMyTischtennisReauth ? 'Login bei myTischtennis erforderlich' : 'Fehler';
|
||||
const errorMessage = getSafeMessage(completedResponse.error, 'Daten konnten nicht abgerufen werden.');
|
||||
const details = completedResponse.debug ? getSafeMessage(JSON.stringify(completedResponse.debug, null, 2)) : '';
|
||||
await showInfo(
|
||||
errorTitle,
|
||||
errorMessage,
|
||||
details,
|
||||
response.data.needsMyTischtennisReauth ? 'warning' : 'error'
|
||||
completedResponse.needsMyTischtennisReauth ? 'warning' : 'error'
|
||||
);
|
||||
myTischtennisError.value = errorMessage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Team-Daten:', error);
|
||||
const isTimeout = error?.code === 'ECONNABORTED';
|
||||
const isTimeout = error?.code === 'ECONNABORTED' || /Zeitüberschreitung/i.test(String(error?.message || ''));
|
||||
const errData = error?.response?.data || {};
|
||||
const errorMsg = isTimeout ? 'Zeitüberschreitung beim Abruf (Timeout).' : getSafeMessage(errData.message || errData.error, 'Daten konnten nicht abgerufen werden.');
|
||||
const errorMsg = isTimeout
|
||||
? 'Zeitüberschreitung beim Abruf (Timeout).'
|
||||
: getSafeMessage(error?.message || errData.message || errData.error, 'Daten konnten nicht abgerufen werden.');
|
||||
const details = errData.debug ? getSafeMessage(JSON.stringify(errData.debug, null, 2)) : '';
|
||||
myTischtennisError.value = errorMsg;
|
||||
await showInfo('Fehler', errorMsg, details, isTimeout ? 'warning' : 'error');
|
||||
|
||||
Reference in New Issue
Block a user