@aeroswarm/client 0.1.13 → 0.1.15

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/dist/index.js CHANGED
@@ -314,6 +314,90 @@ function createAeroTrpcClient(endpoint, token) {
314
314
  });
315
315
  }
316
316
 
317
+ // src/sdk/api/response.ts
318
+ var ENVELOPE_HEADER = "x-aero-envelope";
319
+ var STATUS_CODES = [
320
+ [401, "UNAUTHORIZED"],
321
+ [403, "FORBIDDEN"],
322
+ [404, "NOT_FOUND"],
323
+ [400, "BAD_REQUEST"],
324
+ [409, "CONFLICT"],
325
+ [429, "RATE_LIMITED"],
326
+ [503, "UNAVAILABLE"],
327
+ [500, "INTERNAL"]
328
+ ];
329
+ function codeForStatus(status) {
330
+ const hit = STATUS_CODES.find(([s]) => s === status);
331
+ if (hit) return hit[1];
332
+ return status >= 500 ? "INTERNAL" : "BAD_REQUEST";
333
+ }
334
+ var AeroApiError = class extends Error {
335
+ status;
336
+ code;
337
+ /** Retrying later may succeed (rate limit, server busy or failing). */
338
+ recoverable;
339
+ details;
340
+ constructor(init) {
341
+ super(init.message);
342
+ this.name = "AeroApiError";
343
+ this.status = init.status;
344
+ this.code = init.code;
345
+ this.recoverable = init.recoverable;
346
+ this.details = init.details;
347
+ }
348
+ };
349
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
350
+ function isEnvelope(body) {
351
+ return isObject(body) && body.ok === true && "data" in body;
352
+ }
353
+ function isFailure(body) {
354
+ return isObject(body) && body.ok === false;
355
+ }
356
+ function errorFrom(res, body, text, label) {
357
+ const prefix = label ? `${label} \u2192 ${res.status}` : `HTTP ${res.status}`;
358
+ const failure = isFailure(body) ? body : void 0;
359
+ const e = failure?.error;
360
+ if (isObject(e)) {
361
+ const code = typeof e.code === "string" ? e.code : codeForStatus(res.status);
362
+ const message2 = typeof e.message === "string" ? e.message : res.statusText || prefix;
363
+ return new AeroApiError({
364
+ status: res.status,
365
+ code,
366
+ message: `${prefix}: ${message2}`,
367
+ recoverable: typeof e.recoverable === "boolean" ? e.recoverable : res.status >= 500,
368
+ details: e.details
369
+ });
370
+ }
371
+ const message = typeof e === "string" ? e : text.slice(0, 200) || res.statusText;
372
+ return new AeroApiError({
373
+ status: res.status,
374
+ code: codeForStatus(res.status),
375
+ message: message ? `${prefix}: ${message}` : prefix,
376
+ recoverable: res.status >= 500 || res.status === 429,
377
+ details: failure?.detail
378
+ });
379
+ }
380
+ async function readApiBody(res, label) {
381
+ const text = await res.text();
382
+ let body;
383
+ try {
384
+ body = text ? JSON.parse(text) : void 0;
385
+ } catch {
386
+ body = void 0;
387
+ }
388
+ if (!res.ok || isFailure(body)) throw errorFrom(res, body, text, label);
389
+ return body;
390
+ }
391
+ async function parseApiEnvelope(res, label) {
392
+ const body = await readApiBody(res, label);
393
+ if (!isEnvelope(body)) return { data: body };
394
+ const meta = body.meta;
395
+ return { data: body.data, ...isObject(meta) && { meta } };
396
+ }
397
+ async function parseApiResponse(res, label) {
398
+ return (await parseApiEnvelope(res, label)).data;
399
+ }
400
+
317
401
  // src/sdk/api/rest.ts
318
402
  var AeroRestClient = class {
319
403
  constructor(endpoint, token) {
@@ -331,8 +415,7 @@ var AeroRestClient = class {
331
415
  },
332
416
  body: body ? JSON.stringify(body) : void 0
333
417
  });
334
- if (!res.ok) throw new Error(`${method} ${path} \u2192 ${res.status}`);
335
- return res.json();
418
+ return await readApiBody(res, `${method} ${path}`);
336
419
  }
