@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.
@@ -264,6 +264,101 @@ declare class AeroReplayClient {
264
264
  declare function createAeroTrpcClient(endpoint: string, token: string): _trpc_client.TRPCClient<any>;
265
265
  type AeroTrpcClient = ReturnType<typeof createAeroTrpcClient>;
266
266
 
267
+ interface TrailLine {
268
+ type: "Feature";
269
+ properties: {
270
+ kind: "trail";
271
+ trackId: string;
272
+ /** epoch ms of the first and the last point */
273
+ from: number;
274
+ to: number;
275
+ /** spacing of the points, metres */
276
+ stepM: number;
277
+ /** the line starts at the beginning of the flight (nothing older exists) */
278
+ fromStart: boolean;
279
+ };
280
+ /** [lon, lat, altitude in metres] */
281
+ geometry: {
282
+ type: "LineString";
283
+ coordinates: Array<[number, number, number]>;
284
+ };
285
+ }
286
+ interface TrailMark {
287
+ type: "Feature";
288
+ /** `label` is ready to display, e.g. "01-11-26 11:16" */
289
+ properties: {
290
+ kind: "mark";
291
+ t: number;
292
+ label: string;
293
+ };
294
+ geometry: {
295
+ type: "Point";
296
+ coordinates: [number, number];
297
+ };
298
+ }
299
+ interface TrailFeatureCollection {
300
+ type: "FeatureCollection";
301
+ features: Array<TrailLine | TrailMark>;
302
+ }
303
+ interface TrailMeta {
304
+ serverTime?: number;
305
+ count?: number;
306
+ /** the server dropped points to honour `limit` */
307
+ truncated?: boolean;
308
+ }
309
+ interface TrailResult {
310
+ trail: TrailFeatureCollection;
311
+ meta?: TrailMeta;
312
+ }
313
+ interface TrailRequest {
314
+ trackId: string;
315
+ /** The airport backend's base URL (its `/aero` mount). Without it `airportId` must have a registered historyEndpoint. */
316
+ endpoint?: string;
317
+ airportId?: string;
318
+ /** A period instead of the whole flight (epoch ms). */
319
+ fromMs?: number;
320
+ toMs?: number;
321
+ /** Only what is newer than this: for keeping a trail current. */
322
+ sinceMs?: number;
323
+ /** Coarser spacing than the server stores, metres. */
324
+ stepM?: number;
325
+ /** At most this many points. */
326
+ limit?: number;
327
+ /** A time mark every this many ms (default 120 000), aligned to the clock. 0 = none. */
328
+ labelEveryMs?: number;
329
+ /** Time zone of the labels, e.g. "Asia/Jakarta" (default UTC). */
330
+ tz?: string;
331
+ labelFormat?: "datetime" | "time";
332
+ }
333
+ /** Time of the last point of a trail; undefined when it has none. What to pass as `sinceMs`. */
334
+ declare function trailEnd(trail: TrailFeatureCollection): number | undefined;
335
+ /**
336
+ * Appends a delta (a request with `sinceMs`) to the trail you already have. Returns `prev`
337
+ * itself when the delta adds nothing, so a caller can tell nothing changed.
338
+ */
339
+ declare function mergeTrail(prev: TrailFeatureCollection | undefined, delta: TrailFeatureCollection): TrailFeatureCollection;
340
+ interface TrailPollerOptions {
341
+ /** How often to ask for what is new. Default 5000. 0 or less: fetch once and stop. */
342
+ pollMs?: number;
343
+ /** Called with the whole trail, first when it arrives, then each time it grows. */
344
+ onUpdate: (trail: TrailFeatureCollection) => void;
345
+ onError?: (err: unknown) => void;
346
+ /** Stop for good when the server says the track is unknown (it left and its retention ended). Default true. */
347
+ stopWhenGone?: boolean;
348
+ }
349
+ interface TrailPoller {
350
+ stop(): void;
351
+ /** Ask now instead of waiting for the next tick. */
352
+ refresh(): Promise<void>;
353
+ /** The trail as it stands. */
354
+ current(): TrailFeatureCollection | undefined;
355
+ }
356
+ /**
357
+ * Fetches a trail once and then keeps it current by asking only for what is new.
358
+ * Framework-free: `useTrackTrail` is a thin wrapper around it.
359
+ */
360
+ declare function createTrailPoller(fetchTrail: (sinceMs?: number) => Promise<TrailResult>, options: TrailPollerOptions): TrailPoller;
361
+
267
362
  declare class AeroGraphQLClient {
268
363
  private readonly endpoint;
269
364
  private readonly token;
@@ -358,6 +453,21 @@ declare class AeroApiError extends Error {
358
453
  * envelope themselves (`r.data.x`).
359
454
  */
360
455
  declare function readApiBody(res: Response, label?: string): Promise<unknown>;
456
+ /** What a standard response says about itself (see `meta` in @aeroswarm/core/api). Absent for servers that answer with bare data. */
457
+ interface ApiMetaLike {
458
+ serverTime?: number;
459
+ count?: number;
460
+ nextCursor?: string;
461
+ truncated?: boolean;
462
+ }
463
+ /**
464
+ * The data of a response together with its `meta`: unwrapped from `{ ok, data, meta }`, or the body
465
+ * as it is (and no meta) for servers that answer with bare data. Throws `AeroApiError` on failure.
466
+ */
467
+ declare function parseApiEnvelope<T>(res: Response, label?: string): Promise<{
468
+ data: T;
469
+ meta?: ApiMetaLike;
470
+ }>;
361
471
  /**
362
472
  * The data of a response: unwrapped from `{ ok, data }`, or the body as it is for servers
363
473
  * that answer with bare data. Throws `AeroApiError` on failure.
@@ -367,6 +477,13 @@ declare function parseApiResponse<T>(res: Response, label?: string): Promise<T>;
367
477
  interface AeroSwarmOptions {
368
478
  endpoint: string;
369
479
  token: string;
480
+ /**
481
+ * Credential for the airport's OWN backend (history, trail, replay days: whatever goes to a
482
+ * `historyEndpoint` or an explicit `endpoint`), when it differs from `token`. Pass a function to
483
+ * have it read on every request, e.g. `() => session.accessToken`, so a login that is refreshed
484
+ * is picked up without rebuilding the client. Defaults to `token`.
485
+ */
486
+ backendToken?: string | (() => string | undefined | Promise<string | undefined>);
370
487
  wsEndpoint?: string;
371
488
  reconnect?: boolean;
372
489
  maxReconnectDelay?: number;
@@ -423,6 +540,9 @@ declare class AeroSwarm {
423
540
  * (a broken airports.list() call must never brick replay/history for
424
541
  * everyone else). */
425
542
  private resolveHistoryEndpoint;
543
+ /** What to present to an airport's own backend: `backendToken` (read now if it is a function), else `token`. */
544
+ private backendCredential;
545
+ private federatedRequest;
426
546
  private federatedFetch;
427
547
  constructor(opts: AeroSwarmOptions);
428
548
  readonly tracks: {
@@ -451,6 +571,15 @@ declare class AeroSwarm {
451
571
  limit?: number;
452
572
  endpoint?: string;
453
573
  }) => Promise<TrackHistoryPoint[]>;
574
+ /**
575
+ * The path an aircraft has flown, ready to draw: a GeoJSON line plus a point for every time
576
+ * mark, with the label already written in `tz`. By default the whole flight so far; `fromMs`
577
+ * and `toMs` choose a period, `sinceMs` fetches only what is new (see `createTrailPoller`),
578
+ * `stepM` asks for a coarser line. The airport's backend builds it (memory first, history
579
+ * as a fallback), so nothing is computed here. There is no AeroSwarm-side equivalent:
580
+ * pass `endpoint` or an `airportId` with a registered `historyEndpoint`.
581
+ */
582
+ trail: (input: TrailRequest) => Promise<TrailResult>;
454
583
  };
455
584
  /** Flight plans received from AFTN feeds. Live tracks that correlate with a
456
585
  * plan (Mode-S address, else callsign) also carry `track.flightPlan`. */
@@ -730,4 +859,4 @@ declare class AeroSwarm {
730
859
  reconnect(): boolean;
731
860
  }
732
861
 
733
- export { AeroApiError as A, type DayAvailability as D, type FlightPlanRow as F, LABEL_COLORS as L, type ReplayCallbacks as R, type SourceRow as S, TrackCache as T, type UseAeroTracksResult as U, AeroReplayClient as a, AeroSwarm as b, type AeroSwarmOptions as c, AeroWsClient as d, type AftnMessageRow as e, type ApiErrorCode as f, type ApiErrorInit as g, type LabelCategory as h, type LabeledTrack as i, type LiveTrackCallbacks as j, type ReplayHandle as k, type ReplayProgress as l, type ReplayStreamOptions as m, type StreamMeta as n, type SubscribeOptions as o, type TrackCallbacks as p, type TrackHistoryPoint as q, codeForStatus as r, getLabelCategory as s, parseApiResponse as t, readApiBody as u, withLabel as w };
862
+ export { AeroApiError as A, codeForStatus as B, createTrailPoller as C, type DayAvailability as D, getLabelCategory as E, type FlightPlanRow as F, mergeTrail as G, parseApiEnvelope as H, parseApiResponse as I, readApiBody as J, trailEnd as K, LABEL_COLORS as L, withLabel as M, type ReplayCallbacks as R, type SourceRow as S, TrackCache as T, type UseAeroTracksResult as U, AeroReplayClient as a, AeroSwarm as b, type AeroSwarmOptions as c, AeroWsClient as d, type AftnMessageRow as e, type ApiErrorCode as f, type ApiErrorInit as g, type ApiMetaLike as h, type LabelCategory as i, type LabeledTrack as j, type LiveTrackCallbacks as k, type ReplayHandle as l, type ReplayProgress as m, type ReplayStreamOptions as n, type StreamMeta as o, type SubscribeOptions as p, type TrackCallbacks as q, type TrackHistoryPoint as r, type TrailFeatureCollection as s, type TrailLine as t, type TrailMark as u, type TrailMeta as v, type TrailPoller as w, type TrailPollerOptions as x, type TrailRequest as y, type TrailResult as z };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AeroApiError, a as AeroReplayClient, b as AeroSwarm, c as AeroSwarmOptions, d as AeroWsClient, e as AftnMessageRow, f as ApiErrorCode, g as ApiErrorInit, D as DayAvailability, F as FlightPlanRow, L as LABEL_COLORS, h as LabelCategory, i as LabeledTrack, j as LiveTrackCallbacks, R as ReplayCallbacks, k as ReplayHandle, l as ReplayProgress, m as ReplayStreamOptions, S as SourceRow, n as StreamMeta, o as SubscribeOptions, T as TrackCache, p as TrackCallbacks, q as TrackHistoryPoint, U as UseAeroTracksResult, r as codeForStatus, s as getLabelCategory, t as parseApiResponse, u as readApiBody, w as withLabel } from './index-CyxVoJXL.js';
1
+ export { A as AeroApiError, a as AeroReplayClient, b as AeroSwarm, c as AeroSwarmOptions, d as AeroWsClient, e as AftnMessageRow, f as ApiErrorCode, g as ApiErrorInit, h as ApiMetaLike, D as DayAvailability, F as FlightPlanRow, L as LABEL_COLORS, i as LabelCategory, j as LabeledTrack, k as LiveTrackCallbacks, R as ReplayCallbacks, l as ReplayHandle, m as ReplayProgress, n as ReplayStreamOptions, S as SourceRow, o as StreamMeta, p as SubscribeOptions, T as TrackCache, q as TrackCallbacks, r as TrackHistoryPoint, s as TrailFeatureCollection, t as TrailLine, u as TrailMark, v as TrailMeta, w as TrailPoller, x as TrailPollerOptions, y as TrailRequest, z as TrailResult, U as UseAeroTracksResult, B as codeForStatus, C as createTrailPoller, E as getLabelCategory, G as mergeTrail, H as parseApiEnvelope, I as parseApiResponse, J as readApiBody, K as trailEnd, M as withLabel } from './index-DH3DH1Nj.js';
2
2
  import { CurrentTrack } from '@aeroswarm/core/types';
3
3
  export { AeroConnectionStatus, AeroRecoverableError, Bbox, ConfidenceBudget, CurrentTrack, DataSource, FlightPhase, FlightPlan, FlightPlanStatus, FlightPlanSummary, FusedTrackEvent, Position, Protocol, SourceHealthEvent, SourceStatus, StreamEnvelope, TrackSource, TrackStatus, Velocity } from '@aeroswarm/core/types';
4
4
  export { clamp, haversineKm, isInsideBbox, isRenderableTrack, toCurrentTrack } from '@aeroswarm/core/utils';
package/dist/index.js CHANGED
@@ -388,9 +388,14 @@ async function readApiBody(res, label) {
388
388
  if (!res.ok || isFailure(body)) throw errorFrom(res, body, text, label);
389
389
  return body;
390
390
  }
391
- async function parseApiResponse(res, label) {
391
+ async function parseApiEnvelope(res, label) {
392
392
  const body = await readApiBody(res, label);
393
- return isEnvelope(body) ? body.data : body;
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;
394
399
  }
395
400
 
396
401
  // src/sdk/api/rest.ts
@@ -629,13 +634,22 @@ var AeroSwarm = class _AeroSwarm {
629
634
  return null;
630
635
  }
631
636
  }
632
- async federatedFetch(base, path, query) {
637
+ /** What to present to an airport's own backend: `backendToken` (read now if it is a function), else `token`. */
638
+ async backendCredential() {
639
+ const given = this.opts.backendToken;
640
+ const value = typeof given === "function" ? await given() : given;
641
+ return value || this.opts.token;
642
+ }
643
+ async federatedRequest(base, path, query) {
633
644
  const url = new URL(`${base}${path}`);
634
645
  for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
635
646
  const res = await fetch(url, {
636
- headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
647
+ headers: { Authorization: `Bearer ${await this.backendCredential()}`, [ENVELOPE_HEADER]: "1" }
637
648
  });
638
- return parseApiResponse(res, `Federated request ${url.pathname}`);
649
+ return parseApiEnvelope(res, `Federated request ${url.pathname}`);
650
+ }
651
+ async federatedFetch(base, path, query) {
652
+ return (await this.federatedRequest(base, path, query)).data;
639
653
  }
640
654
  tracks = {
641
655
  subscribe: (opts, callbacks) => {
@@ -726,6 +740,42 @@ var AeroSwarm = class _AeroSwarm {
726
740
  });
727
741
  }
728
742
  return this.trpc.tracks.history.query(input);
743
+ },
744
+ /**
745
+ * The path an aircraft has flown, ready to draw: a GeoJSON line plus a point for every time
746
+ * mark, with the label already written in `tz`. By default the whole flight so far; `fromMs`
747
+ * and `toMs` choose a period, `sinceMs` fetches only what is new (see `createTrailPoller`),
748
+ * `stepM` asks for a coarser line. The airport's backend builds it (memory first, history
749
+ * as a fallback), so nothing is computed here. There is no AeroSwarm-side equivalent:
750
+ * pass `endpoint` or an `airportId` with a registered `historyEndpoint`.
751
+ */
752
+ trail: async (input) => {
753
+ const base = await this.resolveHistoryEndpoint(input.airportId, input.endpoint);
754
+ if (!base) {
755
+ throw new AeroApiError({
756
+ status: 400,
757
+ code: "BAD_REQUEST",
758
+ recoverable: false,
759
+ message: "tracks.trail needs the airport's backend: pass `endpoint`, or an `airportId` that has a historyEndpoint"
760
+ });
761
+ }
762
+ const num = (v) => v === void 0 ? void 0 : String(v);
763
+ const { data, meta } = await this.federatedRequest(
764
+ base,
765
+ `/tracks/${encodeURIComponent(input.trackId)}/trail`,
766
+ {
767
+ fromMs: num(input.fromMs),
768
+ toMs: num(input.toMs),
769
+ sinceMs: num(input.sinceMs),
770
+ stepM: num(input.stepM),
771
+ limit: num(input.limit),
772
+ labelEveryMs: num(input.labelEveryMs),
773
+ tz: input.tz,
774
+ labelFormat: input.labelFormat,
775
+ format: "geojson"
776
+ }
777
+ );
778
+ return { trail: data, ...meta && { meta } };
729
779
  }
730
780
  };
731
781
  /** Flight plans received from AFTN feeds. Live tracks that correlate with a
@@ -1199,6 +1249,78 @@ function formatDataSource(track) {
1199
1249
  const ds = track.dataSource;
1200
1250
  return ds ? `${kind} ${ds.sac}/${ds.sic}` : kind;
1201
1251
  }
1252
+
1253
+ // src/shared/trail/index.ts
1254
+ var lineOf = (fc) => fc.features.find((f) => f.properties.kind === "trail");
1255
+ var marksOf = (fc) => fc.features.filter((f) => f.properties.kind === "mark");
1256
+ function trailEnd(trail) {
1257
+ const line = lineOf(trail);
1258
+ return line && line.geometry.coordinates.length > 0 ? line.properties.to : void 0;
1259
+ }
1260
+ function mergeTrail(prev, delta) {
1261
+ if (!prev) return delta;
1262
+ const before = lineOf(prev);
1263
+ const added = lineOf(delta);
1264
+ if (!before || !added) return prev;
1265
+ if (added.geometry.coordinates.length === 0) return prev;
1266
+ const seen = new Set(marksOf(prev).map((m) => m.properties.t));
1267
+ const newMarks = marksOf(delta).filter((m) => !seen.has(m.properties.t));
1268
+ const line = {
1269
+ ...before,
1270
+ properties: { ...before.properties, to: added.properties.to, stepM: Math.max(before.properties.stepM, added.properties.stepM) },
1271
+ geometry: { type: "LineString", coordinates: [...before.geometry.coordinates, ...added.geometry.coordinates] }
1272
+ };
1273
+ return { type: "FeatureCollection", features: [line, ...marksOf(prev), ...newMarks] };
1274
+ }
1275
+ var isGone = (err) => typeof err === "object" && err !== null && err.code === "NOT_FOUND";
1276
+ function createTrailPoller(fetchTrail, options) {
1277
+ const pollMs = options.pollMs ?? 5e3;
1278
+ const stopWhenGone = options.stopWhenGone ?? true;
1279
+ let current;
1280
+ let stopped = false;
1281
+ let timer;
1282
+ let running;
1283
+ const schedule = () => {
1284
+ if (stopped || pollMs <= 0) return;
1285
+ timer = setTimeout(() => void tick(), pollMs);
1286
+ };
1287
+ async function once() {
1288
+ try {
1289
+ const since = current ? trailEnd(current) : void 0;
1290
+ const { trail } = await fetchTrail(since);
1291
+ if (stopped) return;
1292
+ const next = mergeTrail(current, trail);
1293
+ if (next !== current) {
1294
+ current = next;
1295
+ options.onUpdate(next);
1296
+ }
1297
+ } catch (err) {
1298
+ if (stopped) return;
1299
+ options.onError?.(err);
1300
+ if (stopWhenGone && isGone(err)) stopped = true;
1301
+ }
1302
+ }
1303
+ function tick() {
1304
+ if (stopped) return Promise.resolve();
1305
+ running ??= once().finally(() => {
1306
+ running = void 0;
1307
+ schedule();
1308
+ });
1309
+ return running;
1310
+ }
1311
+ void tick();
1312
+ return {
1313
+ stop() {
1314
+ stopped = true;
1315
+ if (timer) clearTimeout(timer);
1316
+ },
1317
+ async refresh() {
1318
+ if (timer) clearTimeout(timer);
1319
+ await (running ?? tick());
1320
+ },
1321
+ current: () => current
1322
+ };
1323
+ }
1202
1324
  export {
1203
1325
  AeroApiError,
1204
1326
  AeroReplayClient,
@@ -1209,15 +1331,19 @@ export {
1209
1331
  clamp,
1210
1332
  codeForStatus,
1211
1333
  createTrackMotion,
1334
+ createTrailPoller,
1212
1335
  formatDataSource,
1213
1336
  getLabelCategory,
1214
1337
  haversineKm,
1215
1338
  isInsideBbox,
1216
1339
  isRenderableTrack2 as isRenderableTrack,
1340
+ mergeTrail,
1341
+ parseApiEnvelope,
1217
1342
  parseApiResponse,
1218
1343
  readApiBody,
1219
1344
  sourceLabel,
1220
1345
  toCurrentTrack,
1346
+ trailEnd,
1221
1347
  withLabel
1222
1348
  };
1223
1349
  //# sourceMappingURL=index.js.map