@aeroswarm/client 0.1.14 → 0.1.16

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,22 @@ var AeroSwarm = class _AeroSwarm {
633
635
  return null;
634
636
  }
635
637
  }
636
- async federatedFetch(base, path, query) {
638
+ /** What to present to an airport's own backend: `backendToken` (read now if it is a function), else `token`. */
639
+ async backendCredential() {
640
+ const given = this.opts.backendToken;
641
+ const value = typeof given === "function" ? await given() : given;
642
+ return value || this.opts.token;
643
+ }
644
+ async federatedRequest(base, path, query) {
637
645
  const url = new URL(`${base}${path}`);
638
646
  for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
639
647
  const res = await fetch(url, {
640
- headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
648
+ headers: { Authorization: `Bearer ${await this.backendCredential()}`, [ENVELOPE_HEADER]: "1" }
641
649
  });
642
- return parseApiResponse(res, `Federated request ${url.pathname}`);
650
+ return parseApiEnvelope(res, `Federated request ${url.pathname}`);
651
+ }
652
+ async federatedFetch(base, path, query) {
653
+ return (await this.federatedRequest(base, path, query)).data;
643
654
  }
644
655
  tracks = {
645
656
  subscribe: (opts, callbacks) => {
@@ -730,6 +741,42 @@ var AeroSwarm = class _AeroSwarm {
730
741
  });
731
742
  }
732
743
  return this.trpc.tracks.history.query(input);
744
+ },
745
+ /**
746
+ * The path an aircraft has flown, ready to draw: a GeoJSON line plus a point for every time
747
+ * mark, with the label already written in `tz`. By default the whole flight so far; `fromMs`
748
+ * and `toMs` choose a period, `sinceMs` fetches only what is new (see `createTrailPoller`),
749
+ * `stepM` asks for a coarser line. The airport's backend builds it (memory first, history
750
+ * as a fallback), so nothing is computed here. There is no AeroSwarm-side equivalent:
751
+ * pass `endpoint` or an `airportId` with a registered `historyEndpoint`.
752
+ */
753
+ trail: async (input) => {
754
+ const base = await this.resolveHistoryEndpoint(input.airportId, input.endpoint);
755
+ if (!base) {
756
+ throw new AeroApiError({
757
+ status: 400,
758
+ code: "BAD_REQUEST",
759
+ recoverable: false,
760
+ message: "tracks.trail needs the airport's backend: pass `endpoint`, or an `airportId` that has a historyEndpoint"
761
+ });
762
+ }
763
+ const num = (v) => v === void 0 ? void 0 : String(v);
764
+ const { data, meta } = await this.federatedRequest(
765
+ base,
766
+ `/tracks/${encodeURIComponent(input.trackId)}/trail`,
767
+ {
768
+ fromMs: num(input.fromMs),
769
+ toMs: num(input.toMs),
770
+ sinceMs: num(input.sinceMs),
771
+ stepM: num(input.stepM),
772
+ limit: num(input.limit),
773
+ labelEveryMs: num(input.labelEveryMs),
774
+ tz: input.tz,
775
+ labelFormat: input.labelFormat,
776
+ format: "geojson"
777
+ }
778
+ );
779
+ return { trail: data, ...meta && { meta } };
733
780
  }
734
781
  };
735
782
  /** Flight plans received from AFTN feeds. Live tracks that correlate with a
@@ -900,8 +947,135 @@ function AeroSwarmProvider({
900
947
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: qc, children: /* @__PURE__ */ jsx(AeroSwarmContext.Provider, { value: { client }, children }) });
901
948
  }
902
949
 
