142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
import * as Sentry from "@sentry/react-native";
|
||
import { observedFetchJson } from "@/lib/observability/network/observedFetch";
|
||
|
||
/**
|
||
* Position Masters – JR Shikoku mock API
|
||
*
|
||
* Fetches the position-master table from the mock API server and provides
|
||
* a lookup helper to convert (PosNum, Line) → Pos text.
|
||
*
|
||
* Used by:
|
||
* - useTrainMenu: fetches on mock-enable, stores in context
|
||
* - useCurrentTrain: fills Pos when mapping mock TrainEntry → trainDataType
|
||
* - webviewXhrInterceptor: bakes lookup into injected JS so WebView can
|
||
* also resolve Pos text client-side
|
||
*/
|
||
|
||
const POSITION_MASTERS_URL =
|
||
'https://jr-shikoku-backend-mock-api-v1.haruk.in/position-masters';
|
||
|
||
const MOCK_TRAIN_POSITIONS_URL =
|
||
'https://jr-shikoku-backend-mock-api-v1.haruk.in/train-positions/current';
|
||
|
||
export interface PositionMaster {
|
||
pos_num: number;
|
||
/** "yosan" | "koutoku" | "tokushima" | "dosan" | "uwajima" | "kubokawa" */
|
||
line: string;
|
||
/** 表示テキスト e.g. "高松", "高松~栗林" */
|
||
pos_text: string;
|
||
pos_type: 'station' | 'between' | 'approaching' | 'yard';
|
||
display_order: number;
|
||
}
|
||
|
||
/** key: `${pos_num}:${line}` → pos_text */
|
||
export type PositionLookup = Map<string, string>;
|
||
|
||
/** Module-level cache (lives for the app session, not persisted). */
|
||
let _cache: PositionMaster[] | null = null;
|
||
let _lastGoodTrainPositions: any[] | null = null;
|
||
let _lastGoodTrainPositionsAt: number | null = null;
|
||
|
||
/**
|
||
* Fetch position masters from the remote API.
|
||
* Results are cached in memory for the session.
|
||
*/
|
||
export const fetchPositionMasters = async (): Promise<PositionMaster[]> => {
|
||
if (_cache) return _cache;
|
||
const data = await observedFetchJson<PositionMaster[]>(POSITION_MASTERS_URL, {
|
||
endpoint: "positions_master",
|
||
source: "mock_api",
|
||
userVisible: false,
|
||
preload: true,
|
||
fetchPriority: "medium",
|
||
timeoutMs: 8000,
|
||
retry: true,
|
||
urlPathTemplate: "/position-masters",
|
||
});
|
||
_cache = data;
|
||
return data;
|
||
};
|
||
|
||
/** Clear the in-memory cache (useful for testing / forced refresh). */
|
||
export const clearPositionMastersCache = () => {
|
||
_cache = null;
|
||
};
|
||
|
||
/**
|
||
* Build a fast Map from the masters array.
|
||
* When multiple records share the same (pos_num, line) pair (different
|
||
* display_order), the one with the lower display_order takes priority.
|
||
*/
|
||
export const buildPosLookup = (masters: PositionMaster[]): PositionLookup => {
|
||
const sorted = [...masters].sort((a, b) => a.display_order - b.display_order);
|
||
const map = new Map<string, string>();
|
||
for (const m of sorted) {
|
||
const key = `${m.pos_num}:${m.line}`;
|
||
if (!map.has(key)) {
|
||
map.set(key, m.pos_text);
|
||
}
|
||
}
|
||
return map;
|
||
};
|
||
|
||
/**
|
||
* Look up the Pos text for a given (PosNum, Line) pair.
|
||
* Returns `undefined` when no match is found.
|
||
*/
|
||
export const lookupPos = (
|
||
posNum: number,
|
||
line: string,
|
||
lookup: PositionLookup,
|
||
): string | undefined => lookup.get(`${posNum}:${line}`);
|
||
|
||
/**
|
||
* Serialize the lookup as a plain JS object literal suitable for embedding
|
||
* into an injected JavaScript string.
|
||
*/
|
||
export const serializePosLookupForJs = (lookup: PositionLookup): string => {
|
||
const entries = Array.from(lookup.entries())
|
||
.map(([k, v]) => `${JSON.stringify(k)}:${JSON.stringify(v)}`)
|
||
.join(',');
|
||
return `{${entries}}`;
|
||
};
|
||
|
||
/**
|
||
* Fetch the latest train positions from the mock API server.
|
||
* Returns an array of TrainEntry objects (GetDateTime sentinel included).
|
||
* Throws on network error or non-OK response.
|
||
*/
|
||
export const fetchMockTrainPositions = async (): Promise<any[]> => {
|
||
try {
|
||
const data = await observedFetchJson<any[]>(MOCK_TRAIN_POSITIONS_URL, {
|
||
endpoint: "positions_current",
|
||
source: "mock_api",
|
||
userVisible: true,
|
||
preload: false,
|
||
fetchPriority: "high",
|
||
timeoutMs: 8000,
|
||
retry: true,
|
||
urlPathTemplate: "/train-positions/current",
|
||
});
|
||
_lastGoodTrainPositions = data;
|
||
_lastGoodTrainPositionsAt = Date.now();
|
||
return data;
|
||
} catch (error) {
|
||
if (_lastGoodTrainPositions) {
|
||
Sentry.addBreadcrumb({
|
||
category: "data_fetch",
|
||
level: "warning",
|
||
message: "fetch:stale_cache",
|
||
data: {
|
||
endpoint: "positions_current",
|
||
source: "mock_api",
|
||
stale: true,
|
||
cacheAgeSeconds: _lastGoodTrainPositionsAt ? Math.round((Date.now() - _lastGoodTrainPositionsAt) / 1000) : undefined,
|
||
},
|
||
});
|
||
return _lastGoodTrainPositions;
|
||
}
|
||
throw error;
|
||
}
|
||
};
|