hk-bus-eta 3.8.2 → 3.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/journeyTime.ts +64 -86
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hk-bus-eta",
3
- "version": "3.8.2",
3
+ "version": "3.8.3",
4
4
  "description": "Query the ETA (Estimated Time of Arrival) of HK Bus/Minibus/MTR/Lightrail",
5
5
  "main": "dist/index.js",
6
6
  "module": "esm/index.js",
@@ -1,4 +1,4 @@
1
- import { RouteListEntry, StopList } from "./type";
1
+ import { EtaDb, RouteListEntry, StopList } from "./type";
2
2
  import { formatInTimeZone } from "date-fns-tz";
3
3
 
4
4
  type JT_CACHE = Record<
@@ -11,83 +11,50 @@ type JT_CACHE = Record<
11
11
 
12
12
  const __HK_BUS_ETA_JT_CACHE__: JT_CACHE = {};
13
13
 
14
- async function fetchEstJourneyTimeBasedOnHistoricalData({
15
- route,
16
- startSeq,
17
- endSeq,
18
- }: {
19
- route: RouteListEntry;
20
- startSeq: number;
21
- endSeq: number;
22
- }): Promise<number> {
23
- const requests = [];
24
- const stops = Object.values(route.stops)[0];
14
+ const TIME_BETWEEN_STOPS_BASE_URL =
15
+ "https://raw.githubusercontent.com/HK-Bus-ETA/hk-bus-time-between-stops/refs/heads/pages";
25
16
 
26
- for (let i = startSeq; i < endSeq; ++i) {
27
- const start = stops[i];
28
- const end = stops[i + 1];
29
- const day =
30
- parseInt(formatInTimeZone(new Date(), "Asia/Hong_Kong", "i"), 10) - 1;
31
- const hour = formatInTimeZone(new Date(), "Asia/Hong_Kong", "HH");
32
-
33
- requests.push(
34
- fetch(
35
- `https://raw.githubusercontent.com/HK-Bus-ETA/hk-bus-time-between-stops/refs/heads/pages/times_hourly/${day}/${hour}/${start.slice(0, 2)}.json`,
36
- )
37
- .then((r) => r.json())
38
- .then((r) => {
39
- if (r[start][end]) {
40
- return r[start][end];
41
- }
42
- throw new Error("not found");
43
- }),
44
- );
45
- }
46
-
47
- return Promise.all(requests).then((seconds) =>
48
- Math.ceil(seconds.reduce((acc, cur) => acc + cur, 0) / 60),
49
- );
50
- }
17
+ // hk-bus-time-between-stops names weekday folders by Python's %w (Sunday = 0)
18
+ // and files public holidays under 0 as well
19
+ const getTimesHourlyPath = (holidays: string[], now: Date): string => {
20
+ const day = holidays.includes(
21
+ formatInTimeZone(now, "Asia/Hong_Kong", "yyyyMMdd"),
22
+ )
23
+ ? 0
24
+ : parseInt(formatInTimeZone(now, "Asia/Hong_Kong", "i"), 10) % 7;
25
+ const hour = formatInTimeZone(now, "Asia/Hong_Kong", "HH");
26
+ return `times_hourly/${day}/${hour}`;
27
+ };
51
28
 
