dev #72

Merged
root merged 2 commits from dev into main 2026-06-01 19:33:50 +03:00
Showing only changes of commit a4644463e4 - Show all commits

View File

@@ -9,8 +9,10 @@
* Tokens are IP-bound — same proxy port must be reused for the API calls.
*
* Warm pool behavior:
* 09:00-19:00 Istanbul → proactive: maintain >= 1 slot, auto-refresh before expiry
* 19:00-09:00 → on-demand only: capture only when needed
* 24/7 → proactive: maintain >= 1 slot, auto-refresh ~9.5min before expiry.
* (Previously gated to 09:00-19:00 Istanbul; the off-hours cold-start cost
* blew the user-facing latency budget — first click ate 30-120s while the
* pool captured a token. Pool is cheap to maintain — keep it warm always.)
*/
import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
@@ -22,7 +24,8 @@ const TOKEN_TTL = 600; // seconds — TWS- has no built-in expiry, refresh aggre
const REFRESH_BUFFER = 90; // Refresh 90s before expiry
const CAPTURE_POLL_INTERVAL = 500; // ms
const CAPTURE_POLL_MAX = 40; // 40 × 500ms = 20s max wait
const PAGE_TIMEOUT = 30_000; // 30s navigation timeout
const PAGE_TIMEOUT = 10_000; // 10s — healthy partner sites load in <5s through
// the proxy; a longer wait only prolongs dead-port connects in capture retries.
const CONTEXT_CLOSE_TIMEOUT = 5_000;
const SITE_COOLDOWN = 10 * 60 * 1000; // 10 min per site
const MAX_POOL_SIZE = 5;
@@ -34,12 +37,12 @@ const RPM_PER_SLOT = 6; // 1 token per 6 req/min
* Widget loads JS → calls /v3/api/proxy/* with x-api-key + supporting X-* headers.
* Each site uses a different proxy port (IP) to avoid rate limiting.
*/
// Order matters: round-robin starts from index 0, so the first site is the
// one a cold pool waits on. e-acca.com is intentionally last because the
// new DataImpulse rotating proxy (74.81.81.81:10000-10999) can't reach it
// — `page.goto` blocks until the 30s navigation timeout instead of failing
// fast like the other sites. Sites that consistently load in <10s through
// the proxy stay at the top so initial JWT capture finishes in seconds.
// Order matters: round-robin starts from index 0, so the first site is the one
// a cold pool waits on. Sites that consistently load in <10s through the proxy
// stay at the top so initial JWT capture finishes in seconds.
// (e-acca.com was previously included last but removed — the current DataImpulse
// rotating proxy 74.81.81.81:10000-10999 cannot reach it; page.goto always
// blocked until PAGE_TIMEOUT instead of failing fast like the other sites.)
const JWT_SITES = [
"https://www.alkatalog.com/cats/#/catalogs",
"https://auto-komplekt.ru/goodvin#/catalogs",
@@ -50,7 +53,6 @@ const JWT_SITES = [
"https://www.autodo.kz/#/catalogs",
"https://avtoman124.ru/goodvin#/catalogs",
"https://flynestauto.com/auto-parts-oem-catalog",
"https://www.e-acca.com/cats/#/catalogs",
];
// DataImpulse proxy defaults (port-based IP rotation)
@@ -339,84 +341,25 @@ export class PartsCatalogsAuthService implements OnModuleInit, OnModuleDestroy {
this.logger.log("JWT pool: all refresh timers cleared");
}
// ─── Business hours scheduling ────────────────────────────
// ─── Pool maintenance gate ────────────────────────────────
/**
* Get current Istanbul hour and minute using Intl.DateTimeFormat.
* This works correctly regardless of the server's local timezone.
* Was the gate for proactive pool maintenance (09:00-19:00 Istanbul).
* Pool is now 24/7 — see file header. Kept as a single switch so call sites
* stay self-documenting and a future ramp-down is a one-line change.
*/
private getIstanbulTime(): { hour: number; minute: number } {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: "Europe/Istanbul",
hour: "numeric",
minute: "numeric",
hour12: false,
}).formatToParts(new Date());
const hourPart = parts.find((p) => p.type === "hour");
const minutePart = parts.find((p) => p.type === "minute");
if (!hourPart || !minutePart) {
// Intl.DateTimeFormat with hour+minute always emits both parts; fall back to UTC if not.
const now = new Date();
return { hour: now.getUTCHours(), minute: now.getUTCMinutes() };
}
const hour = Number.parseInt(hourPart.value, 10);
const minute = Number.parseInt(minutePart.value, 10);
return { hour, minute };
}
private isBusinessHours(): boolean {
const { hour } = this.getIstanbulTime();
return hour >= 9 && hour < 19;
return true;
}
/**
* No-op stub. Previously toggled 09:00/19:00 Istanbul timers that
* captured an initial slot at start and cleared all refresh timers at
* stop. Pool is now 24/7 so neither edge applies. Kept so the
* onModuleInit call site and businessHoursTimer field stay valid.
*/
private scheduleBusinessHours(): void {
if (this.businessHoursTimer) {
clearTimeout(this.businessHoursTimer);
this.businessHoursTimer = null;
}
const { hour, minute } = this.getIstanbulTime();
let delayMs: number;
let nextEvent: string;
if (this.isBusinessHours()) {
// Schedule 19:00 stop
const minsUntil19 = (19 - hour - 1) * 60 + (60 - minute);
delayMs = minsUntil19 * 60 * 1000;
nextEvent = "stop (19:00)";
this.businessHoursTimer = setTimeout(() => {
this.logger.log("JWT pool: business hours ended, timers cleared");
this.clearAllRefreshTimers();
this.scheduleBusinessHours(); // schedule next 09:00 start
}, delayMs);
} else {
// Schedule 09:00 start
let minsUntil9: number;
if (hour >= 19) {
// Same day evening → next day 09:00
minsUntil9 = (24 - hour + 9 - 1) * 60 + (60 - minute);
} else {
// Before 09:00
minsUntil9 = (9 - hour - 1) * 60 + (60 - minute);
}
delayMs = minsUntil9 * 60 * 1000;
nextEvent = "start (09:00)";
this.businessHoursTimer = setTimeout(() => {
this.logger.log("JWT pool: business hours started, capturing initial slot");
this.captureToPool().catch((err) => {
this.logger.error(`Business hours initial capture failed: ${(err as Error).message}`);
});
this.scheduleBusinessHours(); // schedule 19:00 stop
}, delayMs);
}
this.logger.log(
`JWT pool: next ${nextEvent} in ${Math.round(delayMs / 60_000)}min (Istanbul: ${hour}:${String(minute).padStart(2, "0")})`,
);
return;
}
// ─── RPM tracking & dynamic scaling ───────────────────────