dev #72
@@ -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 ───────────────────────
|
||||
|
||||
Reference in New Issue
Block a user