52
- async function fetchEstJourneyTimeBasedOnHistoricalAvgData({
53
- route,
54
- startSeq,
55
- endSeq,
29
+ function fetchSecondsBetweenStops({
30
+ path,
31
+ start,
32
+ end,
33
+ signal,
56
34
  }: {
57
- route: RouteListEntry;
58
- startSeq: number;
59
- endSeq: number;
35
+ path: string;
36
+ start: string;
37
+ end: string;
38
+ signal?: AbortSignal | null;
60
39
  }): Promise<number> {
61
- const requests = [];
62
- const stops = Object.values(route.stops)[0];
63
-
64
- for (let i = startSeq; i < endSeq; ++i) {
65
- const start = stops[i];
66
- const end = stops[i + 1];
67
-
68
- requests.push(
69
- fetch(
70
- `https://raw.githubusercontent.com/HK-Bus-ETA/hk-bus-time-between-stops/refs/heads/pages/times/${start.slice(0, 2)}.json`,
71
- )
72
- .then((r) => r.json())
73
- .then((r) => {
74
- if (r[start][end]) {
75
- return r[start][end];
76
- }
77
- throw new Error("not found");
78
- }),
79
- );
80
- }
81
-
82
- return Promise.all(requests).then((seconds) =>
83
- Math.ceil(seconds.reduce((acc, cur) => acc + cur, 0) / 60),
84
- );
40
+ return fetch(
41
+ `${TIME_BETWEEN_STOPS_BASE_URL}/${path}/${start.slice(0, 2)}.json`,
42
+ { signal },
43
+ )
44
+ .then((r) => r.json())
45
+ .then((r) => {
46
+ const seconds = r[start]?.[end];
47
+ if (seconds) {
48
+ return seconds;
49
+ }
50
+ throw new Error("not found");
51
+ });
85
52
  }
86
53
 
87
54
  /**
88
55
  * Fetch Journey time in minute for a route
89
56
  * @param {Object}
90
- * @returns {number} journey time in minute
57
+ * @returns {number} journey time in minute, not rounded
91
58
  */
92
59
  export async function fetchEstJourneyTime({
93
60
  route,
@@ -95,6 +62,7 @@ export async function fetchEstJourneyTime({
95
62
  startSeq,
96
63
  endSeq,
97
64
  batchSize = 4,
65
+ holidays = [],
98
66
  signal,
99
67
  }: {
100
68
  route: RouteListEntry;
@@ -102,6 +70,7 @@ export async function fetchEstJourneyTime({
102
70
  startSeq: number;
103
71
  endSeq: number;
104
72
  batchSize?: number;
73
+ holidays?: EtaDb["holidays"];
105
74
  signal?: AbortSignal | null;
106
75
  }): Promise<number> {
107
76
  const stops = Object.values(route.stops)[0];
@@ -118,7 +87,8 @@ export async function fetchEstJourneyTime({
118
87
 
119
88
  let ts = Date.now();
120
89
  let ret = 0;
121
- let payloads: Array<[string, string, Record<string, number>]> = [];
90
+ const timesHourlyPath = getTimesHourlyPath(holidays, new Date(ts));
91
+ let payloads: Array<[string, string, Record<string, string>]> = [];
122
92
  for (let i = startSeq; i < endSeq; ++i) {
123
93
  payloads.push([
124
94
  `${stops[i]}-${stops[i + 1]}`,
@@ -134,16 +104,16 @@ export async function fetchEstJourneyTime({
134
104
  departIn: Math.round(ret / 15) * 15,
135
105
  }),
136
106
  {
137
- startSeq: i,
138
- endSeq: i + 1,
107
+ start: stops[i],
108
+ end: stops[i + 1],
139
109
  },
140
110
  ]);
141
111
  if (payloads.length < batchSize && i !== endSeq - 1) {
142
112
  // skip fetching until whole batch filled
143
113
  continue;
144
114
  }
145
- const seconds = await Promise.all(
146
- payloads.map(([key, payload, { startSeq, endSeq }]) => {
115
+ const minutes = await Promise.all(
116
+ payloads.map(([key, payload, { start, end }]) => {
147
117
  // load from cache if it is query within 15 minutes
148
118
  if (
149
119
  key in __HK_BUS_ETA_JT_CACHE__ &&
@@ -152,18 +122,23 @@ export async function fetchEstJourneyTime({
152
122
  return Promise.resolve(__HK_BUS_ETA_JT_CACHE__[key].s);
153
123
  }
154
124
 
155
- return fetchEstJourneyTimeBasedOnHistoricalData({
156
- route,
157
- startSeq,
158
- endSeq,
125
+ // sum in seconds and convert once; rounding each stop-to-stop segment
126
+ // up to a whole minute overestimates by ~0.5 minute per stop
127
+ return fetchSecondsBetweenStops({
128
+ path: timesHourlyPath,
129
+ start,
130
+ end,
131
+ signal,
159
132
  })
160
133
  .catch(() =>
161
- fetchEstJourneyTimeBasedOnHistoricalAvgData({
162
- route,
163
- startSeq,
164
- endSeq,
134
+ fetchSecondsBetweenStops({
135
+ path: "times",
136
+ start,
137
+ end,
138
+ signal,
165
139
  }),
166
140
  )
141
+ .then((seconds) => seconds / 60)
167
142
  .catch(() =>
168
143
  fetch("https://tdas-api.hkemobility.gov.hk/tdas/api/route", {
169
144
  method: "POST",
@@ -194,8 +169,11 @@ export async function fetchEstJourneyTime({
194
169
  .map((v: string) => parseInt(v, 10));
195
170
  return hh * 60 + mm;
196
171
  })
197
- .catch(() => {
198
- // for any error, assume 4 minutes journey time blindly
172
+ .catch((e) => {
173
+ if (signal?.aborted) {
174
+ throw e;
175
+ }
176
+ // for any other error, assume 4 minutes journey time blindly
199
177
  return 4;
200
178
  })
201
179
  .then((s) => {
@@ -206,8 +184,8 @@ export async function fetchEstJourneyTime({
206
184
  );
207
185
  }),
208
186
  );
209
- seconds.forEach((s) => {
210
- ret += s;
187
+ minutes.forEach((m) => {
188
+ ret += m;
211
189
  });
212
190
  payloads.length = 0;
213
191
  }