@aeroswarm/client 0.1.14 → 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.
@@ -392,9 +392,11 @@ async function readApiBody(res, label) {
392
392
  if (!res.ok || isFailure(body)) throw errorFrom(res, body, text, label);
393
393
  return body;
394
394
  }
395
- async function parseApiResponse(res, label) {
395
+ async function parseApiEnvelope(res, label) {
396
396
  const body = await readApiBody(res, label);
397
- return isEnvelope(body) ? body.data : body;
397
+ if (!isEnvelope(body)) return { data: body };
398
+ const meta = body.meta;
399
+ return { data: body.data, ...isObject(meta) && { meta } };
398
400
  }
399
401
 
400
402
  // src/sdk/api/rest.ts
@@ -633,13 +635,16 @@ var AeroSwarm = class _AeroSwarm {
633
635
  return null;
634
636
  }
635
637
  }
636
- async federatedFetch(base, path, query) {
638
+ async federatedRequest(base, path, query) {
637
639
  const url = new URL(`${base}${path}`);
638
640
  for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
639
641
  const res = await fetch(url, {
640
642
  headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
641
643
  });
642
- return parseApiResponse(res, `Federated request ${url.pathname}`);
644
+ return parseApiEnvelope(res, `Federated request ${url.pathname}`);
645
+ }
646
+ async federatedFetch(base, path, query) {
647
+ return (await this.federatedRequest(base, path, query)).data;
643
648
  }
644
649
  tracks = {
645
650
  subscribe: (opts, callbacks) => {
@@ -730,6 +735,42 @@ var AeroSwarm = class _AeroSwarm {
730
735
  });
731
736
  }
732
737
  return this.trpc.tracks.history.query(input);
738
+ },
739
+ /**
740
+ * The path an aircraft has flown, ready to draw: a GeoJSON line plus a point for every time
741
+ * mark, with the label already written in `tz`. By default the whole flight so far; `fromMs`
742
+ * and `toMs` choose a period, `sinceMs` fetches only what is new (see `createTrailPoller`),
743
+ * `stepM` asks for a coarser line. The airport's backend builds it (memory first, history
744
+ * as a fallback), so nothing is computed here. There is no AeroSwarm-side equivalent:
745
+ * pass `endpoint` or an `airportId` with a registered `historyEndpoint`.
746
+ */
747
+ trail: async (input) => {
748
+ const base = await this.resolveHistoryEndpoint(input.airportId, input.endpoint);
749
+ if (!base) {
750
+ throw new AeroApiError({
751
+ status: 400,
752
+ code: "BAD_REQUEST",
753
+ recoverable: false,
754
+ message: "tracks.trail needs the airport's backend: pass `endpoint`, or an `airportId` that has a historyEndpoint"
755
+ });
756
+ }
757
+ const num = (v) => v === void 0 ? void 0 : String(v);
758
+ const { data, meta } = await this.federatedRequest(
759
+ base,
760
+ `/tracks/${encodeURIComponent(input.trackId)}/trail`,
761
+ {
762
+ fromMs: num(input.fromMs),
763
+ toMs: num(input.toMs),
764
+ sinceMs: num(input.sinceMs),
765
+ stepM: num(input.stepM),
766
+ limit: num(input.limit),
767
+ labelEveryMs: num(input.labelEveryMs),
768
+ tz: input.tz,
769
+ labelFormat: input.labelFormat,
770
+ format: "geojson"
771
+ }
772
+ );
773
+ return { trail: data, ...meta && { meta } };
733
774
  }
734
775
  };
735
776
  /** Flight plans received from AFTN feeds. Live tracks that correlate with a
@@ -900,8 +941,135 @@ function AeroSwarmProvider({
900
941
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: qc, children: /* @__PURE__ */ jsx(AeroSwarmContext.Provider, { value: { client }, children }) });
901
942
  }
902
943
 
