@aeroswarm/client 0.1.13 → 0.1.14

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 CHANGED
@@ -65,6 +65,23 @@ client.disconnect();
65
65
  | `tracks.stream(opts)` | Same live stream as an `AsyncIterable<LabeledTrack>` (`for await`) |
66
66
  | `tracks.updateFilter(opts)` | Change bbox / minConfidence / maxUpdateHz / maxTracks without reconnecting |
67
67
  | `tracks.reconnect()` | Force an immediate reconnect (skips backoff, re-subscribes with the last options, resumes after `disconnect()`); `false` if never subscribed. Also `AeroSwarm.reconnect()` and `useAeroTracks().reconnect` |
68
+ | `tracks.snapshot(input?)` | One-shot current state via tRPC (`airportId`, `bbox`, `source`, `minConfidence`, `limit`, `offset`) |
69
+ | `tracks.history(input)` | Persisted position history for one track (`trackId`, `airportId?`, `limit?`) — federated airports are served by their own backend |
70
+ | `airports.*` | Airport registry: `list/create/update/delete` + `surface`/`setSurface` (runway/taxiway/apron GeoJSON for the map) |
71
+ | `sources.*` | Register/list (optionally by `airportId`)/delete data sources (returns per-source auth tokens) |
72
+ | `alerts.*` | Alert history + live alert subscription |
73
+ | `rules.*` | Detection-rule CRUD — **global or per-airport** (see example below) |
74
+ | `routes.*` | Route conformance: pre-defined ground routes + live track clearances (deviation alerts) |
75
+ | `analytics.movements()` | Takeoffs/landings per hour + peak hour, per airport |
76
+ | `webhooks.*` | Webhook CRUD |
77
+ | `replay.days(input?)` | Data-availability calendar — which days have recorded tracks (see example below) |
78
+ | `replay.range/seek/window` | Historical replay metadata: range summary, point-in-time seek, time-bucketed window for scrubbing |
79
+ | `replay.stream(opts, cb)` | **Server-paced WS playback** — historical tracks streamed like live (see example below) |
80
+ | `system.*` | `stats`, `infraHealth`, `getConfig`/`setConfig`, `clearTable`, `ingestHistory` |
81
+ | `trpc` / `rest` / `graphql` | Direct typed API clients for anything not wrapped above — **untyped** (`createTRPCProxyClient<any>`), so prefer the wrappers above; fall back to `client.trpc.<router>.<procedure>.query/mutate(...)` only for endpoints not yet wrapped |
82
+ | `cache` | A `TrackCache` instance (see below) |
83
+ | `getStatus()` / `getLastUpdateAt()` / `getError()` / `getSourceStatus()` | Connection introspection |
84
+ | `disconnect()` | Tear down the WS connection |
68
85
 
69
86
  ### Label category / color
70
87
 
@@ -106,6 +123,7 @@ The server sends JSON envelopes `{ type, seq, serverTime, payload }`. The client
106
123
  "targetType": "aircraft", // optional: aircraft | vehicle | unknown
107
124
  "vehicleFleet": 3, // optional, only for vehicles
108
125
  "airportId": "WARR", // optional ICAO scope
126
+ "dataSource": { "sac": 32, "sic": 136 }, // optional: the sensor behind the position shown (ASTERIX feeds)
109
127
  "lat": -7.3797, "lon": 112.7868,// number | null
110
128
  "altitudeFt": 0,
111
129
  "groundSpeedKt": 12,
