fix(parcel-sync): fix session expiry during long pagination (Cluj 0 features bug)
Three bugs caused sync to return 0 features after 37 minutes: 1. reloginAttempted was instance-level flag — once set to true after first 401, all subsequent 401s threw immediately without retry. Moved to per-request scope so each request can independently relogin on 401. 2. Session lastUsed never updated during pagination — after ~10 min of paginating, the session store considered it expired and cleanup could evict it. Added touchSession() call before every request. 3. Single eTerra client shared across all cities/steps for hours — now creates a fresh client per city/step (session cache still avoids unnecessary logins when session is valid). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -130,7 +130,7 @@ export class EterraClient {
|
||||
private maxRetries: number;
|
||||
private username: string;
|
||||
private password: string;
|
||||
private reloginAttempted = false;
|
||||
private cacheKey: string;
|
||||
private layerFieldsCache = new Map<string, string[]>();
|
||||
|
||||
private constructor(
|
||||
@@ -147,6 +147,7 @@ export class EterraClient {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.maxRetries = maxRetries;
|
||||
this.cacheKey = makeCacheKey(username, password);
|
||||
}
|
||||
|
||||
/* ---- Factory --------------------------------------------------- */
|
||||
@@ -919,8 +920,7 @@ export class EterraClient {
|
||||
);
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
if (err?.response?.status === 401 && !this.reloginAttempted) {
|
||||
this.reloginAttempted = true;
|
||||
if (err?.response?.status === 401) {
|
||||
await this.login(this.username, this.password);
|
||||
response = await this.requestWithRetry(() =>
|
||||
this.client.get(url, { timeout: this.timeoutMs }),
|
||||
@@ -984,23 +984,28 @@ export class EterraClient {
|
||||
);
|
||||
}
|
||||
|
||||
/** Touch session TTL in global store (prevents expiry during long pagination) */
|
||||
private touchSession(): void {
|
||||
const cached = sessionStore.get(this.cacheKey);
|
||||
if (cached) cached.lastUsed = Date.now();
|
||||
}
|
||||
|
||||
private async requestJson(
|
||||
request: () => Promise<{
|
||||
data: EsriQueryResponse | string;
|
||||
status: number;
|
||||
}>,
|
||||
): Promise<EsriQueryResponse> {
|
||||
this.touchSession();
|
||||
let response;
|
||||
try {
|
||||
response = await this.requestWithRetry(request);
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
if (err?.response?.status === 401 && !this.reloginAttempted) {
|
||||
this.reloginAttempted = true;
|
||||
if (err?.response?.status === 401) {
|
||||
// Always attempt relogin on 401 (session may expire multiple times during long syncs)
|
||||
await this.login(this.username, this.password);
|
||||
response = await this.requestWithRetry(request);
|
||||
} else if (err?.response?.status === 401) {
|
||||
throw new Error("Session expired (401)");
|
||||
} else throw error;
|
||||
}
|
||||
const data = response.data as EsriQueryResponse | string;
|
||||
@@ -1019,17 +1024,15 @@ export class EterraClient {
|
||||
private async requestRaw<T = any>(
|
||||
request: () => Promise<{ data: T | string; status: number }>,
|
||||
): Promise<T> {
|
||||
this.touchSession();
|
||||
let response;
|
||||
try {
|
||||
response = await this.requestWithRetry(request);
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
if (err?.response?.status === 401 && !this.reloginAttempted) {
|
||||
this.reloginAttempted = true;
|
||||
if (err?.response?.status === 401) {
|
||||
await this.login(this.username, this.password);
|
||||
response = await this.requestWithRetry(request);
|
||||
} else if (err?.response?.status === 401) {
|
||||
throw new Error("Session expired (401)");
|
||||
} else throw error;
|
||||
}
|
||||
const data = response.data as T | string;
|
||||
|
||||
@@ -299,17 +299,6 @@ export async function runWeekendDeepSync(): Promise<void> {
|
||||
`[weekend-sync] Sesiune #${state.totalSessions} pornita. ${state.cities.length} orase in coada.`,
|
||||
);
|
||||
|
||||
// Create eTerra client (shared across steps)
|
||||
let client: EterraClient;
|
||||
try {
|
||||
client = await EterraClient.create(username, password);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[weekend-sync] Nu se poate conecta la eTerra: ${msg}`);
|
||||
await saveState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort cities: priority first, then shuffle within same priority
|
||||
const sorted = [...state.cities].sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
@@ -348,9 +337,10 @@ export async function runWeekendDeepSync(): Promise<void> {
|
||||
await sleep(pause);
|
||||
}
|
||||
|
||||
// Execute step
|
||||
// Execute step — fresh client per step (sessions expire after ~10 min)
|
||||
console.log(`[weekend-sync] ${city.name}: ${stepName}...`);
|
||||
try {
|
||||
const client = await EterraClient.create(username, password);
|
||||
const result = await executeStep(city, stepName, client);
|
||||
city.steps[stepName] = result.success ? "done" : "error";
|
||||
if (!result.success) city.errorMessage = result.message;
|
||||
|
||||
Reference in New Issue
Block a user