944
+ // src/react/hooks/use-track-trail.ts
945
+ import { useEffect, useRef, useState } from "react";
946
+
947
+ // src/shared/trail/index.ts
948
+ var lineOf = (fc) => fc.features.find((f) => f.properties.kind === "trail");
949
+ var marksOf = (fc) => fc.features.filter((f) => f.properties.kind === "mark");
950
+ function trailEnd(trail) {
951
+ const line = lineOf(trail);
952
+ return line && line.geometry.coordinates.length > 0 ? line.properties.to : void 0;
953
+ }
954
+ function mergeTrail(prev, delta) {
955
+ if (!prev) return delta;
956
+ const before = lineOf(prev);
957
+ const added = lineOf(delta);
958
+ if (!before || !added) return prev;
959
+ if (added.geometry.coordinates.length === 0) return prev;
960
+ const seen = new Set(marksOf(prev).map((m) => m.properties.t));
961
+ const newMarks = marksOf(delta).filter((m) => !seen.has(m.properties.t));
962
+ const line = {
963
+ ...before,
964
+ properties: { ...before.properties, to: added.properties.to, stepM: Math.max(before.properties.stepM, added.properties.stepM) },
965
+ geometry: { type: "LineString", coordinates: [...before.geometry.coordinates, ...added.geometry.coordinates] }
966
+ };
967
+ return { type: "FeatureCollection", features: [line, ...marksOf(prev), ...newMarks] };
968
+ }
969
+ var isGone = (err) => typeof err === "object" && err !== null && err.code === "NOT_FOUND";
970
+ function createTrailPoller(fetchTrail, options) {
971
+ const pollMs = options.pollMs ?? 5e3;
972
+ const stopWhenGone = options.stopWhenGone ?? true;
973
+ let current;
974
+ let stopped = false;
975
+ let timer;
976
+ let running;
977
+ const schedule = () => {
978
+ if (stopped || pollMs <= 0) return;
979
+ timer = setTimeout(() => void tick(), pollMs);
980
+ };
981
+ async function once() {
982
+ try {
983
+ const since = current ? trailEnd(current) : void 0;
984
+ const { trail } = await fetchTrail(since);
985
+ if (stopped) return;
986
+ const next = mergeTrail(current, trail);
987
+ if (next !== current) {
988
+ current = next;
989
+ options.onUpdate(next);
990
+ }
991
+ } catch (err) {
992
+ if (stopped) return;
993
+ options.onError?.(err);
994
+ if (stopWhenGone && isGone(err)) stopped = true;
995
+ }
996
+ }
997
+ function tick() {
998
+ if (stopped) return Promise.resolve();
999
+ running ??= once().finally(() => {
1000
+ running = void 0;
1001
+ schedule();
1002
+ });
1003
+ return running;
1004
+ }
1005
+ void tick();
1006
+ return {
1007
+ stop() {
1008
+ stopped = true;
1009
+ if (timer) clearTimeout(timer);
1010
+ },
1011
+ async refresh() {
1012
+ if (timer) clearTimeout(timer);
1013
+ await (running ?? tick());
1014
+ },
1015
+ current: () => current
1016
+ };
1017
+ }
1018
+
1019
+ // src/react/hooks/use-track-trail.ts
1020
+ function useTrackTrail(trackId, options = {}) {
1021
+ const { client } = useAeroSwarmContext();
1022
+ const [trail, setTrail] = useState(void 0);
1023
+ const [loading, setLoading] = useState(false);
1024
+ const [error, setError] = useState(void 0);
1025
+ const optionsRef = useRef(options);
1026
+ optionsRef.current = options;
1027
+ const enabled = options.enabled !== false && !!trackId;
1028
+ const key = JSON.stringify([
1029
+ trackId,
1030
+ options.endpoint,
1031
+ options.airportId,
1032
+ options.fromMs,
1033
+ options.toMs,
1034
+ options.stepM,
1035
+ options.limit,
1036
+ options.labelEveryMs,
1037
+ options.tz,
1038
+ options.labelFormat,
1039
+ options.pollMs
1040
+ ]);
1041
+ useEffect(() => {
1042
+ setTrail(void 0);
1043
+ setError(void 0);
1044
+ if (!enabled || !trackId) {
1045
+ setLoading(false);
1046
+ return;
1047
+ }
1048
+ setLoading(true);
1049
+ const { enabled: _enabled, pollMs, ...request } = optionsRef.current;
1050
+ const poller = createTrailPoller(
1051
+ (sinceMs) => client.tracks.trail({ ...request, trackId, sinceMs }),
1052
+ {
1053
+ // A fixed period in the past does not grow: ask once.
1054
+ pollMs: request.toMs !== void 0 ? 0 : pollMs,
1055
+ onUpdate: (t) => {
1056
+ setTrail(t);
1057
+ setLoading(false);
1058
+ setError(void 0);
1059
+ },
1060
+ onError: (e) => {
1061
+ setError(e);
1062
+ setLoading(false);
1063
+ }
1064
+ }
1065
+ );
1066
+ return () => poller.stop();
1067
+ }, [client, enabled, key]);
1068
+ return { trail, loading, error };
1069
+ }
1070
+
903
1071
  // src/react/hooks/use-track-motion.ts
904
- import { useEffect, useMemo as useMemo2, useRef } from "react";
1072
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
905
1073
 
906
1074
  // src/shared/motion/index.ts
907
1075
  var KT_TO_MS = 0.514444;
