import "dotenv/config"; import _ from "lodash"; import pLimit from "p-limit"; import { io } from "socket.io-client"; import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab, } from "./service/app-service.js"; import { updateLoginStatus } from "./system/apis/bid.js"; import browser from "./system/browser.js"; import configs from "./system/config.js"; import { delay, findNearestClosingChild, isTimeReached, safeClosePage, subtractSeconds, } from "./system/utils.js"; global.IS_CLEANING = true; let MANAGER_BIDS = []; const activeTasks = new Set(); let timeToUpdateLogin = new Date().toUTCString(); 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 addProductTab = (data) => { if ( typeof data !== "object" || data === null || !Array.isArray(data.children) ) { console.warn("Data must be an object with a children array"); return; } const { children, ...web } = data; if (children.length === 0) { console.warn( `⚠️ No children found for bid id ${web.id}, skipping addProductTab` ); return; } const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid])); const prevApiBid = managerBidMap.get(web.id); if (prevApiBid) { // Cập nhật const updatedChildren = prevApiBid.children; children.forEach((newChild) => { const existingChildIndex = updatedChildren.findIndex( (c) => c.id === newChild.id ); if (existingChildIndex !== -1) { updatedChildren[existingChildIndex].setNewData(newChild); } else { updatedChildren.push(createBidProduct(web, newChild)); } }); prevApiBid.setNewData({ ...web, children: updatedChildren }); } else { // Tạo mới const newChildren = children.map((item) => createBidProduct(web, item)); const newApiBid = createApiBid({ ...web, children: newChildren }); MANAGER_BIDS.push(newApiBid); console.log("%cindex.js:116 {MANAGER_BIDS}", "color: #007acc;", { MANAGER_BIDS, children: newChildren, }); } }; const tracking = async () => { console.log("🚀 Tracking process started..."); while (true) { try { console.log("🔍 Scanning active bids..."); const productTabs = _.flatMap(MANAGER_BIDS, "children"); 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(); }) ); await Promise.allSettled( MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => { console.log(`🎧 Listening to events for close login: ${apiBid.id}`); return (apiBid.onCloseLogin = (data) => { // Loại bỏ class hiện có. Tạo tiền đề cho việc tạo đối tượng mới lại MANAGER_BIDS = MANAGER_BIDS.filter( (item) => item.id !== data.id && item.type !== data.type ); addProductTab(data); }); }) ); 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; } } // Thời điểm tracking liên tục const earlyTrackingTime = subtractSeconds( productTab.close_time, productTab?.web_bid?.early_tracking_seconds || 0 ); // Check không mở tab nếu chưa đến giờ if (productTab.close_time && !isTimeReached(earlyTrackingTime)) { console.log( `⏳ [${productTab.id}] Early tracking time not reached yet. ` + `Waiting until ${earlyTrackingTime} (current time: ${new Date().toISOString()})` ); 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(); // Bắn event status login console.log("📊 Tracking login status..."); trackingLoginStatus(); } 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 (!global.IS_CLEANING) { console.log("🚀 Cleaning flag is OFF. Proceeding with operation."); return; } if (!browser) { console.warn("⚠️ Browser is not available or disconnected."); return; } try { const pages = await browser.pages(); console.log("🔍 Found pages:", pages.length); const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [ item.url, ...item.children.map((child) => child.url), ]).filter(Boolean); // product tabs const productTabs = _.flatMap(MANAGER_BIDS, "children"); // for (const item of [...productTabs, ...MANAGER_BIDS]) { // if (!item.page_context) continue; // try { // const avalableResult = await isPageAvailable(item.page_context); // if (!avalableResult) { // await safeClosePage(item); // } // } catch (e) { // console.warn("⚠️ Error checking page_context.title()", e.message); // await safeClosePage(item); // } // } for (const page of pages) { try { if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua const pageUrl = page.url(); if (!pageUrl || pageUrl === "about:blank") continue; if (activeUrls.includes(pageUrl)) { const productTab = productTabs.find((item) => item.url === pageUrl); if (!productTab || !productTab?.close_time) continue; const earlyTrackingTime = subtractSeconds( productTab.close_time, productTab?.web_bid?.early_tracking_seconds || 0 ); if (!isTimeReached(earlyTrackingTime)) { await safeClosePage(productTab); console.log(`🛑 Unused page detected: ${pageUrl}`); continue; } continue; } // remove all listents page.removeAllListeners(); console.log(`🛑 Unused page detected: ${pageUrl}`); const bidData = MANAGER_BIDS.filter((item) => item.page_context) .map((i) => ({ current_url: i.page_context.url(), data: i, })) .find((j) => j.current_url === pageUrl); if (bidData && bidData.data) { await safeClosePage(bidData.data); } else { try { await Promise.race([ page.close(), new Promise((_, reject) => setTimeout(() => reject(new Error("Close timeout")), 3000) ), ]); } catch (closeErr) { console.warn( `⚠️ Error closing page ${pageUrl}: ${closeErr.message}` ); } } console.log(`✅ Closed page: ${pageUrl}`); } catch (pageErr) { console.warn(`⚠️ Error handling page: ${pageErr.message}`); } } // Delete lazy tracking page Promise.allSettled( MANAGER_BIDS.map(async (item) => { if (await item.isLazy()) { safeClosePage(item); } }) ); } 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 ); } }; const trackingLoginStatus = async () => { try { if (!MANAGER_BIDS?.length) return; const results = await Promise.allSettled( MANAGER_BIDS.map(async (item) => { try { if (!isTimeReached(timeToUpdateLogin)) return; const login_status = await item.isLogin(); await updateLoginStatus({ data: { id: item.id, type: item.type, origin_url: item.origin_url, }, login_status, }); // Set time to update login sau 1 phút const now = new Date(); const oneMinuteLater = new Date(now.getTime() + 60 * 1000); timeToUpdateLogin = oneMinuteLater; } catch (err) { console.warn( `[⚠️ WARN] Failed to check login for bid ${ item?.id || "unknown" }: ${err.message}` ); } }) ); // Optional: log summary const failed = results.filter((r) => r.status === "rejected").length; if (failed) { console.warn(`[⚠️ WARN] ${failed} login status checks failed.`); } } catch (error) { console.error( `[❌ ERROR] Login status tracking failed: ${error.message}\n`, error.stack ); } }; (async () => { const socket = io(`${configs.SOCKET_URL}/bid-ws`, { transports: ["websocket"], reconnection: true, extraHeaders: { Authorization: process.env.CLIENT_KEY, }, }); // set socket on global app global.socket = socket; // listen connect socket.on("connect", () => { console.log("✅ Connected to WebSocket server"); console.log("🔗 Socket ID:", socket.id); }); // listen connect socket.on("disconnect", () => { console.log("❌Client key is valid. Disconnected"); }); // 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 webBid = MANAGER_BIDS.find((item) => item?.id === data?.id); if ( webBid && webBid.username === data.username && webBid.password === data.password ) return; const isDeleted = deleteProfile(data); if (isDeleted) { console.log("✅ Profile deleted successfully!"); const tab = MANAGER_BIDS.find((item) => item.url === data.url); if (!tab) return; global.IS_CLEANING = false; await Promise.all(tab.children.map((tab) => safeClosePage(tab))); await safeClosePage(tab); MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id); addProductTab(data); global.IS_CLEANING = true; } else { console.log("⚠️ No profile found to delete."); } }); // AUTO TRACKING tracking(); })();