337
420
  createSource(body) {
338
421
  return this.req("POST", "/v1/sources", body);
@@ -551,12 +634,16 @@ var AeroSwarm = class _AeroSwarm {
551
634
  return null;
552
635
  }
553
636
  }
554
- async federatedFetch(base, path, query) {
637
+ async federatedRequest(base, path, query) {
555
638
  const url = new URL(`${base}${path}`);
556
639
  for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
557
- const res = await fetch(url, { headers: { Authorization: `Bearer ${this.opts.token}` } });
558
- if (!res.ok) throw new Error(`Federated request failed: ${res.status} ${res.statusText} (${url.pathname})`);
559
- return res.json();
640
+ const res = await fetch(url, {
641
+ headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
642
+ });
643
+ return parseApiEnvelope(res, `Federated request ${url.pathname}`);
644
+ }
645
+ async federatedFetch(base, path, query) {
646
+ return (await this.federatedRequest(base, path, query)).data;
560
647
  }
561
648
  tracks = {
562
649
  subscribe: (opts, callbacks) => {
@@ -647,6 +734,42 @@ var AeroSwarm = class _AeroSwarm {
647
734
  });
648
735
  }
649
736
  return this.trpc.tracks.history.query(input);
737
+ },
738
+ /**
739
+ * The path an aircraft has flown, ready to draw: a GeoJSON line plus a point for every time
740
+ * mark, with the label already written in `tz`. By default the whole flight so far; `fromMs`
741
+ * and `toMs` choose a period, `sinceMs` fetches only what is new (see `createTrailPoller`),
742
+ * `stepM` asks for a coarser line. The airport's backend builds it (memory first, history
743
+ * as a fallback), so nothing is computed here. There is no AeroSwarm-side equivalent:
744
+ * pass `endpoint` or an `airportId` with a registered `historyEndpoint`.
745
+ */
746
+ trail: async (input) => {
747
+ const base = await this.resolveHistoryEndpoint(input.airportId, input.endpoint);
748
+ if (!base) {
749
+ throw new AeroApiError({
750
+ status: 400,
751
+ code: "BAD_REQUEST",
752
+ recoverable: false,
753
+ message: "tracks.trail needs the airport's backend: pass `endpoint`, or an `airportId` that has a historyEndpoint"
754
+ });
755
+ }
756
+ const num = (v) => v === void 0 ? void 0 : String(v);
757
+ const { data, meta } = await this.federatedRequest(
758
+ base,
759
+ `/tracks/${encodeURIComponent(input.trackId)}/trail`,
760
+ {
761
+ fromMs: num(input.fromMs),
762
+ toMs: num(input.toMs),
763
+ sinceMs: num(input.sinceMs),
764
+ stepM: num(input.stepM),
765
+ limit: num(input.limit),
766
+ labelEveryMs: num(input.labelEveryMs),
767
+ tz: input.tz,
768
+ labelFormat: input.labelFormat,
769
+ format: "geojson"
770
+ }
771
+ );
772
+ return { trail: data, ...meta && { meta } };
650
773
  }
651
774
  };