@@ -1210,10 +1378,10 @@ function createTrackMotion(options = {}) {
1210
1378
  // src/react/hooks/use-track-motion.ts
1211
1379
  function useTrackMotion(options) {
1212
1380
  const { onFrame, ...motionOptions } = options;
1213
- const onFrameRef = useRef(onFrame);
1381
+ const onFrameRef = useRef2(onFrame);
1214
1382
  onFrameRef.current = onFrame;
1215
1383
  const motion = useMemo2(() => createTrackMotion(motionOptions), []);
1216
- useEffect(() => {
1384
+ useEffect2(() => {
1217
1385
  const stop = motion.start((frame) => onFrameRef.current(frame));
1218
1386
  return () => {
1219
1387
  stop();
@@ -1224,7 +1392,7 @@ function useTrackMotion(options) {
1224
1392
  }
1225
1393
 
1226
1394
  // src/react/hooks/use-track-history.ts
1227
- import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
1395
+ import { useCallback, useEffect as useEffect3, useRef as useRef3 } from "react";
1228
1396
  var DR_MIN_SPEED_KT = 10;
1229
1397
  var MAX_SPEED_KT = 1200;
1230
1398
  var GAP_RESET_MS = 3e4;
@@ -1244,22 +1412,22 @@ function useTrackHistory(options) {
1244
1412
  maxJumpDeg = 5,
1245
1413
  onFrame
1246
1414
  } = options;
1247
- const currentPositions = useRef2(/* @__PURE__ */ new Map());
1248
- const lastActualPos = useRef2(/* @__PURE__ */ new Map());
1249
- const history = useRef2(/* @__PURE__ */ new Map());
1250
- const lastSeen = useRef2(/* @__PURE__ */ new Map());
1251
- const lastUpdPerf = useRef2(/* @__PURE__ */ new Map());
1252
- const interp = useRef2(/* @__PURE__ */ new Map());
1253
- const velocity = useRef2(/* @__PURE__ */ new Map());
1254
- const drState = useRef2(/* @__PURE__ */ new Map());
1255
- const rafRef = useRef2(null);
1256
- const onFrameRef = useRef2(onFrame);
1415
+ const currentPositions = useRef3(/* @__PURE__ */ new Map());
1416
+ const lastActualPos = useRef3(/* @__PURE__ */ new Map());
1417
+ const history = useRef3(/* @__PURE__ */ new Map());
1418
+ const lastSeen = useRef3(/* @__PURE__ */ new Map());
1419
+ const lastUpdPerf = useRef3(/* @__PURE__ */ new Map());
1420
+ const interp = useRef3(/* @__PURE__ */ new Map());
1421
+ const velocity = useRef3(/* @__PURE__ */ new Map());
1422
+ const drState = useRef3(/* @__PURE__ */ new Map());
1423
+ const rafRef = useRef3(null);
1424
+ const onFrameRef = useRef3(onFrame);
1257
1425
  onFrameRef.current = onFrame;
1258
- const maxHistoryPointsRef = useRef2(maxHistoryPoints);
1426
+ const maxHistoryPointsRef = useRef3(maxHistoryPoints);
1259
1427
  maxHistoryPointsRef.current = maxHistoryPoints;
1260
- const minDeltaDegRef = useRef2(minDeltaDeg);
1428
+ const minDeltaDegRef = useRef3(minDeltaDeg);
1261
1429
  minDeltaDegRef.current = minDeltaDeg;
1262
- const maxJumpDegRef = useRef2(maxJumpDeg);
1430
+ const maxJumpDegRef = useRef3(maxJumpDeg);
1263
1431
  maxJumpDegRef.current = maxJumpDeg;
1264
1432
  function emitFrame() {
1265
1433
  onFrameRef.current({
@@ -1422,7 +1590,7 @@ function useTrackHistory(options) {
1422
1590
  drState.current.delete(trackId);
1423
1591
  emitFrame();
1424
1592
  }, []);
1425
- useEffect2(() => {
1593
+ useEffect3(() => {
1426
1594
  return () => {
1427
1595
  if (rafRef.current !== null) {
1428
1596
  cancelAnimationFrame(rafRef.current);
@@ -1437,7 +1605,7 @@ function useTrackHistory(options) {
1437
1605
  }
1438
1606
 
1439
1607
  // src/react/hooks/index.ts
1440
- import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3 } from "react";
1608
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4 } from "react";
1441
1609
  import { useQuery } from "@tanstack/react-query";
1442
1610
  import { useShallow } from "zustand/react/shallow";
1443
1611
 
@@ -1537,7 +1705,7 @@ function useAeroTracks(opts = {}) {
1537
1705
  const isDegraded = useTrackStore(selectIsDegraded);
1538
1706
  const lastUpdateAt = useTrackStore(selectLastUpdateAt);
1539
1707
  const error = useTrackStore(selectError);
1540
- const unsubRef = useRef3(null);
1708
+ const unsubRef = useRef4(null);
1541
1709
  const { isLoading } = useQuery({
1542
1710
  queryKey: ["aero-snapshot", opts.bbox?.join(","), opts.source, opts.minConfidence],
1543
1711
  enabled: opts.enabled !== false,
@@ -1551,7 +1719,7 @@ function useAeroTracks(opts = {}) {
1551
1719
  return result;
1552
1720
  }
1553
1721
  });
1554
- useEffect3(() => {
1722
+ useEffect4(() => {
1555
1723
  if (opts.enabled === false) return;
1556
1724
  const actions = useTrackStore.getState();
1557
1725
  unsubRef.current = client.tracks.subscribe(
@@ -1688,6 +1856,7 @@ export {
1688
1856
  useTrackHistory,
1689
1857
  useTrackMotion,
1690
1858
  useTrackStore,
1859
+ useTrackTrail,
1691
1860
  useTracks,
1692
1861
  useWebhooks
1693
1862
  };