archicore 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,10 +10,61 @@ import { Logger } from '../../utils/logger.js';
10
10
  import { ExportManager } from '../../export/index.js';
11
11
  import { authMiddleware } from './auth.js';
12
12
  import { FileUtils } from '../../utils/file-utils.js';
13
+ import { taskQueue } from '../services/task-queue.js';
13
14
  export const apiRouter = Router();
14
15
  // Singleton сервиса проектов
15
16
  const projectService = new ProjectService();
16
17
  const authService = AuthService.getInstance();
18
+ // Регистрация исполнителей задач
19
+ taskQueue.registerExecutor('full-analysis', async (task, updateProgress) => {
20
+ try {
21
+ updateProgress({ phase: 'analyzing', current: 10, message: 'Starting full analysis...' });
22
+ const result = await projectService.runFullAnalysis(task.projectId, (phase, progress) => {
23
+ updateProgress({
24
+ phase,
25
+ current: Math.min(10 + progress * 0.9, 99),
26
+ message: `Analyzing: ${phase}...`,
27
+ });
28
+ });
29
+ return { success: true, data: result };
30
+ }
31
+ catch (error) {
32
+ return {
33
+ success: false,
34
+ error: error instanceof Error ? error.message : String(error),
35
+ };
36
+ }
37
+ });
38
+ taskQueue.registerExecutor('dead-code', async (task, updateProgress) => {
39
+ try {
40
+ updateProgress({ phase: 'analyzing', current: 20, message: 'Analyzing dead code...' });
41
+ const result = await projectService.findDeadCode(task.projectId);
42
+ return { success: true, data: result };
43
+ }
44
+ catch (error) {
45
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
46
+ }
47
+ });
48
+ taskQueue.registerExecutor('security', async (task, updateProgress) => {
49
+ try {
50
+ updateProgress({ phase: 'analyzing', current: 20, message: 'Analyzing security...' });
51
+ const result = await projectService.analyzeSecurity(task.projectId);
52
+ return { success: true, data: result };
53
+ }
54
+ catch (error) {
55
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
56
+ }
57
+ });
58
+ taskQueue.registerExecutor('metrics', async (task, updateProgress) => {
59
+ try {
60
+ updateProgress({ phase: 'analyzing', current: 20, message: 'Calculating metrics...' });
61
+ const result = await projectService.getMetrics(task.projectId);
62
+ return { success: true, data: result };
63
+ }
64
+ catch (error) {
65
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
66
+ }
67
+ });
17
68
  // Helper to get user ID from request