950
+ // src/react/hooks/use-track-trail.ts
951
+ import { useEffect, useRef, useState } from "react";
952
+
953
+ // src/shared/trail/index.ts
954
+ var lineOf = (fc) => fc.features.find((f) => f.properties.kind === "trail");
955
+ var marksOf = (fc) => fc.features.filter((f) => f.properties.kind === "mark");
956
+ function trailEnd(trail) {
957
+ const line = lineOf(trail);
958
+ return line && line.geometry.coordinates.length > 0 ? line.properties.to : void 0;
959
+ }
960
+ function mergeTrail(prev, delta) {
961
+ if (!prev) return delta;
962
+ const before = lineOf(prev);
963
+ const added = lineOf(delta);
964
+ if (!before || !added) return prev;
965
+ if (added.geometry.coordinates.length === 0) return prev;
966
+ const seen = new Set(marksOf(prev).map((m) => m.properties.t));
967
+ const newMarks = marksOf(delta).filter((m) => !seen.has(m.properties.t));
968
+ const line = {
969
+ ...before,
970
+ properties: { ...before.properties, to: added.properties.to, stepM: Math.max(before.properties.stepM, added.properties.stepM) },
971
+ geometry: { type: "LineString", coordinates: [...before.geometry.coordinates, ...added.geometry.coordinates] }
972
+ };
973
+ return { type: "FeatureCollection", features: [line, ...marksOf(prev), ...newMarks] };
974
+ }
975
+ var isGone = (err) => typeof err === "object" && err !== null && err.code === "NOT_FOUND";
976
+ function createTrailPoller(fetchTrail, options) {
977
+ const pollMs = options.pollMs ?? 5e3;
978
+ const stopWhenGone = options.stopWhenGone ?? true;
979
+ let current;
980
+ let stopped = false;
981
+ let timer;
982
+ let running;
983
+ const schedule = () => {
984
+ if (stopped || pollMs <= 0) return;
985
+ timer = setTimeout(() => void tick(), pollMs);
986
+ };
987
+ async function once() {
988
+ try {
989
+ const since = current ? trailEnd(current) : void 0;
990
+ const { trail } = await fetchTrail(since);
991
+ if (stopped) return;
992
+ const next = mergeTrail(current, trail);
993
+ if (next !== current) {
994
+ current = next;
995
+ options.onUpdate(next);
996
+ }
997
+ } catch (err) {
998
+ if (stopped) return;
999
+ options.onError?.(err);
1000
+ if (stopWhenGone && isGone(err)) stopped = true;
1001
+ }
1002
+ }
1003
+ function tick() {
1004
+ if (stopped) return Promise.resolve();
1005
+ running ??= once().finally(() => {
1006
+ running = void 0;
1007
+ schedule();
1008
+ });
1009
+ return running;
1010
+ }
1011
+ void tick();
1012
+ return {
1013
+ stop() {
1014
+ stopped = true;
1015
+ if (timer) clearTimeout(timer);
1016
+ },
1017
+ async refresh() {
1018
+ if (timer) clearTimeout(timer);
1019
+ await (running ?? tick());
1020
+ },
1021
+ current: () => current
1022
+ };
1023
+ }
1024
+
1025
+ // src/react/hooks/use-track-trail.ts
1026
+ function useTrackTrail(trackId, options = {}) {
1027
+ const { client } = useAeroSwarmContext();
1028
+ const [trail, setTrail] = useState(void 0);
1029
+ const [loading, setLoading] = useState(false);
1030
+ const [error, setError] = useState(void 0);
1031
+ const optionsRef = useRef(options);
1032
+ optionsRef.current = options;
1033
+ const enabled = options.enabled !== false && !!trackId;
1034
+ const key = JSON.stringify([
1035
+ trackId,
1036
+ options.endpoint,
1037
+ options.airportId,
1038
+ options.fromMs,
1039
+ options.toMs,
1040
+ options.stepM,
1041
+ options.limit,
1042
+ options.labelEveryMs,
1043
+ options.tz,
1044
+ options.labelFormat,
1045
+ options.pollMs
1046
+ ]);
1047
+ useEffect(() => {
1048
+ setTrail(void 0);
1049
+ setError(void 0);
1050
+ if (!enabled || !trackId) {
1051
+ setLoading(false);
1052
+ return;
1053
+ }
1054
+ setLoading(true);
1055
+ const { enabled: _enabled, pollMs, ...request } = optionsRef.current;
1056
+ const poller = createTrailPoller(
1057
+ (sinceMs) => client.tracks.trail({ ...request, trackId, sinceMs }),
1058
+ {
1059
+ // A fixed period in the past does not grow: ask once.
1060
+ pollMs: request.toMs !== void 0 ? 0 : pollMs,
1061
+ onUpdate: (t) => {
1062
+ setTrail(t);
1063
+ setLoading(false);
1064
+ setError(void 0);
1065
+ },
1066
+ onError: (e) => {
1067
+ setError(e);
1068
+ setLoading(false);
1069
+ }
1070
+ }
1071
+ );
1072
+ return () => poller.stop();
1073
+ }, [client, enabled, key]);
1074
+ return { trail, loading, error };
1075
+ }
1076
+
903
1077
  // src/react/hooks/use-track-motion.ts
904
- import { useEffect, useMemo as useMemo2, useRef } from "react";
1078
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
905
1079
 
906
1080
  // src/shared/motion/index.ts
907
1081
  var KT_TO_MS = 0.514444;