@@ -127,23 +145,8 @@ The server sends JSON envelopes `{ type, seq, serverTime, payload }`. The client
127
145
  ```
128
146
 
129
147
  `status` and `labelColor` change only when the track's category or status actually changes — position-only deltas leave them untouched, so a label painted from `labelColor` does not flicker.
130
- | `tracks.snapshot(input?)` | One-shot current state via tRPC (`airportId`, `bbox`, `source`, `minConfidence`, `limit`, `offset`) |
131
- | `tracks.history(input)` | Persisted position history for one track (`trackId`, `airportId?`, `limit?`) — federated airports are served by their own backend |
132
- | `airports.*` | Airport registry: `list/create/update/delete` + `surface`/`setSurface` (runway/taxiway/apron GeoJSON for the map) |
133
- | `sources.*` | Register/list (optionally by `airportId`)/delete data sources (returns per-source auth tokens) |
134
- | `alerts.*` | Alert history + live alert subscription |
135
- | `rules.*` | Detection-rule CRUD — **global or per-airport** (see example below) |
136
- | `routes.*` | Route conformance: pre-defined ground routes + live track clearances (deviation alerts) |
137
- | `analytics.movements()` | Takeoffs/landings per hour + peak hour, per airport |
138
- | `webhooks.*` | Webhook CRUD |
139
- | `replay.days(input?)` | Data-availability calendar — which days have recorded tracks (see example below) |
140
- | `replay.range/seek/window` | Historical replay metadata: range summary, point-in-time seek, time-bucketed window for scrubbing |
141
- | `replay.stream(opts, cb)` | **Server-paced WS playback** — historical tracks streamed like live (see example below) |
142
- | `system.*` | `stats`, `infraHealth`, `getConfig`/`setConfig`, `clearTable`, `ingestHistory` |
143
- | `trpc` / `rest` / `graphql` | Direct typed API clients for anything not wrapped above — **untyped** (`createTRPCProxyClient<any>`), so prefer the wrappers above; fall back to `client.trpc.<router>.<procedure>.query/mutate(...)` only for endpoints not yet wrapped |
144
- | `cache` | A `TrackCache` instance (see below) |
145
- | `getStatus()` / `getLastUpdateAt()` / `getError()` / `getSourceStatus()` | Connection introspection |
146
- | `disconnect()` | Tear down the WS connection |
148
+
149
+ `dataSource` is the ASTERIX System Area / System Identification Code (SAC/SIC) of the sensor that reported the position shown (for a merged track, the sensor that is currently preferred). `formatDataSource(track)` gives the display string `"ADSB 32/136"` (`sourceLabel(track.source)` gives just the kind: `ADSB`, `SURFACE` for CAT010/011, `MLAT`, `RADAR`, `SYSTEM`, `FUSED`); without a SAC/SIC it is the kind alone.
147
150
 
148
151
  ## Per-domain usage examples
149
152
 
@@ -449,6 +452,24 @@ Shared live-track state; feed it from your subscription callbacks and read it im
449
452
  | `setStatus` / `setSourceStatus` / `setError` / `setDegraded` / `clear` | Connection metadata |
450
453
  | Selectors | `selectAllTracks`, `selectRenderableTracks`, `selectTrack(id)`, `selectTrackCount`, `selectStatus`, `selectIsDegraded`, `selectLastUpdateAt`, `selectError` |
451
454
 
455
+ ### REST errors: `AeroApiError`
456
+
457
+ Every failed REST call (`client.rest.*`, federated `tracks.history` and the like) throws an `AeroApiError` that keeps what the server said, instead of just `GET /x → 500`:
458
+
459
+ ```ts
460
+ try { await client.tracks.history({ trackId, airportId: "WARR" }); }
461
+ catch (e) {
462
+ if (e instanceof AeroApiError) {
463
+ e.status; // 403
464
+ e.code; // "FORBIDDEN" — stable, for switch; a newer server may send a code you do not know
465
+ e.recoverable; // retrying later may help (rate limit, server busy)
466
+ e.message; // "Federated request /tracks/t1/history → 403: not your airport"
467
+ e.details; // whatever the server attached
468
+ }
469
+ }
470
+ ```
471
+ Servers answer either in the standard format (`{ ok, data, meta }` / `{ ok: false, error: { code, message, recoverable } }`, see `@aeroswarm/core/api`) or in the older shape (bare data, `{ ok: false, error: "text" }`); both are read. Federated calls send `x-aero-envelope: 1` to get the standard format. `parseApiResponse(res)` and `readApiBody(res)` are exported for your own `fetch` calls.
472
+
452
473
  ### `useTrackMotion` / `createTrackMotion` — smooth live tracks
453
474
 
454
475
  Feeds update a track once a second (MLAT, ADS-B) or once per antenna sweep (radar, ~7.5 s). Drawn as they arrive, icons jump. `createTrackMotion` (no React needed) shows where the aircraft *probably is right now* and eases corrections in, like a flight tracker:
@@ -0,0 +1,75 @@
1
+ import { CurrentTrack } from '@aeroswarm/core/types';
2
+
3
+ /** What the engine reads from a track. */
4
+ type MotionInput = Pick<CurrentTrack, "trackId" | "lat" | "lon" | "groundSpeedKt" | "headingDeg" | "altitudeFt" | "status" | "lastSeenAt"> & {
5
+ targetType?: CurrentTrack["targetType"];
6
+ };
7
+ interface MotionSample {
8
+ lat: number;
9
+ lon: number;
10
+ /** Degrees 0..360, eased; null until a valid heading has been seen. */
11
+ headingDeg: number | null;
12
+ altitudeFt: number | null;
13
+ /** Speed is above `minMovingKt`. */
14
+ moving: boolean;
15
+ /** Position currently comes from dead reckoning (not frozen, limit not reached). */
16
+ extrapolating: boolean;
17
+ }
18
+ interface MotionFrame {
19
+ /** Server-aligned time of this frame (ms). */
20
+ at: number;
21
+ /** Only tracks whose picture changed since the previous frame. */
22
+ changed: ReadonlyMap<string, MotionSample>;
23
+ }
24
+ interface MotionScheduler {
25
+ request(callback: (timestamp: number) => void): unknown;
26
+ cancel(handle: unknown): void;
27
+ }
28
+ interface MotionOptions {
29
+ /** How long a correction takes to fade out. Default 1500 ms. */
30
+ blendMs?: number;
31
+ /** How long the icon takes to turn to a new heading. Default 800 ms. */
32
+ headingSmoothMs?: number;
33
+ /** Dead reckoning stops after this long without a new fix. Default 20 s in the air, 8 s on the surface. */
34
+ maxExtrapolateMs?: {
35
+ air: number;
36
+ surface: number;
37
+ };
38
+ /** Below this speed a track is treated as standing still. Default 5 kt. */
39
+ minMovingKt?: number;
40
+ /** A correction larger than this is a glitch or a sensor change far away: jump instead of easing. Default 5000 m. */
41
+ snapDistanceM?: number;
42
+ /** Frame rate limit for `start()`. Default 30. */
43
+ frameHz?: number;
44
+ /** Changes smaller than this are not reported. Default 0.5 m (and 0.5 degrees of heading). */
45
+ epsilonM?: number;
46
+ /** false = show raw positions (for comparing against another source). Default true. */
47
+ extrapolate?: boolean;
48
+ /** Decides what counts as "on the surface". Default: vehicles, or below 150 ft. */
49
+ isSurface?: (track: MotionInput) => boolean;
50
+ /** Browser clock. Default Date.now. Inject the replay clock to play back at another speed. */
51
+ clock?: () => number;
52
+ /** Frame timer. Default requestAnimationFrame, else setTimeout. Inject in tests. */
53
+ scheduler?: MotionScheduler;
54
+ }
55
+ interface TrackMotion {
56
+ /** `meta.serverTime` (from the WS envelope) keeps the server/browser clock offset current. */
57
+ update(track: MotionInput, meta?: {
58
+ serverTime?: number;
59
+ }): void;
60
+ remove(trackId: string): void;
61
+ clear(): void;
62
+ /** Where the track is shown at `at` (browser time; default now). Pure: does not touch frame state. */
63
+ sample(trackId: string, at?: number): MotionSample | undefined;
64
+ /** One frame: the tracks whose picture changed since the previous frame. `start()` calls this for you. */
65
+ tick(at?: number): ReadonlyMap<string, MotionSample>;
66
+ /** Runs frames until the returned function is called. Idles (no timer) while nothing moves. */
67
+ start(onFrame: (frame: MotionFrame) => void): () => void;
68
+ /** Tracks currently held. */
69
+ readonly size: number;
70
+ /** Current server minus browser clock, ms. */
71
+ offsetMs(): number;
72
+ }
73
+ declare function createTrackMotion(options?: MotionOptions): TrackMotion;
74
+
75
+ export { type MotionFrame as M, type TrackMotion as T, type MotionInput as a, type MotionOptions as b, type MotionSample as c, type MotionScheduler as d, createTrackMotion as e };
@@ -332,6 +332,38 @@ declare class TrackCache extends TypedEmitter<{
332
332
  })[];
333
333
  }
334
334
 
335
+ /** Mirrors `API_ERROR_CODES` in @aeroswarm/core/api. A server may send codes this client does not know. */
336
+ type ApiErrorCode = "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "BAD_REQUEST" | "CONFLICT" | "RATE_LIMITED" | "UNAVAILABLE" | "INTERNAL";
337
+ /** The code for an HTTP status when the server sent none. */
338
+ declare function codeForStatus(status: number): ApiErrorCode;
339
+ interface ApiErrorInit {
340
+ status: number;
341
+ code: string;
342
+ message: string;
343
+ recoverable: boolean;
344
+ details?: unknown;
345
+ }
346
+ /** A REST call failed. `code` is stable and meant for `switch`; `message` is for people. */
347
+ declare class AeroApiError extends Error {
348
+ readonly status: number;
349
+ readonly code: string;
350
+ /** Retrying later may succeed (rate limit, server busy or failing). */
351
+ readonly recoverable: boolean;
352
+ readonly details?: unknown;
353
+ constructor(init: ApiErrorInit);
354
+ }
355
+ /**
356
+ * The parsed JSON of a response, untouched. Throws `AeroApiError` for a non-2xx status
357
+ * and for a 2xx that carries `{ ok: false }`. For endpoints whose callers read the
358
+ * envelope themselves (`r.data.x`).
359
+ */
360
+ declare function readApiBody(res: Response, label?: string): Promise<unknown>;
361
+ /**
362
+ * The data of a response: unwrapped from `{ ok, data }`, or the body as it is for servers
363
+ * that answer with bare data. Throws `AeroApiError` on failure.
364
+ */
365
+ declare function parseApiResponse<T>(res: Response, label?: string): Promise<T>;
366
+
335
367
  interface AeroSwarmOptions {
336
368
  endpoint: string;
337
369
  token: string;
@@ -698,4 +730,4 @@ declare class AeroSwarm {
698
730
  reconnect(): boolean;
699
731
  }
700
732
 
701
- export { AeroReplayClient 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, AeroSwarm as a, type AeroSwarmOptions as b, AeroWsClient as c, type AftnMessageRow as d, type LabelCategory as e, type LabeledTrack as f, type LiveTrackCallbacks as g, type ReplayHandle as h, type ReplayProgress as i, type ReplayStreamOptions as j, type StreamMeta as k, type SubscribeOptions as l, type TrackCallbacks as m, type TrackHistoryPoint as n, getLabelCategory as o, withLabel as w };
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 };
package/dist/index.d.ts CHANGED
@@ -1,79 +1,26 @@
1
- export { A as AeroReplayClient, a as AeroSwarm, b as AeroSwarmOptions, c as AeroWsClient, d as AftnMessageRow, D as DayAvailability, F as FlightPlanRow, L as LABEL_COLORS, e as LabelCategory, f as LabeledTrack, g as LiveTrackCallbacks, R as ReplayCallbacks, h as ReplayHandle, i as ReplayProgress, j as ReplayStreamOptions, S as SourceRow, k as StreamMeta, l as SubscribeOptions, T as TrackCache, m as TrackCallbacks, n as TrackHistoryPoint, U as UseAeroTracksResult, o as getLabelCategory, w as withLabel } from './index-BXiTmKdS.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, 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';
2
2
  import { CurrentTrack } from '@aeroswarm/core/types';
3
- export { AeroConnectionStatus, AeroRecoverableError, Bbox, ConfidenceBudget, CurrentTrack, FlightPhase, FlightPlan, FlightPlanStatus, FlightPlanSummary, FusedTrackEvent, Position, Protocol, SourceHealthEvent, SourceStatus, StreamEnvelope, TrackSource, TrackStatus, Velocity } from '@aeroswarm/core/types';
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';
5
+ export { M as MotionFrame, a as MotionInput, b as MotionOptions, c as MotionSample, d as MotionScheduler, T as TrackMotion, e as createTrackMotion } from './index-ByNBpjNk.js';
5
6
  import '@trpc/client';
6
7
 
7
- /** What the engine reads from a track. */
8
- type MotionInput = Pick<CurrentTrack, "trackId" | "lat" | "lon" | "groundSpeedKt" | "headingDeg" | "altitudeFt" | "status" | "lastSeenAt"> & {
9
- targetType?: CurrentTrack["targetType"];
10
- };
11
- interface MotionSample {
12
- lat: number;
13
- lon: number;
14
- /** Degrees 0..360, eased; null until a valid heading has been seen. */
15
- headingDeg: number | null;
16
- altitudeFt: number | null;
17
- /** Speed is above `minMovingKt`. */
18
- moving: boolean;
19
- /** Position currently comes from dead reckoning (not frozen, limit not reached). */
20
- extrapolating: boolean;
21
- }
22
- interface MotionFrame {
23
- /** Server-aligned time of this frame (ms). */
24
- at: number;
25
- /** Only tracks whose picture changed since the previous frame. */
26
- changed: ReadonlyMap<string, MotionSample>;
27
- }
28
- interface MotionScheduler {
29
- request(callback: (timestamp: number) => void): unknown;
30
- cancel(handle: unknown): void;
31
- }
32
- interface MotionOptions {
33
- /** How long a correction takes to fade out. Default 1500 ms. */
34
- blendMs?: number;
35
- /** How long the icon takes to turn to a new heading. Default 800 ms. */
36
- headingSmoothMs?: number;
37
- /** Dead reckoning stops after this long without a new fix. Default 20 s in the air, 8 s on the surface. */
38
- maxExtrapolateMs?: {
39
- air: number;
40
- surface: number;
41
- };
42
- /** Below this speed a track is treated as standing still. Default 5 kt. */
43
- minMovingKt?: number;
44
- /** A correction larger than this is a glitch or a sensor change far away: jump instead of easing. Default 5000 m. */
45
- snapDistanceM?: number;
46
- /** Frame rate limit for `start()`. Default 30. */
47
- frameHz?: number;
48
- /** Changes smaller than this are not reported. Default 0.5 m (and 0.5 degrees of heading). */
49
- epsilonM?: number;
50
- /** false = show raw positions (for comparing against another source). Default true. */
51
- extrapolate?: boolean;
52
- /** Decides what counts as "on the surface". Default: vehicles, or below 150 ft. */
53
- isSurface?: (track: MotionInput) => boolean;
54
- /** Browser clock. Default Date.now. Inject the replay clock to play back at another speed. */
55
- clock?: () => number;
56
- /** Frame timer. Default requestAnimationFrame, else setTimeout. Inject in tests. */
57
- scheduler?: MotionScheduler;
58
- }
59
- interface TrackMotion {
60
- /** `meta.serverTime` (from the WS envelope) keeps the server/browser clock offset current. */
61
- update(track: MotionInput, meta?: {
62
- serverTime?: number;
63
- }): void;
64
- remove(trackId: string): void;
65
- clear(): void;
66
- /** Where the track is shown at `at` (browser time; default now). Pure: does not touch frame state. */
67
- sample(trackId: string, at?: number): MotionSample | undefined;
68
- /** One frame: the tracks whose picture changed since the previous frame. `start()` calls this for you. */
69
- tick(at?: number): ReadonlyMap<string, MotionSample>;
70
- /** Runs frames until the returned function is called. Idles (no timer) while nothing moves. */
71
- start(onFrame: (frame: MotionFrame) => void): () => void;
72
- /** Tracks currently held. */
73
- readonly size: number;
74
- /** Current server minus browser clock, ms. */
75
- offsetMs(): number;
76
- }
77
- declare function createTrackMotion(options?: MotionOptions): TrackMotion;
8
+ /**
9
+ * Short label for the kind of sensor behind a track.
10
+ *
11
+ * ADSB ADS-B (CAT021, or raw ADS-B)
12
+ * SURFACE surface movement data (CAT010 / CAT011): SMR, MLAT or an ADS-B station on the ground
13
+ * MLAT multilateration (CAT020)
14
+ * RADAR radar plots and tracks (CAT048)
15
+ * SYSTEM system track (CAT062)
16
+ * FUSED several sensors merged
17
+ */
18
+ type SourceLabel = "ADSB" | "SURFACE" | "MLAT" | "RADAR" | "SYSTEM" | "FUSED";
19
+ declare function sourceLabel(source: CurrentTrack["source"]): SourceLabel;
20
+ /**
21
+ * The sensor behind a track as a display string: `"ADSB 32/136"` (kind, then SAC/SIC),
22
+ * or just `"ADSB"` when the feed gave no SAC/SIC.
23
+ */
24
+ declare function formatDataSource(track: Pick<CurrentTrack, "source" | "dataSource">): string;
78
25
 
79
- export { type MotionFrame, type MotionInput, type MotionOptions, type MotionSample, type MotionScheduler, type TrackMotion, createTrackMotion };
26
+ export { type SourceLabel, formatDataSource, sourceLabel };
package/dist/index.js CHANGED
@@ -314,6 +314,85 @@ 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 parseApiResponse(res, label) {
392
+ const body = await readApiBody(res, label);
393
+ return isEnvelope(body) ? body.data : body;
394
+ }
395
+
317
396
  // src/sdk/api/rest.ts
318
397
  var AeroRestClient = class {
319
398
  constructor(endpoint, token) {
@@ -331,8 +410,7 @@ var AeroRestClient = class {
331
410
  },
332
411
  body: body ? JSON.stringify(body) : void 0
333
412
  });
334
- if (!res.ok) throw new Error(`${method} ${path} \u2192 ${res.status}`);
335
- return res.json();
413
+ return await readApiBody(res, `${method} ${path}`);
336
414
  }
337
415
  createSource(body) {
338
416
  return this.req("POST", "/v1/sources", body);
@@ -554,9 +632,10 @@ var AeroSwarm = class _AeroSwarm {
554
632
  async federatedFetch(base, path, query) {
555
633
  const url = new URL(`${base}${path}`);
556
634
  for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
557
- const res = await fetch(url, { headers: { Authorization: `Bearer ${this.opts.token}` } });
558
- if (!res.ok) throw new Error(`Federated request failed: ${res.status} ${res.statusText} (${url.pathname})`);
559
- return res.json();
635
+ const res = await fetch(url, {
636
+ headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
637
+ });
638
+ return parseApiResponse(res, `Federated request ${url.pathname}`);
560
639
  }
561
640
  tracks = {
562
641
  subscribe: (opts, callbacks) => {
@@ -1091,18 +1170,53 @@ var Motion = class {
1091
1170
  function createTrackMotion(options = {}) {
1092
1171
  return new Motion(options);
1093
1172
  }
1173
+
1174
+ // src/shared/source/index.ts
1175
+ function sourceLabel(source) {
1176
+ switch (source) {
1177
+ case "ADSB":
1178
+ case "ASTERIX_CAT021":
1179
+ return "ADSB";
1180
+ case "ASTERIX_CAT010":
1181
+ case "ASTERIX_CAT011":
1182
+ return "SURFACE";
1183
+ case "ASTERIX_CAT020":
1184
+ return "MLAT";
1185
+ case "ASTERIX_CAT048":
1186
+ return "RADAR";
1187
+ case "ASTERIX_CAT062":
1188
+ return "SYSTEM";
1189
+ case "FUSED":
1190
+ return "FUSED";
1191
+ default: {
1192
+ const unreachable = source;
1193
+ return unreachable;
1194
+ }
1195
+ }
1196
+ }
1197
+ function formatDataSource(track) {
1198
+ const kind = sourceLabel(track.source);
1199
+ const ds = track.dataSource;
1200
+ return ds ? `${kind} ${ds.sac}/${ds.sic}` : kind;
1201
+ }
1094
1202
  export {
1203
+ AeroApiError,
1095
1204
  AeroReplayClient,
1096
1205
  AeroSwarm,
1097
1206
  AeroWsClient,
1098
1207
  LABEL_COLORS,
1099
1208
  TrackCache,
1100
1209
  clamp,
1210
+ codeForStatus,
1101
1211
  createTrackMotion,
1212
+ formatDataSource,
1102
1213
  getLabelCategory,
1103
1214
  haversineKm,
1104
1215
  isInsideBbox,
1105
1216
  isRenderableTrack2 as isRenderableTrack,
1217
+ parseApiResponse,
1218
+ readApiBody,
1219
+ sourceLabel,
1106
1220
  toCurrentTrack,
1107
1221
  withLabel
1108
1222
  };