feat: poll mock API server for live train positions
- positionMasters: add fetchMockTrainPositions() targeting /train-positions/latest on the mock API server - useTrainMenu: when mockApiFeatureEnabled and not recording/playing, poll /train-positions/latest every 15s and update mockTrainPositions (same interval as live mode); on mode switch updates immediately - useCurrentTrain: - playing: use local recording data (no network call) - mock ON idle: fetch from mock API server (15s poll driven by useTrainMenu) - mock OFF: fetch from real backend as before - Recording still forces mock OFF so real API data is captured Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
export { generateXhrInterceptorJs, MockApiConfig, TrainEntry } from './webviewXhrInterceptor';
|
||||
export { PositionMaster, PositionLookup, fetchPositionMasters, buildPosLookup, lookupPos } from './positionMasters';
|
||||
export { PositionMaster, PositionLookup, fetchPositionMasters, fetchMockTrainPositions, buildPosLookup, lookupPos } from './positionMasters';
|
||||
|
||||
// Pre-captured sample train position data from the official site
|
||||
import trainJson from './mockData/train.json';
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
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/latest';
|
||||
|
||||
export interface PositionMaster {
|
||||
pos_num: number;
|
||||
/** "yosan" | "koutoku" | "tokushima" | "dosan" | "uwajima" | "kubokawa" */
|
||||
@@ -85,3 +88,14 @@ export const serializePosLookupForJs = (lookup: PositionLookup): string => {
|
||||
.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[]> => {
|
||||
const res = await fetch(MOCK_TRAIN_POSITIONS_URL);
|
||||
if (!res.ok) throw new Error(`mock train-positions fetch failed: ${res.status}`);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { checkDuplicateTrainData } from "@/lib/checkDuplicateTrainData";
|
||||
import { getStationID } from "@/lib/eachTrainInfoCoreLib/getStationData";
|
||||
import { trainDataType } from "@/lib/trainPositionTextArray";
|
||||
import { MOCK_TRAIN_POSITIONS } from "@/lib/mockApi";
|
||||
import { fetchMockTrainPositions } from "@/lib/mockApi/positionMasters";
|
||||
import WebView from "react-native-webview";
|
||||
import { StationProps } from "@/lib/CommonTypes";
|
||||
type loading = "loading" | "success" | "error";
|
||||
@@ -77,7 +78,7 @@ export const CurrentTrainProvider: FC<props> = ({ children }) => {
|
||||
const [currentTrainLoading, setCurrentTrainLoading] =
|
||||
useState<loading>("loading");
|
||||
|
||||
const { mockApiFeatureEnabled, mockTrainPositions, addTrainSnapshot, lookupPosText } = useTrainMenu();
|
||||
const { mockApiFeatureEnabled, mockTrainPositions, addTrainSnapshot, lookupPosText, recorderState } = useTrainMenu();
|
||||
|
||||
const { getInjectJavascriptAddress, stationList, originalStationList } =
|
||||
useStationList();
|
||||
@@ -261,31 +262,54 @@ export const CurrentTrainProvider: FC<props> = ({ children }) => {
|
||||
}
|
||||
};
|
||||
const getCurrentTrain = () => {
|
||||
// モックAPI機能が有効な場合はモックデータを使用
|
||||
if (mockApiFeatureEnabled) {
|
||||
// 再生中: 録画データをそのまま使う(ポーリング不要)
|
||||
if (recorderState === 'playing') {
|
||||
const source = mockTrainPositions ?? MOCK_TRAIN_POSITIONS;
|
||||
const mapped = source
|
||||
.filter((x): x is import("@/lib/mockApi/webviewXhrInterceptor").TrainEntry =>
|
||||
"TrainNum" in x
|
||||
)
|
||||
.map((x) => {
|
||||
// Pos が空の場合は位置マスターから補完
|
||||
const pos = x.Pos || lookupPosText(x.PosNum, x.Line) || '';
|
||||
return {
|
||||
Index: x.Index,
|
||||
num: x.TrainNum,
|
||||
delay: x.delay,
|
||||
Pos: pos,
|
||||
PosNum: x.PosNum,
|
||||
Direction: x.Direction,
|
||||
Type: x.Type,
|
||||
Line: x.Line,
|
||||
};
|
||||
});
|
||||
.map((x) => ({
|
||||
Index: x.Index,
|
||||
num: x.TrainNum,
|
||||
delay: x.delay as "入線" | number,
|
||||
Pos: x.Pos || lookupPosText(x.PosNum, x.Line) || '',
|
||||
PosNum: x.PosNum,
|
||||
Direction: x.Direction,
|
||||
Type: x.Type,
|
||||
Line: x.Line,
|
||||
}));
|
||||
setCurrentTrain(mapped);
|
||||
setCurrentTrainLoading("success");
|
||||
return;
|
||||
}
|
||||
|
||||
// モックON(通常): モックAPIサーバーをポーリング
|
||||
if (mockApiFeatureEnabled) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||
fetchMockTrainPositions()
|
||||
.then((data) => {
|
||||
const entries = data.filter((x: any) => "TrainNum" in x);
|
||||
const mapped = entries.map((x: any) => ({
|
||||
Index: x.Index as number,
|
||||
num: x.TrainNum as string,
|
||||
delay: x.delay as "入線" | number,
|
||||
Pos: (x.Pos || lookupPosText(x.PosNum, x.Line) || '') as string,
|
||||
PosNum: x.PosNum as number,
|
||||
Direction: x.Direction as number,
|
||||
Type: x.Type as string,
|
||||
Line: x.Line as string,
|
||||
}));
|
||||
setCurrentTrain(mapped);
|
||||
setCurrentTrainLoading("success");
|
||||
})
|
||||
.catch(() => {
|
||||
setCurrentTrainLoading("error");
|
||||
})
|
||||
.finally(() => clearTimeout(timeoutId));
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
||||
fetch("https://n8n.haruk.in/webhook/c501550c-7d1b-4e50-927b-4429fe18931a", { signal: controller.signal })
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
FC,
|
||||
} from "react";
|
||||
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
PositionMaster,
|
||||
PositionLookup,
|
||||
fetchPositionMasters,
|
||||
fetchMockTrainPositions,
|
||||
buildPosLookup,
|
||||
lookupPos,
|
||||
} from "../lib/mockApi/positionMasters";
|
||||
@@ -387,10 +389,30 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
|
||||
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV).then((value) => {
|
||||
setBackendApiBaseUrl(getBackendApiBaseUrl(value));
|
||||
}).catch(() => {});
|
||||
// サンプルデータで初期化
|
||||
// 静的サンプルデータで初期化(モックAPIが応答するまでのフォールバック)
|
||||
setMockTrainPositions(MOCK_TRAIN_POSITIONS);
|
||||
}, []);
|
||||
|
||||
// モックAPIポーリング: モックON かつ 録画/再生中でない場合に15秒ごとに更新
|
||||
const fetchAndSetMockPositions = useCallback(() => {
|
||||
if (!mockApiFeatureEnabled || recorderState !== 'idle') return;
|
||||
fetchMockTrainPositions()
|
||||
.then((data) => {
|
||||
const entries = data.filter((x: any) => "TrainNum" in x) as TrainEntry[];
|
||||
setMockTrainPositions(entries);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [mockApiFeatureEnabled, recorderState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mockApiFeatureEnabled || recorderState !== 'idle') return;
|
||||
// 即時取得
|
||||
fetchAndSetMockPositions();
|
||||
// 15秒ごとにポーリング
|
||||
const timer = setInterval(fetchAndSetMockPositions, 15000);
|
||||
return () => clearInterval(timer);
|
||||
}, [mockApiFeatureEnabled, recorderState]);
|
||||
|
||||
return (
|
||||
<TrainMenuContext.Provider
|
||||
value={{
|
||||
|
||||
Reference in New Issue
Block a user