@@ -1210,10 +1384,10 @@ function createTrackMotion(options = {}) {
1210
1384
  // src/react/hooks/use-track-motion.ts
1211
1385
  function useTrackMotion(options) {
1212
1386
  const { onFrame, ...motionOptions } = options;
1213
- const onFrameRef = useRef(onFrame);
1387
+ const onFrameRef = useRef2(onFrame);
1214
1388
  onFrameRef.current = onFrame;
1215
1389
  const motion = useMemo2(() => createTrackMotion(motionOptions), []);
1216
- useEffect(() => {
1390
+ useEffect2(() => {
1217
1391
  const stop = motion.start((frame) => onFrameRef.current(frame));
1218
1392
  return () => {
1219
1393
  stop();
@@ -1224,7 +1398,7 @@ function useTrackMotion(options) {
1224
1398
  }
1225
1399
 
1226
1400
  // src/react/hooks/use-track-history.ts
1227
- import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
1401
+ import { useCallback, useEffect as useEffect3, useRef as useRef3 } from "react";
1228
1402
  var DR_MIN_SPEED_KT = 10;
1229
1403
  var MAX_SPEED_KT = 1200;
1230
1404
  var GAP_RESET_MS = 3e4;
@@ -1244,22 +1418,22 @@ function useTrackHistory(options) {
1244
1418
  maxJumpDeg = 5,
1245
1419
  onFrame
1246
1420
  } = 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);
1421
+ const currentPositions = useRef3(/* @__PURE__ */ new Map());
1422
+ const lastActualPos = useRef3(/* @__PURE__ */ new Map());
1423
+ const history = useRef3(/* @__PURE__ */ new Map());
1424
+ const lastSeen = useRef3(/* @__PURE__ */ new Map());
1425
+ const lastUpdPerf = useRef3(/* @__PURE__ */ new Map());
1426
+ const interp = useRef3(/* @__PURE__ */ new Map());
1427
+ const velocity = useRef3(/* @__PURE__ */ new Map());
1428
+ const drState = useRef3(/* @__PURE__ */ new Map());
1429
+ const rafRef = useRef3(null);
1430
+ const onFrameRef = useRef3(onFrame);
1257
1431
  onFrameRef.current = onFrame;
1258
- const maxHistoryPointsRef = useRef2(maxHistoryPoints);
1432
+ const maxHistoryPointsRef = useRef3(maxHistoryPoints);
1259
1433
  maxHistoryPointsRef.current = maxHistoryPoints;
1260
- const minDeltaDegRef = useRef2(minDeltaDeg);
1434
+ const minDeltaDegRef = useRef3(minDeltaDeg);
1261
1435
  minDeltaDegRef.current = minDeltaDeg;
1262
- const maxJumpDegRef = useRef2(maxJumpDeg);
1436
+ const maxJumpDegRef = useRef3(maxJumpDeg);
1263
1437
  maxJumpDegRef.current = maxJumpDeg;
1264
1438
  function emitFrame() {
1265
1439
  onFrameRef.current({
@@ -1422,7 +1596,7 @@ function useTrackHistory(options) {
1422
1596
  drState.current.delete(trackId);
1423
1597
  emitFrame();
1424
1598
  }, []);
1425
- useEffect2(() => {
1599
+ useEffect3(() => {
1426
1600
  return () => {
1427
1601
  if (rafRef.current !== null) {
1428
1602
  cancelAnimationFrame(rafRef.current);
@@ -1437,7 +1611,7 @@ function useTrackHistory(options) {
1437
1611
  }
1438
1612
 
1439
1613
  // src/react/hooks/index.ts
1440
- import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3 } from "react";
1614
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4 } from "react";
1441
1615
  import { useQuery } from "@tanstack/react-query";
1442
1616
  import { useShallow } from "zustand/react/shallow";
1443
1617
 
@@ -1537,7 +1711,7 @@ function useAeroTracks(opts = {}) {
1537
1711
  const isDegraded = useTrackStore(selectIsDegraded);
1538
1712
  const lastUpdateAt = useTrackStore(selectLastUpdateAt);
1539
1713
  const error = useTrackStore(selectError);
1540
- const unsubRef = useRef3(null);
1714
+ const unsubRef = useRef4(null);
1541
1715
  const { isLoading } = useQuery({
1542
1716
  queryKey: ["aero-snapshot", opts.bbox?.join(","), opts.source, opts.minConfidence],
1543
1717
  enabled: opts.enabled !== false,
@@ -1551,7 +1725,7 @@ function useAeroTracks(opts = {}) {
1551
1725
  return result;
1552
1726
  }
1553
1727
  });
1554
- useEffect3(() => {
1728
+ useEffect4(() => {
1555
1729
  if (opts.enabled === false) return;
1556
1730
  const actions = useTrackStore.getState();
1557
1731
  unsubRef.current = client.tracks.subscribe(
@@ -1688,6 +1862,7 @@ export {
1688
1862
  useTrackHistory,
1689
1863
  useTrackMotion,
1690
1864
  useTrackStore,
1865
+ useTrackTrail,
1691
1866
  useTracks,
1692
1867
  useWebhooks
1693
1868
  };