import 'dotenv/config'; import _ from 'lodash'; import { io } from 'socket.io-client'; import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js'; import browser from './system/browser.js'; import configs from './system/config.js'; import { delay, isTimeReached, safeClosePage } from './system/utils.js'; import pLimit from 'p-limit'; let MANAGER_BIDS = []; const activeTasks = new Set(); const handleUpdateProductTabs = (data) => { if (!Array.isArray(data)) { console.log('Data must be array'); return; } const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid])); const newDataManager = data.map(({ children, ...web }) => { const prevApiBid = managerBidMap.get(web.id); const newChildren = children.map((item) => { const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id); if (prevProductTab) { prevProductTab.setNewData(item); return prevProductTab; } return createBidProduct(web, item); }); if (prevApiBid) { prevApiBid.setNewData({ children: newChildren, ...web }); return prevApiBid; } return createApiBid({ ...web, children: newChildren }); }); MANAGER_BIDS = newDataManager; }; const tracking = async () => { console.log('🚀 Tracking process started...'); while (true) { try { console.log('🔍 Scanning active bids...'); const productTabs = _.flatMap(MANAGER_BIDS, 'children'); // Lắng nghe sự kiện của API bids (chạy song song) await Promise.allSettled( MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => { console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`); return apiBid.listen_events(); }), ); Promise.allSettled( productTabs.map(async (productTab) => { console.log(`📌 Processing Product ID: ${productTab.id}`); // Xác định parent context if (!productTab.parent_browser_context) { const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id }); productTab.parent_browser_context = parent?.browser_context; if (!productTab.parent_browser_context) { console.log(`⏳ Waiting for parent process... (Product ID: ${productTab.id})`); return; } } // Kết nối Puppeteer nếu chưa có page_context if (!productTab.page_context) { console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`); await productTab.puppeteer_connect(); } // Kiểm tra URL và điều hướng nếu cần if ((await productTab.page_context.url()) !== productTab.url) { console.log(`🔄 Redirecting to new URL for Product ID: ${productTab.id}`); await productTab.gotoLink(); } // Cập nhật nếu cần thiết if (shouldUpdateProductTab(productTab)) { console.log(`🔄 Updating Product ID: ${productTab.id}...`); await productTab.update(); } else { console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`); } // Chờ first bid if (!productTab.first_bid) { console.log(`🎯 Waiting for first bid for Product ID: ${productTab.id}`); return; } // Kiểm tra thời gian bid if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) { console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`); return; } // Thực thi hành động console.log(`🚀 Executing action for Product ID: ${productTab.id}`); await productTab.action(); }), ); // Dọn dẹp tab không dùng console.log('🧹 Cleaning up unused tabs...'); clearLazyTab(); // Cập nhật trạng thái tracking console.log('📊 Tracking work status...'); workTracking(); } catch (error) { console.error('❌ Error in tracking loop:', error); } console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`); await delay(configs.AUTO_TRACKING_DELAY); } }; const clearLazyTab = async () => { if (!browser) { console.warn('⚠️ Browser is not available or disconnected.'); return; } try { const pages = await browser.pages(); // Lấy danh sách URL từ flattenedArray const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined console.log( '🔍 Page URLs:', pages.map((page) => page.url()), ); for (const page of pages) { const pageUrl = page.url(); // 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL if (!pageUrl || pageUrl === 'about:blank') continue; if (!activeUrls.includes(pageUrl)) { if (!page.isClosed() && browser.isConnected()) { try { await page.close(); console.log(`🛑 Closing unused tab: ${pageUrl}`); } catch (err) { console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`); } } } } } catch (err) { console.error('❌ Error in clearLazyTab:', err.message); } }; const workTracking = async () => { try { const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]); const limit = pLimit(5); await Promise.allSettled( activeData .filter((item) => item.page_context && !item.page_context.isClosed()) .filter((item) => !activeTasks.has(item.id)) .map((item) => limit(async () => { activeTasks.add(item.id); try { await item.handleTakeWorkSnapshot(); } catch (error) { console.error(`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`, error); } finally { activeTasks.delete(item.id); } }), ), ); } catch (error) { console.error(`[❌ ERROR] Work tracking failed: ${error.message}\n`, error.stack); } }; (async () => { const socket = io(configs.SOCKET_URL, { transports: ['websocket'], reconnection: true, }); // listen connect socket.on('connect', () => { console.log('✅ Connected to WebSocket server'); console.log('🔗 Socket ID:', socket.id); }); // listen event socket.on('bidsUpdated', async (data) => { console.log('📢 Bids Data:', data); handleUpdateProductTabs(data); }); socket.on('webUpdated', async (data) => { console.log('📢 Account was updated:', data); const isDeleted = deleteProfile(data); if (isDeleted) { console.log('✅ Profile deleted successfully!'); const tabs = MANAGER_BIDS.filter((item) => item.url === data.url || item?.web_bid.url === data.url); if (tabs.length <= 0) return; await Promise.all(tabs.map((tab) => safeClosePage(tab))); } else { console.log('⚠️ No profile found to delete.'); } }); // AUTO TRACKING tracking(); })();