@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.
- package/README.md +631 -592
- package/dist/{index-CyxVoJXL.d.ts → index-DH3DH1Nj.d.ts} +130 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +131 -5
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +23 -2
- package/dist/react/index.js +201 -26
- package/dist/react/index.js.map +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +56 -5
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -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
|
|
395
|
+
async function parseApiEnvelope(res, label) {
|
|
396
396
|
const body = await readApiBody(res, label);
|
|
397
|
-
|
|
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
|
-
|
|
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.
|
|
648
|
+
headers: { Authorization: `Bearer ${await this.backendCredential()}`, [ENVELOPE_HEADER]: "1" }
|
|
641
649
|
});
|
|
642
|
-
return
|
|
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 =
|
|
1387
|
+
const onFrameRef = useRef2(onFrame);
|
|
1214
1388
|
onFrameRef.current = onFrame;
|
|
1215
1389
|
const motion = useMemo2(() => createTrackMotion(motionOptions), []);
|
|
1216
|
-
|
|
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
|
|
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 =
|
|
1248
|
-
const lastActualPos =
|
|
1249
|
-
const history =
|
|
1250
|
-
const lastSeen =
|
|
1251
|
-
const lastUpdPerf =
|
|
1252
|
-
const interp =
|
|
1253
|
-
const velocity =
|
|
1254
|
-
const drState =
|
|
1255
|
-
const rafRef =
|
|
1256
|
-
const onFrameRef =
|
|
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 =
|
|
1432
|
+
const maxHistoryPointsRef = useRef3(maxHistoryPoints);
|
|
1259
1433
|
maxHistoryPointsRef.current = maxHistoryPoints;
|
|
1260
|
-
const minDeltaDegRef =
|
|
1434
|
+
const minDeltaDegRef = useRef3(minDeltaDeg);
|
|
1261
1435
|
minDeltaDegRef.current = minDeltaDeg;
|
|
1262
|
-
const maxJumpDegRef =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
};
|