652
775
  /** Flight plans received from AFTN feeds. Live tracks that correlate with a
@@ -1091,19 +1214,130 @@ var Motion = class {
1091
1214
  function createTrackMotion(options = {}) {
1092
1215
  return new Motion(options);
1093
1216
  }
1217
+
1218
+ // src/shared/source/index.ts
1219
+ function sourceLabel(source) {
1220
+ switch (source) {
1221
+ case "ADSB":
1222
+ case "ASTERIX_CAT021":
1223
+ return "ADSB";
1224
+ case "ASTERIX_CAT010":
1225
+ case "ASTERIX_CAT011":
1226
+ return "SURFACE";
1227
+ case "ASTERIX_CAT020":
1228
+ return "MLAT";
1229
+ case "ASTERIX_CAT048":
1230
+ return "RADAR";
1231
+ case "ASTERIX_CAT062":
1232
+ return "SYSTEM";
1233
+ case "FUSED":
1234
+ return "FUSED";
1235
+ default: {
1236
+ const unreachable = source;
1237
+ return unreachable;
1238
+ }
1239
+ }
1240
+ }
1241
+ function formatDataSource(track) {
1242
+ const kind = sourceLabel(track.source);
1243
+ const ds = track.dataSource;
1244
+ return ds ? `${kind} ${ds.sac}/${ds.sic}` : kind;
1245
+ }
1246
+
1247
+ // src/shared/trail/index.ts
1248
+ var lineOf = (fc) => fc.features.find((f) => f.properties.kind === "trail");
1249
+ var marksOf = (fc) => fc.features.filter((f) => f.properties.kind === "mark");
1250
+ function trailEnd(trail) {
1251
+ const line = lineOf(trail);
1252
+ return line && line.geometry.coordinates.length > 0 ? line.properties.to : void 0;
1253
+ }
1254
+ function mergeTrail(prev, delta) {
1255
+ if (!prev) return delta;
1256
+ const before = lineOf(prev);
1257
+ const added = lineOf(delta);
1258
+ if (!before || !added) return prev;
1259
+ if (added.geometry.coordinates.length === 0) return prev;
1260
+ const seen = new Set(marksOf(prev).map((m) => m.properties.t));
1261
+ const newMarks = marksOf(delta).filter((m) => !seen.has(m.properties.t));
1262
+ const line = {
1263
+ ...before,
1264
+ properties: { ...before.properties, to: added.properties.to, stepM: Math.max(before.properties.stepM, added.properties.stepM) },
1265
+ geometry: { type: "LineString", coordinates: [...before.geometry.coordinates, ...added.geometry.coordinates] }
1266
+ };
1267
+ return { type: "FeatureCollection", features: [line, ...marksOf(prev), ...newMarks] };
1268
+ }
1269
+ var isGone = (err) => typeof err === "object" && err !== null && err.code === "NOT_FOUND";
1270
+ function createTrailPoller(fetchTrail, options) {
1271
+ const pollMs = options.pollMs ?? 5e3;
1272
+ const stopWhenGone = options.stopWhenGone ?? true;
1273
+ let current;
1274
+ let stopped = false;
1275
+ let timer;
1276
+ let running;
1277
+ const schedule = () => {
1278
+ if (stopped || pollMs <= 0) return;
1279
+ timer = setTimeout(() => void tick(), pollMs);
1280
+ };
1281
+ async function once() {
1282
+ try {
1283
+ const since = current ? trailEnd(current) : void 0;
1284
+ const { trail } = await fetchTrail(since);
1285
+ if (stopped) return;
1286
+ const next = mergeTrail(current, trail);
1287
+ if (next !== current) {
1288
+ current = next;
1289
+ options.onUpdate(next);
1290
+ }
1291
+ } catch (err) {
1292
+ if (stopped) return;
1293
+ options.onError?.(err);
1294
+ if (stopWhenGone && isGone(err)) stopped = true;
1295
+ }
1296
+ }
1297
+ function tick() {
1298
+ if (stopped) return Promise.resolve();
1299
+ running ??= once().finally(() => {
1300
+ running = void 0;
1301
+ schedule();
1302
+ });
1303
+ return running;
1304
+ }
1305
+ void tick();
1306
+ return {
1307
+ stop() {
1308
+ stopped = true;
1309
+ if (timer) clearTimeout(timer);
1310
+ },
1311
+ async refresh() {
1312
+ if (timer) clearTimeout(timer);
1313
+ await (running ?? tick());
1314
+ },
1315
+ current: () => current
1316
+ };
1317
+ }
1094
1318
  export {
1319
+ AeroApiError,
1095
1320
  AeroReplayClient,
1096
1321
  AeroSwarm,
1097
1322
  AeroWsClient,
1098
1323
  LABEL_COLORS,
1099
1324
  TrackCache,
1100
1325
  clamp,
1326
+ codeForStatus,
1101
1327
  createTrackMotion,
1328
+ createTrailPoller,
1329
+ formatDataSource,
1102
1330
  getLabelCategory,
1103
1331
  haversineKm,
1104
1332
  isInsideBbox,
1105
1333
  isRenderableTrack2 as isRenderableTrack,
1334
+ mergeTrail,
1335
+ parseApiEnvelope,
1336
+ parseApiResponse,
1337
+ readApiBody,
1338
+ sourceLabel,
1106
1339
  toCurrentTrack,
1340
+ trailEnd,
1107
1341
  withLabel
1108
1342
  };
1109
1343
  //# sourceMappingURL=index.js.map