straeto 0.1.5 → 0.1.7
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.
- package/README.md +0 -1
- package/package.json +1 -1
- package/src/api.ts +67 -24
package/README.md
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://www.npmjs.com/package/straeto"><img src="https://img.shields.io/npm/v/straeto" alt="npm version" /></a>
|
|
13
13
|
<a href="https://github.com/magtastic/straeto-cli/actions/workflows/ci.yml"><img src="https://github.com/magtastic/straeto-cli/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
14
|
-
<a href="https://www.npmjs.com/package/straeto"><img src="https://img.shields.io/npm/dm/straeto" alt="npm downloads" /></a>
|
|
15
14
|
<a href="https://github.com/magtastic/straeto-cli/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/straeto" alt="license" /></a>
|
|
16
15
|
</p>
|
|
17
16
|
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -8,24 +8,65 @@ const HEADERS = {
|
|
|
8
8
|
Origin: "https://www.straeto.is",
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
const MAX_RETRIES = 3;
|
|
12
|
+
const TIMEOUT_MS = 5000;
|
|
13
|
+
|
|
11
14
|
async function query<T>(
|
|
12
15
|
gql: string,
|
|
13
16
|
variables: Record<string, unknown> | undefined,
|
|
14
17
|
schema: z.ZodType<T>,
|
|
15
18
|
): Promise<T> {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const start = Date.now();
|
|
20
|
+
|
|
21
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
22
|
+
if (Date.now() - start > TIMEOUT_MS) break;
|
|
23
|
+
|
|
24
|
+
if (attempt > 0) {
|
|
25
|
+
const delay = 200 * 2 ** (attempt - 1);
|
|
26
|
+
const remaining = TIMEOUT_MS - (Date.now() - start);
|
|
27
|
+
if (remaining <= 0) break;
|
|
28
|
+
await new Promise((r) => setTimeout(r, Math.min(delay, remaining)));
|
|
29
|
+
}
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(API_URL, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: HEADERS,
|
|
35
|
+
body: JSON.stringify({ query: gql, variables }),
|
|
36
|
+
signal: AbortSignal.timeout(TIMEOUT_MS - (Date.now() - start)),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
40
|
+
|
|
41
|
+
const json = (await res.json()) as { data?: unknown; errors?: { message: string }[] };
|
|
42
|
+
if (json.errors && !json.data)
|
|
43
|
+
throw new Error(json.errors.map((error) => error.message).join(", "));
|
|
44
|
+
if (!json.data) throw new Error("No data returned from API");
|
|
45
|
+
|
|
46
|
+
const result = schema.safeParse(json.data);
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
const issues = result.error.issues
|
|
49
|
+
.map((i) => ` ${i.path.join(".")}: ${i.message}`)
|
|
50
|
+
.join("\n");
|
|
51
|
+
const firstPath = result.error.issues[0]?.path ?? [];
|
|
52
|
+
let sample: unknown = json.data;
|
|
53
|
+
for (const key of firstPath) {
|
|
54
|
+
if (sample != null && typeof sample === "object")
|
|
55
|
+
sample = (sample as Record<string | number, unknown>)[key as string | number];
|
|
56
|
+
}
|
|
57
|
+
const raw = JSON.stringify(sample, null, 2);
|
|
58
|
+
const preview = raw && raw.length > 500 ? `${raw.slice(0, 500)}…` : raw;
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Schema validation failed:\n${issues}\n\nValue at "${firstPath.join(".")}":\n${preview}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return result.data;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (attempt === MAX_RETRIES - 1 || Date.now() - start > TIMEOUT_MS) throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
23
68
|
|
|
24
|
-
|
|
25
|
-
if (json.errors && !json.data)
|
|
26
|
-
throw new Error(json.errors.map((error) => error.message).join(", "));
|
|
27
|
-
if (!json.data) throw new Error("No data returned from API");
|
|
28
|
-
return schema.parse(json.data);
|
|
69
|
+
throw new Error("Strætó is not responding — please try again in a moment.");
|
|
29
70
|
}
|
|
30
71
|
|
|
31
72
|
// --- Schemas ---
|
|
@@ -168,14 +209,16 @@ export function findLastStop(bus: BusLocation, stops: Stop[]): string | undefine
|
|
|
168
209
|
// --- Response schemas ---
|
|
169
210
|
|
|
170
211
|
const BusLocationResponseSchema = z.object({
|
|
171
|
-
BusLocationByRoute: z
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
212
|
+
BusLocationByRoute: z
|
|
213
|
+
.object({
|
|
214
|
+
lastUpdate: z.string(),
|
|
215
|
+
results: z.array(BusLocationSchema),
|
|
216
|
+
})
|
|
217
|
+
.nullable(),
|
|
175
218
|
});
|
|
176
219
|
|
|
177
220
|
const StopsResponseSchema = z.object({
|
|
178
|
-
GtfsStops: z.object({ results: z.array(StopSchema) }),
|
|
221
|
+
GtfsStops: z.object({ results: z.array(StopSchema) }).nullable(),
|
|
179
222
|
});
|
|
180
223
|
|
|
181
224
|
const StopDetailSchema = StopSchema.extend({
|
|
@@ -187,15 +230,15 @@ const StopResponseSchema = z.object({
|
|
|
187
230
|
});
|
|
188
231
|
|
|
189
232
|
const AlertsResponseSchema = z.object({
|
|
190
|
-
Alerts: z.object({ results: z.array(AlertSchema) }),
|
|
233
|
+
Alerts: z.object({ results: z.array(AlertSchema) }).nullable(),
|
|
191
234
|
});
|
|
192
235
|
|
|
193
236
|
const GeocodeResponseSchema = z.object({
|
|
194
|
-
Geocode: z.object({ results: z.array(GeoResultSchema) }),
|
|
237
|
+
Geocode: z.object({ results: z.array(GeoResultSchema) }).nullable(),
|
|
195
238
|
});
|
|
196
239
|
|
|
197
240
|
const TripPlannerResponseSchema = z.object({
|
|
198
|
-
TripPlanner: z.object({ results: z.array(TripItinerarySchema) }),
|
|
241
|
+
TripPlanner: z.object({ results: z.array(TripItinerarySchema) }).nullable(),
|
|
199
242
|
});
|
|
200
243
|
|
|
201
244
|
// --- Queries ---
|
|
@@ -214,7 +257,7 @@ export async function getBusLocations(routes: string[]) {
|
|
|
214
257
|
{ routes },
|
|
215
258
|
BusLocationResponseSchema,
|
|
216
259
|
);
|
|
217
|
-
return data.BusLocationByRoute;
|
|
260
|
+
return data.BusLocationByRoute ?? { lastUpdate: "", results: [] };
|
|
218
261
|
}
|
|
219
262
|
|
|
220
263
|
export async function getStops() {
|
|
@@ -223,7 +266,7 @@ export async function getStops() {
|
|
|
223
266
|
undefined,
|
|
224
267
|
StopsResponseSchema,
|
|
225
268
|
);
|
|
226
|
-
return data.GtfsStops
|
|
269
|
+
return data.GtfsStops?.results ?? [];
|
|
227
270
|
}
|
|
228
271
|
|
|
229
272
|
export async function getStop(id: string) {
|
|
@@ -251,7 +294,7 @@ export async function getAlerts(language: string = "IS") {
|
|
|
251
294
|
{ language },
|
|
252
295
|
AlertsResponseSchema,
|
|
253
296
|
);
|
|
254
|
-
return data.Alerts
|
|
297
|
+
return data.Alerts?.results ?? [];
|
|
255
298
|
}
|
|
256
299
|
|
|
257
300
|
export async function geocode(placesQuery: string) {
|
|
@@ -264,7 +307,7 @@ export async function geocode(placesQuery: string) {
|
|
|
264
307
|
{ placesQuery },
|
|
265
308
|
GeocodeResponseSchema,
|
|
266
309
|
);
|
|
267
|
-
return data.Geocode
|
|
310
|
+
return data.Geocode?.results ?? [];
|
|
268
311
|
}
|
|
269
312
|
|
|
270
313
|
export async function planTrip(opts: {
|
|
@@ -298,5 +341,5 @@ export async function planTrip(opts: {
|
|
|
298
341
|
opts,
|
|
299
342
|
TripPlannerResponseSchema,
|
|
300
343
|
);
|
|
301
|
-
return data.TripPlanner
|
|
344
|
+
return data.TripPlanner?.results ?? [];
|
|
302
345
|
}
|