@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/README.md +63 -17
- package/dist/index-ByNBpjNk.d.ts +75 -0
- package/dist/{index-BXiTmKdS.d.ts → index-CeayQnTp.d.ts} +153 -1
- package/dist/index.d.ts +21 -74
- package/dist/index.js +240 -6
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +24 -4
- package/dist/react/index.js +275 -27
- package/dist/react/index.js.map +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +135 -7
- package/dist/sdk/index.js.map +1 -1
- package/package.json +2 -2
package/dist/react/index.js
CHANGED
|
@@ -318,6 +318,87 @@ function createAeroTrpcClient(endpoint, token) {
|
|
|
318
318
|
});
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// src/sdk/api/response.ts
|
|
322
|
+
var ENVELOPE_HEADER = "x-aero-envelope";
|
|
323
|
+
var STATUS_CODES = [
|
|
324
|
+
[401, "UNAUTHORIZED"],
|
|
325
|
+
[403, "FORBIDDEN"],
|
|
326
|
+
[404, "NOT_FOUND"],
|
|
327
|
+
[400, "BAD_REQUEST"],
|
|
328
|
+
[409, "CONFLICT"],
|
|
329
|
+
[429, "RATE_LIMITED"],
|
|
330
|
+
[503, "UNAVAILABLE"],
|
|
331
|
+
[500, "INTERNAL"]
|
|
332
|
+
];
|
|
333
|
+
function codeForStatus(status) {
|
|
334
|
+
const hit = STATUS_CODES.find(([s]) => s === status);
|
|
335
|
+
if (hit) return hit[1];
|
|
336
|
+
return status >= 500 ? "INTERNAL" : "BAD_REQUEST";
|
|
337
|
+
}
|
|
338
|
+
var AeroApiError = class extends Error {
|
|
339
|
+
status;
|
|
340
|
+
code;
|
|
341
|
+
/** Retrying later may succeed (rate limit, server busy or failing). */
|
|
342
|
+
recoverable;
|
|
343
|
+
details;
|
|
344
|
+
constructor(init) {
|
|
345
|
+
super(init.message);
|
|
346
|
+
this.name = "AeroApiError";
|
|
347
|
+
this.status = init.status;
|
|
348
|
+
this.code = init.code;
|
|
349
|
+
this.recoverable = init.recoverable;
|
|
350
|
+
this.details = init.details;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
354
|
+
function isEnvelope(body) {
|
|
355
|
+
return isObject(body) && body.ok === true && "data" in body;
|
|
356
|
+
}
|
|
357
|
+
function isFailure(body) {
|
|
358
|
+
return isObject(body) && body.ok === false;
|
|
359
|
+
}
|
|
360
|
+
function errorFrom(res, body, text, label) {
|
|
361
|
+
const prefix = label ? `${label} \u2192 ${res.status}` : `HTTP ${res.status}`;
|
|
362
|
+
const failure = isFailure(body) ? body : void 0;
|
|
363
|
+
const e = failure?.error;
|
|
364
|
+
if (isObject(e)) {
|
|
365
|
+
const code = typeof e.code === "string" ? e.code : codeForStatus(res.status);
|
|
366
|
+
const message2 = typeof e.message === "string" ? e.message : res.statusText || prefix;
|
|
367
|
+
return new AeroApiError({
|
|
368
|
+
status: res.status,
|
|
369
|
+
code,
|
|
370
|
+
message: `${prefix}: ${message2}`,
|
|
371
|
+
recoverable: typeof e.recoverable === "boolean" ? e.recoverable : res.status >= 500,
|
|
372
|
+
details: e.details
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
const message = typeof e === "string" ? e : text.slice(0, 200) || res.statusText;
|
|
376
|
+
return new AeroApiError({
|
|
377
|
+
status: res.status,
|
|
378
|
+
code: codeForStatus(res.status),
|
|
379
|
+
message: message ? `${prefix}: ${message}` : prefix,
|
|
380
|
+
recoverable: res.status >= 500 || res.status === 429,
|
|
381
|
+
details: failure?.detail
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async function readApiBody(res, label) {
|
|
385
|
+
const text = await res.text();
|
|
386
|
+
let body;
|
|
387
|
+
try {
|
|
388
|
+
body = text ? JSON.parse(text) : void 0;
|
|
389
|
+
} catch {
|
|
390
|
+
body = void 0;
|
|
391
|
+
}
|
|
392
|
+
if (!res.ok || isFailure(body)) throw errorFrom(res, body, text, label);
|
|
393
|
+
return body;
|
|
394
|
+
}
|
|
395
|
+
async function parseApiEnvelope(res, label) {
|
|
396
|
+
const body = await readApiBody(res, label);
|
|
397
|
+
if (!isEnvelope(body)) return { data: body };
|
|
398
|
+
const meta = body.meta;
|
|
399
|
+
return { data: body.data, ...isObject(meta) && { meta } };
|
|
400
|
+
}
|
|
401
|
+
|
|
321
402
|
// src/sdk/api/rest.ts
|
|
322
403
|
var AeroRestClient = class {
|
|
323
404
|
constructor(endpoint, token) {
|
|
@@ -335,8 +416,7 @@ var AeroRestClient = class {
|
|
|
335
416
|
},
|
|
336
417
|
body: body ? JSON.stringify(body) : void 0
|
|
337
418
|
});
|
|
338
|
-
|
|
339
|
-
return res.json();
|
|
419
|
+
return await readApiBody(res, `${method} ${path}`);
|
|
340
420
|
}
|
|
341
421
|
createSource(body) {
|
|
342
422
|
return this.req("POST", "/v1/sources", body);
|
|
@@ -555,12 +635,16 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
555
635
|
return null;
|
|
556
636
|
}
|
|
557
637
|
}
|
|
558
|
-
async
|
|
638
|
+
async federatedRequest(base, path, query) {
|
|
559
639
|
const url = new URL(`${base}${path}`);
|
|
560
640
|
for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
|
|
561
|
-
const res = await fetch(url, {
|
|
562
|
-
|
|
563
|
-
|
|
641
|
+
const res = await fetch(url, {
|
|
642
|
+
headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
|
|
643
|
+
});
|
|
644
|
+
return parseApiEnvelope(res, `Federated request ${url.pathname}`);
|
|
645
|
+
}
|
|
646
|
+
async federatedFetch(base, path, query) {
|
|
647
|
+
return (await this.federatedRequest(base, path, query)).data;
|
|
564
648
|
}
|
|
565
649
|
tracks = {
|
|
566
650
|
subscribe: (opts, callbacks) => {
|
|
@@ -651,6 +735,42 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
651
735
|
});
|
|
652
736
|
}
|
|
653
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 } };
|
|
654
774
|
}
|
|
655
775
|
};
|
|
656
776
|
/** Flight plans received from AFTN feeds. Live tracks that correlate with a
|
|
@@ -821,8 +941,135 @@ function AeroSwarmProvider({
|
|
|
821
941
|
return /* @__PURE__ */ jsx(QueryClientProvider, { client: qc, children: /* @__PURE__ */ jsx(AeroSwarmContext.Provider, { value: { client }, children }) });
|
|
822
942
|
}
|
|
823
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
|
+
|
|
824
1071
|
// src/react/hooks/use-track-motion.ts
|
|
825
|
-
import { useEffect, useMemo as useMemo2, useRef } from "react";
|
|
1072
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
826
1073
|
|
|
827
1074
|
// src/shared/motion/index.ts
|
|
828
1075
|
var KT_TO_MS = 0.514444;
|
|
@@ -1131,10 +1378,10 @@ function createTrackMotion(options = {}) {
|
|
|
1131
1378
|
// src/react/hooks/use-track-motion.ts
|
|
1132
1379
|
function useTrackMotion(options) {
|
|
1133
1380
|
const { onFrame, ...motionOptions } = options;
|
|
1134
|
-
const onFrameRef =
|
|
1381
|
+
const onFrameRef = useRef2(onFrame);
|
|
1135
1382
|
onFrameRef.current = onFrame;
|
|
1136
1383
|
const motion = useMemo2(() => createTrackMotion(motionOptions), []);
|
|
1137
|
-
|
|
1384
|
+
useEffect2(() => {
|
|
1138
1385
|
const stop = motion.start((frame) => onFrameRef.current(frame));
|
|
1139
1386
|
return () => {
|
|
1140
1387
|
stop();
|
|
@@ -1145,7 +1392,7 @@ function useTrackMotion(options) {
|
|
|
1145
1392
|
}
|
|
1146
1393
|
|
|
1147
1394
|
// src/react/hooks/use-track-history.ts
|
|
1148
|
-
import { useCallback, useEffect as
|
|
1395
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
1149
1396
|
var DR_MIN_SPEED_KT = 10;
|
|
1150
1397
|
var MAX_SPEED_KT = 1200;
|
|
1151
1398
|
var GAP_RESET_MS = 3e4;
|
|
@@ -1165,22 +1412,22 @@ function useTrackHistory(options) {
|
|
|
1165
1412
|
maxJumpDeg = 5,
|
|
1166
1413
|
onFrame
|
|
1167
1414
|
} = options;
|
|
1168
|
-
const currentPositions =
|
|
1169
|
-
const lastActualPos =
|
|
1170
|
-
const history =
|
|
1171
|
-
const lastSeen =
|
|
1172
|
-
const lastUpdPerf =
|
|
1173
|
-
const interp =
|
|
1174
|
-
const velocity =
|
|
1175
|
-
const drState =
|
|
1176
|
-
const rafRef =
|
|
1177
|
-
const onFrameRef =
|
|
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);
|
|
1178
1425
|
onFrameRef.current = onFrame;
|
|
1179
|
-
const maxHistoryPointsRef =
|
|
1426
|
+
const maxHistoryPointsRef = useRef3(maxHistoryPoints);
|
|
1180
1427
|
maxHistoryPointsRef.current = maxHistoryPoints;
|
|
1181
|
-
const minDeltaDegRef =
|
|
1428
|
+
const minDeltaDegRef = useRef3(minDeltaDeg);
|
|
1182
1429
|
minDeltaDegRef.current = minDeltaDeg;
|
|
1183
|
-
const maxJumpDegRef =
|
|
1430
|
+
const maxJumpDegRef = useRef3(maxJumpDeg);
|
|
1184
1431
|
maxJumpDegRef.current = maxJumpDeg;
|
|
1185
1432
|
function emitFrame() {
|
|
1186
1433
|
onFrameRef.current({
|
|
@@ -1343,7 +1590,7 @@ function useTrackHistory(options) {
|
|
|
1343
1590
|
drState.current.delete(trackId);
|
|
1344
1591
|
emitFrame();
|
|
1345
1592
|
}, []);
|
|
1346
|
-
|
|
1593
|
+
useEffect3(() => {
|
|
1347
1594
|
return () => {
|
|
1348
1595
|
if (rafRef.current !== null) {
|
|
1349
1596
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -1358,7 +1605,7 @@ function useTrackHistory(options) {
|
|
|
1358
1605
|
}
|
|
1359
1606
|
|
|
1360
1607
|
// src/react/hooks/index.ts
|
|
1361
|
-
import { useCallback as useCallback2, useEffect as
|
|
1608
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4 } from "react";
|
|
1362
1609
|
import { useQuery } from "@tanstack/react-query";
|
|
1363
1610
|
import { useShallow } from "zustand/react/shallow";
|
|
1364
1611
|
|
|
@@ -1458,7 +1705,7 @@ function useAeroTracks(opts = {}) {
|
|
|
1458
1705
|
const isDegraded = useTrackStore(selectIsDegraded);
|
|
1459
1706
|
const lastUpdateAt = useTrackStore(selectLastUpdateAt);
|
|
1460
1707
|
const error = useTrackStore(selectError);
|
|
1461
|
-
const unsubRef =
|
|
1708
|
+
const unsubRef = useRef4(null);
|
|
1462
1709
|
const { isLoading } = useQuery({
|
|
1463
1710
|
queryKey: ["aero-snapshot", opts.bbox?.join(","), opts.source, opts.minConfidence],
|
|
1464
1711
|
enabled: opts.enabled !== false,
|
|
@@ -1472,7 +1719,7 @@ function useAeroTracks(opts = {}) {
|
|
|
1472
1719
|
return result;
|
|
1473
1720
|
}
|
|
1474
1721
|
});
|
|
1475
|
-
|
|
1722
|
+
useEffect4(() => {
|
|
1476
1723
|
if (opts.enabled === false) return;
|
|
1477
1724
|
const actions = useTrackStore.getState();
|
|
1478
1725
|
unsubRef.current = client.tracks.subscribe(
|
|
@@ -1609,6 +1856,7 @@ export {
|
|
|
1609
1856
|
useTrackHistory,
|
|
1610
1857
|
useTrackMotion,
|
|
1611
1858
|
useTrackStore,
|
|
1859
|
+
useTrackTrail,
|
|
1612
1860
|
useTracks,
|
|
1613
1861
|
useWebhooks
|
|
1614
1862
|
};
|