All files / src/lib geo-client.ts

0% Statements 0/44
100% Branches 0/0
0% Functions 0/3
0% Lines 0/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68                                                                                                                                       
import type { AppConfig } from "../config.js";
 
/** A single [lon, lat] pair (GeoJSON order). */
export type Coord = [number, number];
 
export interface TrackGeometry {
  /** Simplified [lon, lat] polyline (route silhouette). */
  simplified: Coord[];
  /** Resampled elevation (m) for a sparkline, or null if geo-service omitted it. */
  profile: number[] | null;
}
 
/**
 * Best-effort: ask geo-service to analyze a GPX track. Returns the simplified
 * polyline + elevation profile, or null if geo-service is unavailable / errored
 * / slow — the upload must never fail because of this.
 */
export async function fetchTrackGeometry(
  buffer: Buffer,
  config: AppConfig,
): Promise<TrackGeometry | null> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), config.geoTimeoutMs);
 
  try {
    const response = await fetch(`${config.geoServiceUrl}/v1/geo/analyze`, {
      method: "POST",
      headers: {
        "x-service-token": config.serviceToken,
        "content-type": "application/gpx+xml",
      },
      body: new Uint8Array(buffer),
      signal: controller.signal,
    });
 
    if (!response.ok) {
      return null;
    }
 
    const payload = (await response.json()) as { simplified?: unknown; profile?: unknown };
    if (!isCoordArray(payload.simplified)) {
      return null;
    }
    return {
      simplified: payload.simplified,
      profile: isNumberArray(payload.profile) ? payload.profile : null,
    };
  } catch {
    return null;
  } finally {
    clearTimeout(timeout);
  }
}
 
function isCoordArray(value: unknown): value is Coord[] {
  return (
    Array.isArray(value) &&
    value.every(
      (p) =>
        Array.isArray(p) && p.length === 2 && typeof p[0] === "number" && typeof p[1] === "number",
    )
  );
}
 
function isNumberArray(value: unknown): value is number[] {
  return Array.isArray(value) && value.length > 0 && value.every((n) => typeof n === "number");
}