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:
AI Assistant
2026-03-28 10:05:06 +02:00
parent 9bab9db4df
commit 58442da355
2 changed files with 16 additions and 23 deletions
@@ -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;