@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/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
|
-
|
|
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
|
|
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, {
|
|
558
|
-
|
|
559
|
-
|
|
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
|