feat(mileage): add OneOS source priority controls
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import dotenv from 'dotenv';
|
||||
import {
|
||||
DEFAULT_ONEOS_PROTOCOL_PRIORITY,
|
||||
normalizeOneOsProtocol,
|
||||
type OneOsProtocol,
|
||||
} from './source-policy.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -20,6 +25,7 @@ export interface OneOsDailyMileage {
|
||||
dataTime: string | null;
|
||||
calculatedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
sourceProtocol: OneOsProtocol | null;
|
||||
}
|
||||
|
||||
interface OneOsMileageResponse {
|
||||
@@ -42,6 +48,17 @@ interface CacheEntry {
|
||||
const cache = new Map<string, CacheEntry>();
|
||||
const inflight = new Map<string, Promise<OneOsDailyMileage[]>>();
|
||||
const rangeInflight = new Map<string, Promise<Map<string, OneOsDailyMileage[]>>>();
|
||||
let protocolPrioritySupported: boolean | null = null;
|
||||
let protocolFallbackWarned = false;
|
||||
|
||||
function markProtocolPriorityUnsupported(): void {
|
||||
protocolPrioritySupported = false;
|
||||
if (protocolFallbackWarned) return;
|
||||
protocolFallbackWarned = true;
|
||||
console.warn(
|
||||
'[mileage] OneOS does not support protocolPriority/sourceProtocol yet; using the legacy request contract',
|
||||
);
|
||||
}
|
||||
|
||||
function apiConfig(): { baseUrl: string; apiKey: string; timeoutMs: number } {
|
||||
const baseUrl = (process.env.ONEOS_MILEAGE_API_BASE_URL || '').replace(/\/+$/, '');
|
||||
@@ -94,6 +111,9 @@ function normalizeRows(value: unknown, requestedDate: string): OneOsDailyMileage
|
||||
const dataTime = stringField('dataTime', 'statisticTime', 'recordTime');
|
||||
const calculatedAt = stringField('calculatedAt', 'calculationTime');
|
||||
const updatedAt = stringField('updatedAt');
|
||||
const sourceProtocol = normalizeOneOsProtocol(
|
||||
stringField('sourceProtocol', 'protocol', 'dataSource'),
|
||||
);
|
||||
if (!plateNumber || date !== requestedDate) continue;
|
||||
|
||||
const normalized: OneOsDailyMileage = {
|
||||
@@ -106,6 +126,7 @@ function normalizeRows(value: unknown, requestedDate: string): OneOsDailyMileage
|
||||
dataTime,
|
||||
calculatedAt,
|
||||
updatedAt,
|
||||
sourceProtocol,
|
||||
};
|
||||
const existing = best.get(plateNumber);
|
||||
if (!existing || (dailyMileageKm ?? -1) > (existing.dailyMileageKm ?? -1)) {
|
||||
@@ -131,12 +152,18 @@ function normalizeRangeRows(value: unknown, startDate: string, endDate: string):
|
||||
return result;
|
||||
}
|
||||
|
||||
async function requestDate(date: string): Promise<OneOsDailyMileage[]> {
|
||||
async function requestDate(
|
||||
date: string,
|
||||
protocolPriority: OneOsProtocol[],
|
||||
): Promise<OneOsDailyMileage[]> {
|
||||
const { baseUrl, apiKey, timeoutMs } = apiConfig();
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
const includeProtocolPriority = protocolPrioritySupported !== false;
|
||||
const body: Record<string, unknown> = { date };
|
||||
if (includeProtocolPriority) body.protocolPriority = protocolPriority;
|
||||
const response = await fetch(`${baseUrl}${ENDPOINT}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -145,10 +172,18 @@ async function requestDate(date: string): Promise<OneOsDailyMileage[]> {
|
||||
},
|
||||
// Intentionally omit plateNumbers: the API then returns every vehicle
|
||||
// authorized for this application on the requested natural day.
|
||||
body: JSON.stringify({ date }),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
const payload = await response.json().catch(() => null) as OneOsMileageResponse | null;
|
||||
if (
|
||||
includeProtocolPriority &&
|
||||
response.status === 400 &&
|
||||
payload?.code === 'INVALID_REQUEST'
|
||||
) {
|
||||
markProtocolPriorityUnsupported();
|
||||
continue;
|
||||
}
|
||||
if (!response.ok || payload?.code !== 'SUCCESS') {
|
||||
const trace = payload?.traceId ? `, traceId=${payload.traceId}` : '';
|
||||
const error = new Error(
|
||||
@@ -157,6 +192,7 @@ async function requestDate(date: string): Promise<OneOsDailyMileage[]> {
|
||||
if (response.status < 500 || attempt === 2) throw error;
|
||||
lastError = error;
|
||||
} else {
|
||||
if (includeProtocolPriority) protocolPrioritySupported = true;
|
||||
return normalizeRows(payload.data, date);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -175,6 +211,7 @@ async function requestRange(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
plateNumbers: string[],
|
||||
protocolPriority: OneOsProtocol[],
|
||||
): Promise<Map<string, OneOsDailyMileage[]>> {
|
||||
const { baseUrl, apiKey, timeoutMs } = apiConfig();
|
||||
const allRows: unknown[] = [];
|
||||
@@ -192,6 +229,8 @@ async function requestRange(
|
||||
endDate,
|
||||
pageSize: RANGE_PAGE_SIZE,
|
||||
};
|
||||
const includeProtocolPriority = protocolPrioritySupported !== false;
|
||||
if (includeProtocolPriority) body.protocolPriority = protocolPriority;
|
||||
if (plateNumbers.length > 0) body.plateNumbers = plateNumbers;
|
||||
if (cursor) body.cursor = cursor;
|
||||
const response = await fetch(`${baseUrl}${RANGE_ENDPOINT}`, {
|
||||
@@ -204,6 +243,14 @@ async function requestRange(
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
payload = await response.json().catch(() => null) as OneOsMileageRangeResponse | null;
|
||||
if (
|
||||
includeProtocolPriority &&
|
||||
response.status === 400 &&
|
||||
payload?.code === 'INVALID_REQUEST'
|
||||
) {
|
||||
markProtocolPriorityUnsupported();
|
||||
continue;
|
||||
}
|
||||
if (!response.ok || payload?.code !== 'SUCCESS') {
|
||||
const trace = payload?.traceId ? `, traceId=${payload.traceId}` : '';
|
||||
const error = new Error(
|
||||
@@ -212,6 +259,7 @@ async function requestRange(
|
||||
if (response.status < 500 || attempt === 2) throw error;
|
||||
lastError = error;
|
||||
} else {
|
||||
if (includeProtocolPriority) protocolPrioritySupported = true;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -252,26 +300,29 @@ function trimCache(): void {
|
||||
export async function fetchOneOsDailyMileage(
|
||||
date: string,
|
||||
plateNumbers?: string[],
|
||||
protocolPriority: OneOsProtocol[] = DEFAULT_ONEOS_PROTOCOL_PRIORITY,
|
||||
): Promise<OneOsDailyMileage[]> {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error(`Invalid mileage date: ${date}`);
|
||||
|
||||
const hit = cache.get(date);
|
||||
const policyKey = protocolPriority.join('>');
|
||||
const cacheKey = `${date}:${policyKey}`;
|
||||
const hit = cache.get(cacheKey);
|
||||
let rows: OneOsDailyMileage[];
|
||||
if (hit && hit.expiresAt > Date.now()) {
|
||||
rows = hit.rows;
|
||||
} else {
|
||||
let pending = inflight.get(date);
|
||||
let pending = inflight.get(cacheKey);
|
||||
if (!pending) {
|
||||
pending = requestDate(date).then(result => {
|
||||
pending = requestDate(date, protocolPriority).then(result => {
|
||||
const ttl = date === shanghaiDate() ? CURRENT_DAY_TTL_MS : HISTORICAL_TTL_MS;
|
||||
cache.delete(date);
|
||||
cache.delete(cacheKey);
|
||||
if (ttl > 0) {
|
||||
cache.set(date, { rows: result, expiresAt: Date.now() + ttl });
|
||||
cache.set(cacheKey, { rows: result, expiresAt: Date.now() + ttl });
|
||||
trimCache();
|
||||
}
|
||||
return result;
|
||||
}).finally(() => inflight.delete(date));
|
||||
inflight.set(date, pending);
|
||||
}).finally(() => inflight.delete(cacheKey));
|
||||
inflight.set(cacheKey, pending);
|
||||
}
|
||||
rows = await pending;
|
||||
}
|
||||
@@ -284,6 +335,7 @@ export async function fetchOneOsDailyMileage(
|
||||
export async function fetchOneOsMileageDates(
|
||||
dates: string[],
|
||||
plateNumbers?: string[],
|
||||
protocolPriority: OneOsProtocol[] = DEFAULT_ONEOS_PROTOCOL_PRIORITY,
|
||||
): Promise<Map<string, OneOsDailyMileage[]>> {
|
||||
const result = new Map<string, OneOsDailyMileage[]>();
|
||||
const uniqueDates = Array.from(new Set(dates)).sort();
|
||||
@@ -312,10 +364,20 @@ export async function fetchOneOsMileageDates(
|
||||
await Promise.all(groups.map(async group => {
|
||||
const startDate = group[0];
|
||||
const endDate = group[group.length - 1];
|
||||
const key = `${startDate}:${endDate}:${selectedPlates.length > 0 ? selectedPlates.join('\u0000') : '*'}`;
|
||||
const key = [
|
||||
startDate,
|
||||
endDate,
|
||||
selectedPlates.length > 0 ? selectedPlates.join('\u0000') : '*',
|
||||
protocolPriority.join('>'),
|
||||
].join(':');
|
||||
let pending = rangeInflight.get(key);
|
||||
if (!pending) {
|
||||
pending = requestRange(startDate, endDate, selectedPlates).finally(() => rangeInflight.delete(key));
|
||||
pending = requestRange(
|
||||
startDate,
|
||||
endDate,
|
||||
selectedPlates,
|
||||
protocolPriority,
|
||||
).finally(() => rangeInflight.delete(key));
|
||||
rangeInflight.set(key, pending);
|
||||
}
|
||||
const rowsByDate = await pending;
|
||||
|
||||
Reference in New Issue
Block a user