Files
jrshikoku/lib/mockApi/positionMasters.ts

142 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
};