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.
Files changed (3) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "straeto",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Strætó — Icelandic bus system CLI",
5
5
  "author": "magtastic",
6
6
  "license": "MIT",
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 res = await fetch(API_URL, {
17
- method: "POST",
18
- headers: HEADERS,
19
- body: JSON.stringify({ query: gql, variables }),
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
- if (!res.ok) throw new Error(`API error: ${res.status}`);
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
- const json = (await res.json()) as { data?: unknown; errors?: { message: string }[] };
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.object({
172
- lastUpdate: z.string(),
173
- results: z.array(BusLocationSchema),
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.results;
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.results;
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.results;
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.results;
344
+ return data.TripPlanner?.results ?? [];
302
345
  }