18
69
  const getUserId = (req) => {
19
70
  return req.user?.id;
@@ -631,11 +682,17 @@ apiRouter.post('/projects/:id/export', authMiddleware, checkProjectAccess, async
631
682
  /**
632
683
  * POST /api/projects/:id/full-analysis
633
684
  * Полный анализ проекта (все анализаторы)
685
+ * Запускает асинхронную задачу и возвращает task ID
686
+ *
687
+ * Query params:
688
+ * - async=true: вернуть task ID для отслеживания (по умолчанию)
689
+ * - async=false: синхронное выполнение (для обратной совместимости)
634
690
  */
635
691
  apiRouter.post('/projects/:id/full-analysis', authMiddleware, checkProjectAccess, async (req, res) => {
636
692
  try {
637
693
  const { id } = req.params;
638
694
  const userId = getUserId(req);
695
+ const asyncMode = req.query.async !== 'false'; // По умолчанию async=true
639
696
  // Check full analysis limit
640
697
  if (userId) {
641
698
  const usageResult = await authService.checkAndUpdateUsage(userId, 'analysis');
@@ -648,8 +705,20 @@ apiRouter.post('/projects/:id/full-analysis', authMiddleware, checkProjectAccess
648
705
  return;
649
706
  }
650
707
  }
651
- const result = await projectService.runFullAnalysis(id);
652
- res.json(result);
708
+ if (asyncMode) {
709
+ // Асинхронный режим - создаём задачу и возвращаем ID
710
+ const task = taskQueue.createTask('full-analysis', id, userId || 'anonymous');
711
+ res.json({
712
+ taskId: task.id,
713
+ status: task.status,
714
+ message: 'Analysis task queued. Poll /api/tasks/:taskId for status.',
715
+ });
716
+ }
717
+ else {
718
+ // Синхронный режим - ждём завершения (для обратной совместимости)
719
+ const result = await projectService.runFullAnalysis(id);
720
+ res.json(result);
721
+ }
653
722
  }
654
723
  catch (error) {
655
724
  Logger.error('Failed to run full analysis:', error);
@@ -789,6 +858,183 @@ apiRouter.get('/projects/:id/scan', authMiddleware, checkProjectAccess, async (r
789
858
  res.status(500).json({ error: 'Failed to scan project' });
790
859
  }
791
860
  });
861
+ // ==================== TASK QUEUE ENDPOINTS ====================
862
+ /**
863
+ * GET /api/tasks/:taskId
864
+ * Получить статус и результат задачи
865
+ */
866
+ apiRouter.get('/tasks/:taskId', authMiddleware, async (req, res) => {
867
+ try {
868
+ const { taskId } = req.params;
869
+ const userId = getUserId(req);
870
+ const task = taskQueue.getTask(taskId);
871
+ if (!task) {
872
+ res.status(404).json({ error: 'Task not found' });
873
+ return;
874
+ }
875
+ // Проверяем, что задача принадлежит пользователю (или админ)
876
+ if (task.userId !== userId && !(req.user?.tier === 'enterprise' || req.user?.id?.startsWith('admin-'))) {
877
+ res.status(403).json({ error: 'Access denied' });
878
+ return;
879
+ }
880
+ res.json({
881
+ id: task.id,
882
+ type: task.type,
883
+ projectId: task.projectId,
884
+ status: task.status,
885
+ progress: task.progress,
886
+ result: task.result,
887
+ error: task.error,
888
+ createdAt: task.createdAt,
889
+ startedAt: task.startedAt,
890
+ completedAt: task.completedAt,
891
+ });
892
+ }
893
+ catch (error) {
894
+ Logger.error('Failed to get task:', error);
895
+ res.status(500).json({ error: 'Failed to get task' });
896
+ }
897
+ });
898
+ /**
899
+ * GET /api/tasks
900
+ * Получить список задач пользователя
901
+ */
902
+ apiRouter.get('/tasks', authMiddleware, async (req, res) => {
903
+ try {
904
+ const userId = getUserId(req);
905
+ const limit = parseInt(req.query.limit) || 10;
906
+ if (!userId) {
907
+ res.status(401).json({ error: 'Authentication required' });
908
+ return;
909
+ }
910
+ const tasks = taskQueue.getUserTasks(userId, limit);
911
+ res.json({ tasks });
912
+ }
913
+ catch (error) {
914
+ Logger.error('Failed to get tasks:', error);
915
+ res.status(500).json({ error: 'Failed to get tasks' });
916
+ }
917
+ });
918
+ /**
919
+ * GET /api/projects/:id/tasks
920
+ * Получить список задач проекта
921
+ */
922
+ apiRouter.get('/projects/:id/tasks', authMiddleware, checkProjectAccess, async (req, res) => {
923
+ try {
924
+ const { id } = req.params;
925
+ const limit = parseInt(req.query.limit) || 10;
926
+ const tasks = taskQueue.getProjectTasks(id, limit);
927
+ res.json({ tasks });
928
+ }
929
+ catch (error) {
930
+ Logger.error('Failed to get project tasks:', error);
931
+ res.status(500).json({ error: 'Failed to get project tasks' });
932
+ }
933
+ });
934
+ /**
935
+ * GET /api/tasks/:taskId/progress
936
+ * SSE endpoint для real-time прогресса задачи
937
+ */
938
+ apiRouter.get('/tasks/:taskId/progress', async (req, res) => {
939
+ const { taskId } = req.params;
940
+ // Handle auth via query param (EventSource doesn't support headers)
941
+ const token = req.query.token || req.headers.authorization?.substring(7);
942
+ if (!token) {
943
+ res.status(401).json({ error: 'Authentication required' });
944
+ return;
945
+ }
946
+ const user = await authService.validateToken(token);
947
+ if (!user) {
948
+ res.status(401).json({ error: 'Invalid token' });
949
+ return;
950
+ }
951
+ const task = taskQueue.getTask(taskId);
952
+ if (!task) {
953
+ res.status(404).json({ error: 'Task not found' });
954
+ return;
955
+ }
956
+ if (task.userId !== user.id && !(user.tier === 'enterprise' || user.id?.startsWith('admin-'))) {
957
+ res.status(403).json({ error: 'Access denied' });
958
+ return;
959
+ }
960
+ // Set SSE headers
961
+ res.setHeader('Content-Type', 'text/event-stream');
962
+ res.setHeader('Cache-Control', 'no-cache');
963
+ res.setHeader('Connection', 'keep-alive');
964
+ res.setHeader('X-Accel-Buffering', 'no');
965
+ res.flushHeaders();
966
+ // Send current status
967
+ res.write(`data: ${JSON.stringify({ type: 'status', task: { id: task.id, status: task.status, progress: task.progress } })}\n\n`);
968
+ // If already completed, close connection
969
+ if (task.status === 'completed' || task.status === 'failed') {
970
+ res.write(`data: ${JSON.stringify({ type: 'done', result: task.result, error: task.error })}\n\n`);
971
+ res.end();
972
+ return;
973
+ }
974
+ // Subscribe to task events
975
+ const onProgress = (updatedTask) => {
976
+ if (updatedTask.id === taskId) {
977
+ try {
978
+ res.write(`data: ${JSON.stringify({ type: 'progress', progress: updatedTask.progress })}\n\n`);
979
+ }
980
+ catch {
981
+ // Client disconnected
982
+ }
983
+ }
984
+ };
985
+ const onComplete = (updatedTask) => {
986
+ if (updatedTask.id === taskId) {
987
+ try {
988
+ res.write(`data: ${JSON.stringify({ type: 'done', result: updatedTask.result })}\n\n`);
989
+ res.end();
990
+ }
991
+ catch {
992
+ // Client disconnected
993
+ }
994
+ cleanup();
995
+ }
996
+ };
997
+ const onFailed = (updatedTask) => {
998
+ if (updatedTask.id === taskId) {
999
+ try {
1000
+ res.write(`data: ${JSON.stringify({ type: 'error', error: updatedTask.error })}\n\n`);
1001
+ res.end();
1002
+ }
1003
+ catch {
1004
+ // Client disconnected
1005
+ }
1006
+ cleanup();
1007
+ }
1008
+ };
1009
+ const cleanup = () => {
1010
+ taskQueue.off('taskProgress', onProgress);
1011
+ taskQueue.off('taskCompleted', onComplete);
1012
+ taskQueue.off('taskFailed', onFailed);
1013
+ };
1014
+ taskQueue.on('taskProgress', onProgress);
1015
+ taskQueue.on('taskCompleted', onComplete);
1016
+ taskQueue.on('taskFailed', onFailed);
1017
+ // Handle client disconnect
1018
+ req.on('close', cleanup);
1019
+ });
1020
+ /**
1021
+ * GET /api/queue/stats
1022
+ * Статистика очереди задач (для админов)
1023
+ */
1024
+ apiRouter.get('/queue/stats', authMiddleware, async (req, res) => {
1025
+ try {
1026
+ if (!(req.user?.tier === 'enterprise' || req.user?.id?.startsWith('admin-'))) {
1027
+ res.status(403).json({ error: 'Admin access required' });
1028
+ return;
1029
+ }
1030
+ const stats = taskQueue.getStats();
1031
+ res.json(stats);
1032
+ }
1033
+ catch (error) {
1034
+ Logger.error('Failed to get queue stats:', error);
1035
+ res.status(500).json({ error: 'Failed to get queue stats' });
1036
+ }
1037
+ });
792
1038
  // Helper function to format time
793
1039
  function formatTime(seconds) {
794
1040
  if (seconds < 60) {
@@ -161,8 +161,7 @@ authRouter.get('/me', authMiddleware, async (req, res) => {
161
161
  limits: {
162
162
  requestsPerDay: limits.requestsPerDay,
163
163
  fullAnalysisPerDay: limits.fullAnalysisPerDay,
164
- projectsPerDay: limits.projectsPerDay,
165
- maxProjectSizeMB: limits.maxProjectSizeMB
164
+ projectsPerDay: limits.projectsPerDay
166
165
  },
167
166
  usage: user.usage
168
167
  });
@@ -297,8 +296,7 @@ authRouter.get('/usage', authMiddleware, async (req, res) => {
297
296
  limits: {
298
297
  requestsPerDay: limits.requestsPerDay,
299
298
  fullAnalysisPerDay: limits.fullAnalysisPerDay,
300
- projectsPerDay: limits.projectsPerDay,
301
- maxProjectSizeMB: limits.maxProjectSizeMB
299
+ projectsPerDay: limits.projectsPerDay
302
300
  },
303
301
  subscription: user.subscription
304
302
  });
@@ -13,7 +13,6 @@ import { ProjectService } from '../services/project-service.js';
13
13
  import { AuthService } from '../services/auth-service.js';
14
14
  import { authMiddleware } from './auth.js';
15
15
  import { Logger } from '../../utils/logger.js';
16
- import { TIER_LIMITS } from '../../types/user.js';
17
16
  export const githubRouter = Router();
18
17
  // Services
19
18
  const githubService = new GitHubService();
@@ -275,22 +274,6 @@ githubRouter.post('/repositories/connect', authMiddleware, async (req, res) => {
275
274
  Logger.progress(`Downloading repository: ${connectedRepo.fullName} (branch: ${targetBranch})`);
276
275
  const zipBuffer = await githubService.downloadRepository(req.user.id, connectedRepo.fullName, targetBranch);
277
276
  Logger.info(`Downloaded ZIP: ${zipBuffer.length} bytes`);
278
- // Check project size limit
279
- const user = await authService.getUser(req.user.id);
280
- if (user) {
281
- const limits = TIER_LIMITS[user.tier];
282
- const projectSizeMB = zipBuffer.length / (1024 * 1024);
283
- if (projectSizeMB > limits.maxProjectSizeMB) {
284
- res.status(413).json({
285
- error: 'Project size limit exceeded',
286
- message: `Project size (${projectSizeMB.toFixed(1)}MB) exceeds your plan limit (${limits.maxProjectSizeMB}MB). Upgrade to a higher tier for larger projects.`,
287
- size: projectSizeMB,
288
- limit: limits.maxProjectSizeMB,
289
- tier: user.tier
290
- });
291
- return;
292
- }
293
- }
294
277
  // Create projects directory
295
278
  const projectsDir = process.env.PROJECTS_DIR || join('.archicore', 'projects');
296
279
  await mkdir(projectsDir, { recursive: true });
@@ -157,8 +157,10 @@ export declare class ProjectService {
157
157
  getExportData(projectId: string): Promise<ExportData>;
158
158
  /**
159
159
  * Полный анализ проекта
160
+ * @param projectId - ID проекта
161
+ * @param onProgress - опциональный callback для отслеживания прогресса (phase, progress 0-100)
160
162
  */
161
- runFullAnalysis(projectId: string): Promise<{
163
+ runFullAnalysis(projectId: string, onProgress?: (phase: string, progress: number) => void): Promise<{
162
164
  metrics: ProjectMetrics;
163
165
  rules: RulesCheckResult;
164
166
  deadCode: DeadCodeResult;
@@ -788,21 +788,32 @@ export class ProjectService {
788
788
  }
789
789
  /**
790
790
  * Полный анализ проекта
791
+ * @param projectId - ID проекта
792
+ * @param onProgress - опциональный callback для отслеживания прогресса (phase, progress 0-100)
791
793
  */
792
- async runFullAnalysis(projectId) {
794
+ async runFullAnalysis(projectId, onProgress) {
793
795
  Logger.progress('Running full analysis...');
796
+ onProgress?.('starting', 0);
797
+ // Запускаем все анализы параллельно с отслеживанием прогресса
798
+ const metricsPromise = this.getMetrics(projectId).then(r => { onProgress?.('metrics', 20); return r; });
799
+ const rulesPromise = this.checkRules(projectId).then(r => { onProgress?.('rules', 35); return r; });
800
+ const deadCodePromise = this.findDeadCode(projectId).then(r => { onProgress?.('dead-code', 50); return r; });
801
+ const duplicationPromise = this.findDuplication(projectId).then(r => { onProgress?.('duplication', 60); return r; });
802
+ const securityPromise = this.analyzeSecurity(projectId).then(r => { onProgress?.('security', 70); return r; });
794
803
  const [metrics, rules, deadCode, duplication, security] = await Promise.all([
795
- this.getMetrics(projectId),
796
- this.checkRules(projectId),
797
- this.findDeadCode(projectId),
798
- this.findDuplication(projectId),
799
- this.analyzeSecurity(projectId)
804
+ metricsPromise,
805
+ rulesPromise,
806
+ deadCodePromise,
807
+ duplicationPromise,
808
+ securityPromise,
800
809
  ]);
801
810
  // Refactoring зависит от предыдущих результатов
811
+ onProgress?.('refactoring', 75);
802
812
  const data = await this.getProjectData(projectId);
803
813
  const fileContents = await this.getFileContents(projectId);
804
814
  const engine = new RefactoringEngine();
805
815
  const refactoring = await engine.analyze(data.graph, data.symbols, fileContents, metrics, duplication, deadCode, rules);
816
+ onProgress?.('complete', 100);
806
817
  Logger.success('Full analysis complete');
807
818
  return {
808
819
  metrics,
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Task Queue Service
3
+ *
4
+ * Асинхронная очередь задач для параллельного выполнения анализа
5
+ * Позволяет множеству пользователей запускать анализ одновременно
6
+ */
7
+ import { EventEmitter } from 'events';
8
+ export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
9
+ export interface TaskProgress {
10
+ phase: string;
11
+ current: number;
12
+ total: number;
13
+ message: string;
14
+ }
15
+ export interface Task<T = unknown> {
16
+ id: string;
17
+ type: string;
18
+ projectId: string;
19
+ userId: string;
20
+ status: TaskStatus;
21
+ progress: TaskProgress;
22
+ result?: T;
23
+ error?: string;
24
+ createdAt: number;
25
+ startedAt?: number;
26
+ completedAt?: number;
27
+ }
28
+ export interface TaskResult {
29
+ success: boolean;
30
+ data?: unknown;
31
+ error?: string;
32
+ }
33
+ type TaskExecutor = (task: Task, updateProgress: (progress: Partial<TaskProgress>) => void) => Promise<TaskResult>;
34
+ declare class TaskQueueService extends EventEmitter {
35
+ private tasks;
36
+ private executors;
37
+ private runningTasks;
38
+ private maxConcurrentTasks;
39
+ private taskTimeout;
40
+ constructor();
41
+ /**
42
+ * Регистрация исполнителя задач
43
+ */
44
+ registerExecutor(taskType: string, executor: TaskExecutor): void;
45
+ /**
46
+ * Создание новой задачи
47
+ */
48
+ createTask(type: string, projectId: string, userId: string): Task;
49
+ /**
50
+ * Получение задачи по ID
51
+ */
52
+ getTask(taskId: string): Task | undefined;
53
+ /**
54
+ * Получение задач пользователя
55
+ */
56
+ getUserTasks(userId: string, limit?: number): Task[];
57
+ /**
58
+ * Получение задач проекта
59
+ */
60
+ getProjectTasks(projectId: string, limit?: number): Task[];
61
+ /**
62
+ * Обработка очереди задач
63
+ */
64
+ private processQueue;
65
+ /**
66
+ * Выполнение задачи
67
+ */
68
+ private executeTask;
69
+ /**
70
+ * Обновление прогресса задачи
71
+ */
72
+ private updateTaskProgress;
73
+ /**
74
+ * Завершение задачи успешно
75
+ */
76
+ private completeTask;
77
+ /**
78
+ * Завершение задачи с ошибкой
79
+ */
80
+ private failTask;
81
+ /**
82
+ * Очистка старых задач (старше 1 часа)
83
+ */
84
+ private cleanupOldTasks;
85
+ /**
86
+ * Статистика очереди
87
+ */
88
+ getStats(): {
89
+ total: number;
90
+ pending: number;
91
+ running: number;
92
+ completed: number;
93
+ failed: number;
94
+ };
95
+ }
96
+ export declare const taskQueue: TaskQueueService;
97
+ export {};
98
+ //# sourceMappingURL=task-queue.d.ts.map